package report

import (
	"context"
	"log"
	"strings"
	"testing"

	"bytes"
)

// Scenario F (v0.127.0) — the escrowed-state stale-blob re-check. The §8 truth table's NEW rows:
// an ESCROWED box whose hub blob does not cover the current password (hash mismatch, or a present
// blob with an EMPTY hash — the spike's hash-less supersession) raises a display-only stale flag
// + ONE warn per distinct hub hash. State never flips; nothing blocks.

type staleHarness struct {
	*confirmerHarness
	escrowed bool
}

func newStaleHarness(t *testing.T) *staleHarness {
	t.Helper()
	h := &staleHarness{confirmerHarness: &confirmerHarness{local: hubHash, localOK: true, logbuf: &bytes.Buffer{}}}
	h.escrowed = true // the box state under test
	h.c = &EscrowAutoConfirmer{
		Pending:   func() bool { return h.pending },
		Escrowed:  func() bool { return h.escrowed },
		LocalHash: func() (string, bool) { return h.local, h.localOK },
		Flip:      func() error { h.flips++; return nil },
		Wipe:      func(context.Context) error { h.wipes++; return nil },
		Logger:    log.New(h.logbuf, "", 0),
	}
	return h
}

// escrowed + hash mismatch → stale flag + ONE warn (deduped per distinct hub hash), no flip.
func TestEscrowStale_MismatchWarnsOnceAndFlags(t *testing.T) {
	h := newStaleHarness(t)
	h.local = otherHash

	h.c.Reconcile(matchStatus(hubHash))
	if !h.c.StaleBlob() {
		t.Fatal("mismatch on an escrowed box must raise the stale flag")
	}
	if h.flips != 0 {
		t.Fatal("the stale re-check must NEVER flip state (no auto-UN-confirm)")
	}
	if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 1 {
		t.Fatalf("want exactly 1 STALE warn, got %d: %s", got, h.logbuf.String())
	}
	// Dedupe: the same hub hash again → still exactly one warn; the flag stays up.
	h.c.Reconcile(matchStatus(hubHash))
	if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 1 {
		t.Fatalf("same stale hash must warn ONCE, got %d", got)
	}
	if !h.c.StaleBlob() {
		t.Fatal("flag must persist across deduped ACKs")
	}
	// A NEW distinct stale hash → warns again.
	h.c.Reconcile(matchStatus("2222222222222222222222222222222222222222222222222222222222222222"))
	if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 2 {
		t.Fatalf("a new distinct stale hash must warn again, got %d", got)
	}
}

// escrowed + blob present with an EMPTY hash (the spike's exact hash-less supersession) → stale
// + one warn under the hashless dedupe sentinel.
func TestEscrowStale_HashlessBlobWarnsOnce(t *testing.T) {
	h := newStaleHarness(t)

	h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: true}) // blob present, hash empty
	if !h.c.StaleBlob() {
		t.Fatal("a hash-less superseding blob must raise the stale flag")
	}
	if got := strings.Count(h.logbuf.String(), "NO password hash"); got != 1 {
		t.Fatalf("want the hash-less warn once, got %d: %s", got, h.logbuf.String())
	}
	h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: false}) // K-only legacy shape — still hash-less
	if got := strings.Count(h.logbuf.String(), "NO password hash"); got != 1 {
		t.Fatalf("hash-less must dedupe under its sentinel, got %d warns", got)
	}
}

// escrowed + hash MATCHES → clears an earlier stale flag; no warn on the clean path.
func TestEscrowStale_MatchClearsFlag(t *testing.T) {
	h := newStaleHarness(t)
	h.local = otherHash
	h.c.Reconcile(matchStatus(hubHash)) // go stale
	if !h.c.StaleBlob() {
		t.Fatal("setup: expected stale")
	}
	h.local = hubHash
	h.c.Reconcile(matchStatus(hubHash)) // a covering blob arrives (re-ceremony ran)
	if h.c.StaleBlob() {
		t.Fatal("a matching hash must CLEAR the stale flag")
	}
	// And a clean box never warns.
	h2 := newStaleHarness(t)
	h2.c.Reconcile(matchStatus(hubHash))
	if h2.c.StaleBlob() || strings.Contains(h2.logbuf.String(), "STALE") {
		t.Fatalf("match must be silent: %s", h2.logbuf.String())
	}
}

// Not-escrowed / no-local-password / nil-status rows: the re-check never runs (silent).
func TestEscrowStale_SilentRows(t *testing.T) {
	h := newStaleHarness(t)
	h.escrowed = false // offbox not configured (or any non-escrowed state)
	h.c.Reconcile(matchStatus(otherHash))
	if h.c.StaleBlob() || h.logbuf.Len() != 0 {
		t.Fatalf("non-escrowed must be silent: %s", h.logbuf.String())
	}

	h2 := newStaleHarness(t)
	h2.localOK = false // no local repo password file
	h2.c.Reconcile(matchStatus(otherHash))
	if h2.c.StaleBlob() || h2.logbuf.Len() != 0 {
		t.Fatal("no local password → nothing to compare → silent")
	}

	h3 := newStaleHarness(t)
	h3.c.Reconcile(nil) // no escrow row at all (blob absent — out of the truth table)
	if h3.c.StaleBlob() || h3.logbuf.Len() != 0 {
		t.Fatal("nil status must be silent")
	}
}

// A fresh pending→escrowed auto-confirm clears any stale leftovers (the flag must not survive a
// successful re-ceremony's confirm).
func TestEscrowStale_AutoConfirmClears(t *testing.T) {
	h := newStaleHarness(t)
	h.local = otherHash
	h.c.Reconcile(matchStatus(hubHash)) // stale while escrowed
	if !h.c.StaleBlob() {
		t.Fatal("setup: expected stale")
	}
	// The re-ceremony re-staged + re-uploaded; the box re-enters pending (edit flow) and the new
	// blob covers the local password → auto-confirm path runs and must clear the flag.
	h.escrowed = false
	h.pending = true
	h.c.Reconcile(matchStatus(otherHash))
	if h.flips != 1 {
		t.Fatal("setup: auto-confirm should have flipped")
	}
	if h.c.StaleBlob() {
		t.Fatal("a hub-verified auto-confirm must clear the stale flag")
	}
}
