package monitor

import (
	"encoding/json"
	"strings"
	"testing"
	"time"

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

// R-192 (v0.93.0) — the alert must describe what was MEASURED, and a refusal to self-heal must leave a
// trace.
//
// What was wrong, quoted from the live e-mail demo-hp sent daily from 2026-08-03: "one-time password
// consumed 284h19m0s ago and 500 report(s) since carry no offbox target — the credential is likely
// burned … Re-issue delivers a fresh one." OffsiteReportsSinceConsume for that same call was 500:
// every one of those reports carried a target. The message stated the precise negation of its own
// measurement and prescribed a remedy for a failure mode that had not occurred — while the self-heal
// correctly declined, in complete silence.

// regressedFixture puts c1 into demo-hp's real shape: consumed >1h ago, reports WITH an offbox target
// after the consume, then reports WITHOUT — the latest carrying none.
//
// The report timestamps get jitter deliberately: a perfectly regular series lands exactly on
// boundaries and survives mutations that a realistic one catches.
func (h dtHarness) regressedFixture(t *testing.T, withOffsite, without int) {
	t.Helper()
	if err := h.st.SaveOneTimeSecret("c1", "x"); err != nil {
		t.Fatal(err)
	}
	consumed := time.Now().UTC().Add(-30 * time.Hour).Format("2006-01-02 15:04:05")
	staged := time.Now().UTC().Add(-31 * time.Hour).Format("2006-01-02 15:04:05")
	if err := h.st.SetOneTimeSecretTimesForTest("c1", staged, consumed); err != nil {
		t.Fatal(err)
	}
	jitter := []int{0, 3, 1, 7, 2, 5, 11, 4}
	for i := 0; i < withOffsite; i++ {
		if err := h.st.SaveReport("c1", []byte(dtReportWithOffsite)); err != nil {
			t.Fatal(err)
		}
		time.Sleep(time.Duration(jitter[i%len(jitter)]) * time.Millisecond)
	}
	for i := 0; i < without; i++ {
		if err := h.st.SaveReport("c1", []byte(dtReportNoOffsite)); err != nil {
			t.Fatal(err)
		}
		time.Sleep(time.Duration(jitter[(i+3)%len(jitter)]) * time.Millisecond)
	}
}

func (h dtHarness) refusalRows(t *testing.T) []store.NotificationLogEntry {
	t.Helper()
	all, err := h.st.GetRecentNotifications("c1", 50)
	if err != nil {
		t.Fatal(err)
	}
	var out []store.NotificationLogEntry
	for _, n := range all {
		if n.Status == "refused" {
			out = append(out, n)
		}
	}
	return out
}

// Scenario F — the message states the measured state, not a denied cause.
// RED-PROOF: restore the old single hardcoded sentence (interpolate ReportsSinceConsume into
// "report(s) since carry no offbox target") → the regressed assertions FAIL.
func TestDeliveryChecker_RegressedShape_MessageMatchesMeasurement(t *testing.T) {
	h := newDTHarness(t, true)
	h.regressedFixture(t, 6, 2)

	h.checker.Check()

	saved := h.savedEvents(t, "offsite_delivery_stuck")
	if len(saved) != 1 {
		t.Fatalf("stuck events = %d, want 1", len(saved))
	}
	msg := saved[0].Message

	// It must NOT assert the thing the data contradicts.
	if strings.Contains(msg, "carry no offbox target") || strings.Contains(msg, "likely burned") {
		t.Fatalf("R-192(a): the message still asserts a burned credential on a REGRESSED shape — this is "+
			"the sentence that stated the negation of its own measurement:\n%s", msg)
	}
	// It must name the shape it detected and the measurement behind it.
	if !strings.Contains(msg, "REGRESSED") {
		t.Errorf("the message must name the detected shape, got:\n%s", msg)
	}
	if !strings.Contains(msg, "6 DID carry an offbox target") {
		t.Errorf("the message must state the measured offbox-bearing count (6), got:\n%s", msg)
	}
	// It must not prescribe the action for a failure mode that did not occur.
	if !strings.Contains(msg, "Re-issue is NOT the indicated action") {
		t.Errorf("the message must withdraw the Re-issue recommendation for this shape, got:\n%s", msg)
	}
	// The counting window is named, because CountReportsOffsiteSince reads the OLDEST 500 — an
	// instrument that can silently mis-scope must say so where it reports (R-192's other half, open).
	if !strings.Contains(msg, "500") {
		t.Errorf("the message must name its counting window, got:\n%s", msg)
	}

	var details map[string]any
	if err := json.Unmarshal([]byte(saved[0].DetailsJSON), &details); err != nil {
		t.Fatal(err)
	}
	if details["shape"] != "regressed" {
		t.Errorf("details.shape = %v, want regressed", details["shape"])
	}
	// The field whose absence was defect (a) is now carried.
	if got, ok := details["offsite_reports_since_consume"].(float64); !ok || int(got) != 6 {
		t.Errorf("details must carry offsite_reports_since_consume=6, got %v", details["offsite_reports_since_consume"])
	}
}

// The burned shape keeps its own (correct) text and its recommendation — the fix must not blunt the
// case that was always right.
func TestDeliveryChecker_BurnedShape_KeepsItsRecommendation(t *testing.T) {
	h := newDTHarness(t, false)
	h.burnedFixture(t, 5)

	h.checker.Check()

	saved := h.savedEvents(t, "offsite_delivery_stuck")
	if len(saved) != 1 {
		t.Fatalf("stuck events = %d, want 1", len(saved))
	}
	msg := saved[0].Message
	if !strings.Contains(msg, "BURNED") || !strings.Contains(msg, "NONE carried an offbox target") {
		t.Errorf("the burned shape must state its own measurement, got:\n%s", msg)
	}
	if !strings.Contains(msg, "Re-issue delivers a fresh one") {
		t.Errorf("the burned shape must keep its recommendation, got:\n%s", msg)
	}
	var details map[string]any
	_ = json.Unmarshal([]byte(saved[0].DetailsJSON), &details)
	if details["shape"] != "burned" {
		t.Errorf("details.shape = %v, want burned", details["shape"])
	}
}

// Scenario G — a refusal to self-heal leaves a durable record naming its reason.
// RED-PROOF: replace the recordHealRefusal call in the regressed branch with a bare `return`
// (production behaviour up to v0.92.0) → no row → this FAILS.
func TestDeliveryChecker_HealRefusal_LeavesARecord(t *testing.T) {
	h := newDTHarness(t, true)
	h.regressedFixture(t, 6, 2)

	h.checker.Check()

	if h.reissuer.count() != 0 {
		t.Fatal("the heal must NOT fire on a regressed shape — the guard is unchanged by this fix")
	}
	rows := h.refusalRows(t)
	if len(rows) != 1 {
		t.Fatalf("R-192(b): the heal declined and left %d record(s), want 1 — 'we chose not to act' and "+
			"'the heal never ran' must not look identical", len(rows))
	}
	r := rows[0]
	if r.Channel != "operator" {
		t.Errorf("refusal channel = %q, want operator (the channel the heal's own event would use)", r.Channel)
	}
	if !strings.Contains(r.ErrorMessage, "regressed-apply shape") {
		t.Errorf("the record must name WHY, got %q", r.ErrorMessage)
	}
	if !strings.Contains(r.ErrorMessage, "6 of the first 8") {
		t.Errorf("the record must carry the measurement it refused on, got %q", r.ErrorMessage)
	}
}

// The R-39(a) guard's refusal is recorded too — it was the one refusal that already logged, and it
// still must not be the only one that does.
//
// Reaching it needs the real TOCTOU, not a pre-staged row: staging BEFORE Check() makes the derive
// itself return staged_awaiting_consume and the pass never reaches the heal at all. Same technique as
// TestDeliveryChecker_R39aGuard_NeverRestagesOverUnconsumed — the operator stages mid-tick, on the
// stuck event's dispatch, which runs after the derive and before maybeHeal.
func TestDeliveryChecker_R39aRefusal_LeavesARecord(t *testing.T) {
	h := newDTHarness(t, true)
	h.burnedFixture(t, 5)

	base := h.checker.onEvent
	h.checker.onEvent = func(cid, et, sev, msg, det, src string) {
		if et == "offsite_delivery_stuck" {
			if err := h.st.SaveOneTimeSecret("c1", "operator-fresh"); err != nil {
				t.Errorf("mid-tick stage: %v", err)
			}
			if err := h.st.SetOneTimeSecretTimesForTest("c1", "2026-07-23 12:00:00", ""); err != nil {
				t.Errorf("mid-tick stamp: %v", err)
			}
		}
		base(cid, et, sev, msg, det, src)
	}

	h.checker.Check()

	if h.reissuer.count() != 0 {
		t.Fatal("R-39(a): the heal must never restage over an unconsumed secret")
	}
	rows := h.refusalRows(t)
	if len(rows) != 1 {
		t.Fatalf("the R-39(a) refusal left %d record(s), want 1", len(rows))
	}
	if !strings.Contains(rows[0].ErrorMessage, "R-39(a)") {
		t.Errorf("the record must name the guard, got %q", rows[0].ErrorMessage)
	}
}

// The refusal record rides the stuck event's 24h cadence: it explains the e-mail the operator just
// received, and must not accumulate one row per monitor tick.
func TestDeliveryChecker_HealRefusal_RidesTheStuckCadence(t *testing.T) {
	h := newDTHarness(t, true)
	h.regressedFixture(t, 6, 2)

	h.checker.Check()
	h.checker.Check() // the stuck event is swallowed by its cooldown; so is the refusal record
	h.checker.Check()

	if n := len(h.refusalRows(t)); n != 1 {
		t.Fatalf("refusal records = %d after three passes, want 1 (one per stuck e-mail, not per tick)", n)
	}
}
