package api

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

// R-197 (v0.93.0) — a changed offsite repository key must be REPORTED. Both halves of the comparison
// (host_escrow.restic_pw_sha256 and the row it replaces) have been in the database since SLICE 3 and
// nothing read them: demo-felhom's repository password changed on 2026-08-03, orphaning 36 snapshots /
// 1.14 GB, and no event, e-mail, card or log line said so for thirteen hours.
//
// Driven through the real endpoint (PUT /hosts/<id>/escrow), not by calling the emitter, so the wiring
// is part of what is proven.

// escrowBodyWithHash is escrowBody plus the sealed-password hash and an identity blob — the fork-4
// upload shape. Hash VALUES here are test fixtures, never real secrets.
func escrowBodyWithHash(blob []byte, resticPwSHA, identity string) string {
	m := map[string]string{
		"blob_b64":         base64.StdEncoding.EncodeToString(blob),
		"key_fingerprint":  "ab:cd:ef",
		"posture":          "zero_knowledge",
		"created_at":       "2026-08-04T05:00:00Z",
		"restic_pw_sha256": resticPwSHA,
	}
	if identity != "" {
		m["identity_blob_b64"] = base64.StdEncoding.EncodeToString([]byte(identity))
	}
	b, _ := json.Marshal(m)
	return string(b)
}

// Scenario D — a new escrow sealing a DIFFERENT repo password raises exactly one operator signal,
// naming the host, carrying NO hash value.
// RED-PROOF: remove the maybeEmitRepoKeyChanged call from handleHostEscrowPut (or make its predicate
// always fall through) → no event → this FAILS.
func TestEscrowPut_ChangedRepoKey_RaisesSignal(t *testing.T) {
	h, st, _ := newTestHandler(t)
	st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})

	if rr := do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", escrowBodyWithHash([]byte("k1"), "SHA_GEN1", "age-gen1")); rr.Code != http.StatusOK {
		t.Fatalf("first PUT = %d (%s)", rr.Code, rr.Body.String())
	}
	// Generation 1 is onboarding, not a change — nothing may fire yet.
	if ev, _ := st.GetLatestEventByType("c1", eventRepoKeyChanged); ev != nil {
		t.Fatalf("a FIRST escrow must not raise %s: %+v", eventRepoKeyChanged, ev)
	}

	if rr := do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", escrowBodyWithHash([]byte("k2"), "SHA_GEN2", "age-gen2")); rr.Code != http.StatusOK {
		t.Fatalf("second PUT = %d (%s)", rr.Code, rr.Body.String())
	}

	ev, err := st.GetLatestEventByType("c1", eventRepoKeyChanged)
	if err != nil {
		t.Fatal(err)
	}
	if ev == nil {
		t.Fatal("R-197: the repository key demonstrably changed and NO signal was raised — this is the " +
			"thirteen-hour silence that let demo-felhom lose 1.14 GB unremarked")
	}
	if ev.Severity != "warning" {
		t.Errorf("severity = %q, want warning (info is an intentional non-notify — the operator would not be told)", ev.Severity)
	}
	if !strings.Contains(ev.Message, "h1") {
		t.Errorf("message must name the host, got %q", ev.Message)
	}
	// NO HASH VALUE may travel — not in the message, not in the details.
	for _, forbidden := range []string{"SHA_GEN1", "SHA_GEN2"} {
		if strings.Contains(ev.Message, forbidden) || strings.Contains(ev.DetailsJSON, forbidden) {
			t.Errorf("a repo-password hash value leaked into the event (%s): msg=%q details=%q", forbidden, ev.Message, ev.DetailsJSON)
		}
	}
	// Exactly one, not one per anything.
	evs, _ := st.GetRecentEvents("c1", 50)
	n := 0
	for _, e := range evs {
		if e.EventType == eventRepoKeyChanged {
			n++
		}
	}
	if n != 1 {
		t.Fatalf("%s fired %d times for one supersession, want 1 (edge-triggered)", eventRepoKeyChanged, n)
	}
}

// Scenario E — an UNCHANGED key says nothing. A customer re-running a ceremony for good reasons must
// not be punished for it with an alarm.
func TestEscrowPut_UnchangedRepoKey_Silent(t *testing.T) {
	h, st, _ := newTestHandler(t)
	st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})

	do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", escrowBodyWithHash([]byte("k1"), "SHA_SAME", "age-1"))
	do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", escrowBodyWithHash([]byte("k2"), "SHA_SAME", "age-2"))

	if ev, _ := st.GetLatestEventByType("c1", eventRepoKeyChanged); ev != nil {
		t.Fatalf("a same-hash re-ceremony must be SILENT, got: %+v", ev)
	}
	// And it must not have created a superseded row either (pre-existing contract, re-asserted here
	// because Scenario E's silence would also be produced by the supersede branch never running).
	if n, _ := st.CountSupersededEscrow("h1"); n != 0 {
		t.Fatalf("same-hash re-upload created %d superseded row(s), want 0", n)
	}
}

// The in-between shapes must not be silent-and-indistinguishable either: a hash-less NEW blob cannot
// show whether the key changed, and saying nothing at all would look identical to "unchanged".
// Asserted at the level the code offers — no event (correct: nothing was measured) and no crash.
func TestEscrowPut_HashlessSupersession_NoSignal(t *testing.T) {
	h, st, _ := newTestHandler(t)
	st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})

	do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", escrowBodyWithHash([]byte("k1"), "SHA_GEN1", "age-1"))
	if rr := do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", escrowBodyWithHash([]byte("k2"), "", "age-2")); rr.Code != http.StatusOK {
		t.Fatalf("hash-less supersession PUT = %d", rr.Code)
	}
	if ev, _ := st.GetLatestEventByType("c1", eventRepoKeyChanged); ev != nil {
		t.Fatalf("a hash-less new blob cannot EVIDENCE a change; it must not claim one: %+v", ev)
	}
	// The retention still ran (the hashes differ, so this IS a supersession) — the key is kept even
	// though the change cannot be evidenced.
	if n, _ := st.CountSupersededEscrow("h1"); n != 1 {
		t.Fatalf("superseded rows = %d, want 1", n)
	}
}

// R-97a's lesson, pinned in the SAME commit that mints the type: an operator-tier event that is not in
// operatorOnlyEvents reaches customers as raw English, because a missing customerMessages entry is NOT
// a block. Checked here rather than in notify so the type's two registers are asserted together.
func TestRepoKeyChanged_IsOperatorOnly(t *testing.T) {
	if !notify.IsOperatorOnly(eventRepoKeyChanged) {
		t.Fatalf("%s is not registered operator-only — a customer would receive operator-grade English "+
			"about escrow custody", eventRepoKeyChanged)
	}
	// The two R-192 types have the same property and the same reason (see the register's comment).
	for _, et := range []string{"offsite_delivery_stuck", "offsite_credential_restaged"} {
		if !notify.IsOperatorOnly(et) {
			t.Errorf("%s is not registered operator-only", et)
		}
	}
}
