package store

import (
	"database/sql"
	"errors"
	"io"
	"log"
	"path/filepath"
	"testing"
)

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

// R-204 item 4 / R-193 — THE FACT THE WHOLE DESIGN RESTS ON: the one-time offsite secret's VALUE
// survives consumption, so it can be re-armed without any storage-provider call.
//
// This is asserted rather than inherited from the PBS analogy on purpose. The two secrets are
// different objects with different lifecycles, and assuming a shared shape is exactly how two earlier
// sessions confused the two credentials. If a future change ever clears `value` on consume — a
// perfectly reasonable-looking hardening — restage-before-mint becomes silently impossible and every
// rebuild turns into an external mint. This test fails the moment that happens.
func TestRestageOneTimeSecret_ReArmsTheSameValue(t *testing.T) {
	st := newRestageStore(t)
	const cust, pw = "c1", "the-one-time-password"

	if err := st.SaveOneTimeSecret(cust, pw); err != nil {
		t.Fatal(err)
	}
	got, err := st.ConsumeOneTimeSecret(cust)
	if err != nil || got != pw {
		t.Fatalf("first consume: got %q err=%v", got, err)
	}
	// Single-use holds before the restage.
	if _, err := st.ConsumeOneTimeSecret(cust); !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("a consumed secret was served twice: err=%v", err)
	}

	restaged, err := st.RestageOneTimeSecret(cust)
	if err != nil {
		t.Fatal(err)
	}
	if !restaged {
		t.Fatal("an existing secret row reported nothing to re-stage")
	}

	// THE ASSERTION: the SAME value comes back. Not "a value" — the same one, which is what makes
	// this a re-arm rather than a silent mint.
	again, err := st.ConsumeOneTimeSecret(cust)
	if err != nil {
		t.Fatalf("consume after restage: %v", err)
	}
	if again != pw {
		t.Fatalf("the re-armed secret changed value: got %q want %q — the value did NOT survive the consume, so restage-before-mint is impossible and the reconciler must mint instead", again, pw)
	}
	// And it is single-use again.
	if _, err := st.ConsumeOneTimeSecret(cust); !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("the re-armed secret was not single-use: err=%v", err)
	}
}

// No row → restaged=false, so the caller escalates to a fresh mint instead of silently doing nothing.
func TestRestageOneTimeSecret_NoRowReportsFalse(t *testing.T) {
	st := newRestageStore(t)
	restaged, err := st.RestageOneTimeSecret("nobody")
	if err != nil {
		t.Fatal(err)
	}
	if restaged {
		t.Fatal("re-staging a customer with no stored secret claimed success — the caller would never escalate")
	}
}

// A restage must not touch the timestamps' meaning beyond the consumed flag: created_at stays put, so
// the delivery-state age math is unchanged.
func TestRestageOneTimeSecret_ClearsOnlyTheConsumedFlag(t *testing.T) {
	st := newRestageStore(t)
	const cust = "c1"
	if err := st.SaveOneTimeSecret(cust, "pw"); err != nil {
		t.Fatal(err)
	}
	before, err := st.GetOneTimeSecretInfo(cust)
	if err != nil || before == nil {
		t.Fatalf("info before: %v", err)
	}
	if _, err := st.ConsumeOneTimeSecret(cust); err != nil {
		t.Fatal(err)
	}
	if _, err := st.RestageOneTimeSecret(cust); err != nil {
		t.Fatal(err)
	}
	after, err := st.GetOneTimeSecretInfo(cust)
	if err != nil || after == nil {
		t.Fatalf("info after: %v", err)
	}
	if !after.ConsumedAt.IsZero() {
		t.Errorf("consumed_at was not cleared: %v", after.ConsumedAt)
	}
	if !after.CreatedAt.Equal(before.CreatedAt) {
		t.Errorf("created_at moved on a restage: %v → %v (the delivery-state age math would jump)", before.CreatedAt, after.CreatedAt)
	}
}

// reportHasOffsite is TIGHTENED in v0.96.0 to require enabled:true, because controller v0.199.0 sends
// an offsite object with enabled:false to ASK for a credential. This test pins BOTH halves: the
// tightening works, AND it is a no-op for every report shape that existed before — the equivalence is
// measured here rather than argued in a comment.
func TestReportHasOffsite_EnabledOnly(t *testing.T) {
	cases := []struct {
		name   string
		report string
		want   bool
	}{
		// The PRE-v0.199.0 shapes. These are the equivalence claim: every object the old controller
		// could ever attach carried enabled:true, so the tightening changes none of them.
		{"configured box (pre-0.199 shape)", `{"offsite":{"enabled":true,"escrow_state":"escrowed","last_status":"ok"}}`, true},
		{"configured box, minimal", `{"offsite":{"enabled":true}}`, true},
		{"no offsite object", `{"controller_version":"0.198.0"}`, false},
		{"explicit null", `{"offsite":null}`, false},
		{"unparseable", `{not json`, false},

		// The v0.199.0 DECLARATION — a request for help, not evidence of an applied tier.
		{"stranded rebuild declaring", `{"offsite":{"enabled":false,"state":"needs_credential"}}`, false},
		{"disabled object without a state", `{"offsite":{"enabled":false}}`, false},
	}
	for _, tc := range cases {
		if got := reportHasOffsite(tc.report); got != tc.want {
			t.Errorf("%s: reportHasOffsite = %v, want %v — a declaration read as an applied tier makes DeliveryStateFor say the stranded box is fine", tc.name, got, tc.want)
		}
	}
}

// The declaration is readable as such, and an older controller's report yields "" rather than an error.
func TestLatestReportOffsiteDeclaration(t *testing.T) {
	st := newRestageStore(t)
	const cust = "c1"

	found, _, state, err := st.LatestReportOffsiteDeclaration(cust)
	if err != nil {
		t.Fatal(err)
	}
	if found {
		t.Fatal("a customer with no reports was reported as found")
	}

	if err := st.SaveReport(cust, []byte(`{"controller_version":"0.198.0"}`)); err != nil {
		t.Fatal(err)
	}
	found, id1, state, err := st.LatestReportOffsiteDeclaration(cust)
	if err != nil || !found {
		t.Fatalf("found=%v err=%v", found, err)
	}
	if state != "" {
		t.Errorf("an old controller's report declared %q", state)
	}

	if err := st.SaveReport(cust, []byte(`{"offsite":{"enabled":false,"state":"needs_credential"}}`)); err != nil {
		t.Fatal(err)
	}
	found, id2, state, err := st.LatestReportOffsiteDeclaration(cust)
	if err != nil || !found {
		t.Fatalf("found=%v err=%v", found, err)
	}
	if state != "needs_credential" {
		t.Errorf("declared state = %q, want needs_credential", state)
	}
	if id2 <= id1 {
		t.Errorf("the report id did not advance (%d → %d) — the debounce counts distinct reports and would never confirm", id1, id2)
	}
}
