package store

import (
	"bytes"
	"testing"
)

// R-198 (v0.93.0) — the retention must keep the OFF-SITE data key, not only the PBS key.
//
// WHAT WAS BROKEN, and why these tests are the ones that would have caught it: host_escrow_superseded
// shipped with `blob` (the K-escrow / PBS datastore key) and `identity_blob` was added to host_escrow
// by a later ALTER and never to the retained table. The restic REPOSITORY password lives inside
// identity_blob. So every supersession retained the whole-guest key and destroyed the off-site data
// key — the exact secret the retention exists to preserve — and the destroying act is the escrow
// ceremony a rebuilt box asks its customer to run.
//
// The pre-existing TestSaveHostEscrow_RetainsSuperseded was GREEN throughout, because it asserts the
// MECHANISM (a retained row exists, with the old K-blob) and not the CONSEQUENCE (the retained row can
// still yield a repository password). These assert the consequence.

// Scenario A — a re-escrow retains BOTH sealed keys.
// RED-PROOF: drop `identity_blob` from demoteCurrentEscrowTx's INSERT/SELECT (production behaviour up
// to v0.92.0) → the retained row's identity blob is nil → this FAILS.
func TestSaveHostEscrow_RetainsIdentityBlob(t *testing.T) {
	st := newTestStore(t)
	const h = "hid-1"
	oldIdentity := []byte("age-blob-sealing-REPO-PASSWORD-OLD")
	newIdentity := []byte("age-blob-sealing-REPO-PASSWORD-NEW")

	// Generation 1: the K-escrow, then the identity blob — the real order the escrow PUT uses
	// (SaveHostEscrow, then SaveHostDRBundle).
	if _, _, err := st.SaveHostEscrow(h, []byte("k-blob-old"), "fp-old", "zk", "2026-07-09T00:00:00Z", "SHA_OLD"); err != nil {
		t.Fatal(err)
	}
	if err := st.SaveHostDRBundle(h, oldIdentity, `{"gen":1}`); err != nil {
		t.Fatal(err)
	}

	// Generation 2 with a DIFFERENT sealed repo password → supersede.
	sup, prev, err := st.SaveHostEscrow(h, []byte("k-blob-new"), "fp-new", "zk", "2026-07-16T00:00:00Z", "SHA_NEW")
	if err != nil {
		t.Fatal(err)
	}
	if !sup {
		t.Fatal("a different-passphrase upload must supersede")
	}
	if prev != "SHA_OLD" {
		t.Fatalf("prevResticPwSHA256 = %q, want SHA_OLD (R-197 needs the replaced hash)", prev)
	}
	if err := st.SaveHostDRBundle(h, newIdentity, `{"gen":2}`); err != nil {
		t.Fatal(err)
	}

	retained, err := st.ListSupersededEscrow(h)
	if err != nil {
		t.Fatal(err)
	}
	if len(retained) != 1 {
		t.Fatalf("retained rows = %d, want 1", len(retained))
	}
	// THE ASSERTION THIS WHOLE ITEM IS ABOUT: the off-site data key survived the supersession.
	if retained[0].IdentityBlob == nil {
		t.Fatal("R-198: the retained row carries NO identity blob — the off-site repository password " +
			"was destroyed by the ceremony that was supposed to preserve it")
	}
	// And it is the PREVIOUS generation's blob, not the one that replaced it. This pins the ordering
	// dependency named on demoteCurrentEscrowTx: the identity blob is written AFTER SaveHostEscrow, so
	// the demote sees the old one. If that order ever inverts, the retained bytes would silently be
	// the new blob filed under the old hash — recoverable-looking and wrong.
	if !bytes.Equal(retained[0].IdentityBlob, oldIdentity) {
		t.Fatalf("retained identity blob is not the PREVIOUS generation (got %q) — the demote ran after the overwrite",
			retained[0].IdentityBlob)
	}
	if retained[0].ResticPwSHA256 != "SHA_OLD" || string(retained[0].Blob) != "k-blob-old" {
		t.Fatalf("retained row is not the old generation: %+v", retained[0])
	}
	// Current row unchanged in behaviour: the NEW generation, both blobs.
	if bundle, berr := st.GetHostDRBundle(h); berr != nil || bundle == nil || !bytes.Equal(bundle.IdentityBlob, newIdentity) {
		t.Fatalf("current identity blob is not the new one: %+v (%v)", bundle, berr)
	}
}

