package store

import (
	"io"
	"log"
	"path/filepath"
	"testing"
)

func newResetStore(t *testing.T) *Store {
	t.Helper()
	st, err := New(filepath.Join(t.TempDir(), "t.db"), log.New(io.Discard, "", 0))
	if err != nil {
		t.Fatalf("store.New: %v", err)
	}
	t.Cleanup(func() { st.Close() })
	return st
}

// seedRetainedBlob leaves a superseded escrow blob reachable only via F-14 host_deletions provenance
// (host deleted, blob retained) — the customer-RESET escrow-custody target.
func seedRetainedBlob(t *testing.T, st *Store, customerID string) {
	t.Helper()
	hostID := customerID + "-h1"
	if err := st.UpsertHost(&Host{HostID: hostID, CustomerID: customerID, APIKey: "h"}); err != nil {
		t.Fatalf("upsert host: %v", err)
	}
	if _, _, err := st.SaveHostEscrow(hostID, []byte("A"), "fpA", "p", "2026-01-01T00:00:00Z", "shaA"); err != nil {
		t.Fatalf("escrow A: %v", err)
	}
	if _, _, err := st.SaveHostEscrow(hostID, []byte("B"), "fpB", "p", "2026-01-02T00:00:00Z", "shaB"); err != nil {
		t.Fatalf("escrow B: %v", err)
	}
	if err := st.DeleteHost(hostID, true); err != nil {
		t.Fatalf("delete host: %v", err)
	}
}

// The store-level ack contract: PurgeCustomerResetDBState must NEVER destroy retained escrow custody
// unless escrowAck is true — even while it clears the rest of the operational state. (The web handler
// gates ack separately; this proves the store primitive is itself honest.)
func TestPurgeCustomerResetDBState_AckGatesEscrow(t *testing.T) {
	st := newResetStore(t)
	if err := st.SaveCustomerConfig(&CustomerConfig{CustomerID: "acme", APIKey: "capi", RetrievalPassword: "pw"}); err != nil {
		t.Fatalf("seed config: %v", err)
	}
	seedRetainedBlob(t, st, "acme")
	if err := st.SaveOneTimeSecret("acme", "ots"); err != nil {
		t.Fatalf("seed ots: %v", err)
	}
	if err := st.SaveDRRecipeHostHalf("acme", "acme-h1", 1, []byte("half")); err != nil {
		t.Fatalf("seed dr: %v", err)
	}

	// ack=false: everything else goes, blobs SURVIVE.
	if err := st.PurgeCustomerResetDBState("acme", false); err != nil {
		t.Fatalf("purge (no ack): %v", err)
	}
	inv, err := st.CustomerResetInventory("acme")
	if err != nil {
		t.Fatal(err)
	}
	if inv.SupersededBlobs == 0 {
		t.Error("ack=false destroyed retained escrow custody")
	}
	if inv.OneTimeSecretPresent || inv.DRRecipePresent {
		t.Errorf("ack=false failed to clear the non-escrow state: %+v", inv)
	}

	// ack=true: blobs go too.
	if err := st.PurgeCustomerResetDBState("acme", true); err != nil {
		t.Fatalf("purge (ack): %v", err)
	}
	if inv, _ := st.CustomerResetInventory("acme"); inv.SupersededBlobs != 0 {
		t.Errorf("ack=true failed to destroy retained custody: %d", inv.SupersededBlobs)
	}
	// Identity SURVIVES the purge.
	if cfg, _ := st.GetCustomerConfig("acme"); cfg == nil {
		t.Error("purge destroyed customer_config — identity must survive")
	}
}

// The journal round-trips: open → per-leg status → finish, and Latest reflects it.
func TestCustomerResetJournal_RoundTrip(t *testing.T) {
	st := newResetStore(t)
	id, err := st.StartCustomerReset("acme", true)
	if err != nil {
		t.Fatalf("start: %v", err)
	}
	if err := st.UpdateResetLeg(id, "hetzner", "ok"); err != nil {
		t.Fatalf("leg: %v", err)
	}
	if err := st.UpdateResetLeg(id, "pbs", "failed"); err != nil {
		t.Fatalf("leg: %v", err)
	}
	if err := st.UpdateResetLeg(id, "pbs", "ok"); err != nil { // overwrite on re-run
		t.Fatalf("leg: %v", err)
	}
	cr, err := st.LatestCustomerReset("acme")
	if err != nil || cr == nil {
		t.Fatalf("latest: %v / %v", cr, err)
	}
	if cr.CompletedAt != nil {
		t.Error("not finished yet — CompletedAt should be nil")
	}
	if !cr.EscrowAcked || cr.Legs["hetzner"] != "ok" || cr.Legs["pbs"] != "ok" {
		t.Errorf("journal wrong: %+v", cr)
	}
	if err := st.FinishCustomerReset(id); err != nil {
		t.Fatalf("finish: %v", err)
	}
	if cr, _ := st.LatestCustomerReset("acme"); cr.CompletedAt == nil {
		t.Error("finish did not stamp CompletedAt")
	}
}