// Scenario B — deleting a host demotes BOTH blobs too. demoteCurrentEscrowTx is shared by the
// re-escrow path and the host-delete path; a shared routine proven through one caller is how a fix
// gets believed on a path nobody exercised.
// RED-PROOF: fix only the re-escrow caller (e.g. carry the column in SaveHostEscrow's own SQL instead
// of in the shared routine) → this FAILS while Scenario A passes.
func TestDeleteHost_DemotesIdentityBlob(t *testing.T) {
	s := newTestStore(t)
	const hostID, cust = "hid-del", "cust-del"
	identity := []byte("age-blob-sealing-REPO-PASSWORD")
	if err := s.UpsertHost(&Host{HostID: hostID, CustomerID: cust, APIKey: "k"}); err != nil {
		t.Fatal(err)
	}
	if _, _, err := s.SaveHostEscrow(hostID, []byte("k-blob"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA_A"); err != nil {
		t.Fatal(err)
	}
	if err := s.SaveHostDRBundle(hostID, identity, `{}`); err != nil {
		t.Fatal(err)
	}

	if err := s.DeleteHost(hostID, true); err != nil {
		t.Fatalf("DeleteHost: %v", err)
	}
	retained, err := s.ListSupersededEscrow(hostID)
	if err != nil {
		t.Fatal(err)
	}
	if len(retained) != 1 {
		t.Fatalf("demoted rows = %d, want 1", len(retained))
	}
	if !bytes.Equal(retained[0].IdentityBlob, identity) {
		t.Fatalf("R-198: host delete demoted custody WITHOUT the identity blob (got %q) — the off-site "+
			"repository password was destroyed by a host delete", retained[0].IdentityBlob)
	}
}

// Scenario C — a legacy host whose current escrow has NO identity blob supersedes cleanly. The column
// is nullable on purpose: a NOT NULL constraint here would make the fix block a ceremony, which is a
// worse failure than the one it repairs.
func TestSaveHostEscrow_SupersedesWithoutIdentityBlob(t *testing.T) {
	st := newTestStore(t)
	const h = "hid-legacy"
	if _, _, err := st.SaveHostEscrow(h, []byte("k-old"), "fp", "zk", "2026-07-09T00:00:00Z", "SHA_OLD"); err != nil {
		t.Fatal(err)
	}
	// No SaveHostDRBundle — a slice-7-era upload.
	sup, prev, err := st.SaveHostEscrow(h, []byte("k-new"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA_NEW")
	if err != nil {
		t.Fatalf("a supersession of an identity-less escrow must not fail: %v", err)
	}
	if !sup || prev != "SHA_OLD" {
		t.Fatalf("superseded=%v prev=%q, want true/SHA_OLD", sup, prev)
	}
	retained, err := st.ListSupersededEscrow(h)
	if err != nil {
		t.Fatal(err)
	}
	if len(retained) != 1 {
		t.Fatalf("retained rows = %d, want 1", len(retained))
	}
	if retained[0].IdentityBlob != nil {
		t.Fatalf("a legacy row must retain a NULL identity blob, got %q", retained[0].IdentityBlob)
	}
}

// CountCurrentEscrowWithIdentity is the census §8.1 asks for: which hosts hold an identity blob today
// and are therefore protected from the next ceremony by this fix. Asserted rather than eyeballed,
// because the report quotes its numbers.
func TestCountCurrentEscrowWithIdentity(t *testing.T) {
	st := newTestStore(t)
	if _, _, err := st.SaveHostEscrow("with-id", []byte("k"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA1"); err != nil {
		t.Fatal(err)
	}
	if err := st.SaveHostDRBundle("with-id", []byte("age-blob"), `{}`); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.SaveHostEscrow("without-id", []byte("k"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA2"); err != nil {
		t.Fatal(err)
	}
	total, withIdentity, err := st.CountCurrentEscrowWithIdentity()
	if err != nil {
		t.Fatal(err)
	}
	if total != 2 || withIdentity != 1 {
		t.Fatalf("census = %d/%d, want 2 total / 1 with identity", withIdentity, total)
	}
}
