package pipeline

import (
	"strings"
	"testing"
)

// regressionguard_test.go — the post-reflow regression guard (D38). Precision over recall: each
// flagger asserts BOTH firing on a real regression AND non-firing on a legitimate reflow.

func TestRegressionGuardLengthCollapse(t *testing.T) {
	// A draft well over the min-chars floor, halved by the "reflow" → collapse fires.
	draft := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 12) // ~560 non-space chars
	shortFinal := "Судзуки шёл."
	if r := runRegressionGuard(draft, shortFinal); r.LengthCollapse == 0 {
		t.Errorf("expected a length-collapse flag (draft≫final)")
	}
	// A legitimate reflow that merges lines keeps ~all the content → must NOT fire.
	sameFinal := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 11)
	if r := runRegressionGuard(draft, sameFinal); r.LengthCollapse != 0 {
		t.Errorf("length-collapse FALSE POSITIVE on a near-equal reflow: %v", r.Detail)
	}
	// A short draft below the floor must never fire (a % ratio is meaningless there).
	if r := runRegressionGuard("Он ушёл.", "Он."); r.LengthCollapse != 0 {
		t.Errorf("length-collapse fired below the min-chars floor: %v", r.Detail)
	}
}

func TestRegressionGuardNumberDrift(t *testing.T) {
	// 四成四=44% rendered as «44» in the draft, drifted to a word form in the final → 44 disappears.
	if r := runRegressionGuard("У него было 44 процента запаса.", "У него было четыре десятых запаса."); r.NumberDrift == 0 {
		t.Errorf("expected a number-drift flag (44 vanished)")
	}
	// A number APPEARING in the final that was not in the draft (hallucinated figure).
	if r := runRegressionGuard("Он собрал войско.", "Он собрал войско из 3000 солдат."); r.NumberDrift == 0 {
		t.Errorf("expected a number-drift flag (3000 appeared)")
	}
	clean := []struct{ draft, final string }{
		// The multi-digit number is preserved across the reflow (44 on both sides).
		{"У него было 44 процента.", "У него имелось 44 процента запаса."},
		// Grouped-triple vs joined form of the SAME number must not drift (10 000 ≡ 10000).
		{"Армия в 10 000 воинов.", "Армия насчитывала 10000 воинов."},
		// SINGLE digits are excluded (routinely spelled out), so «5»→«пять» is not a drift.
		{"У него было 5 мечей.", "У него было пять мечей."},
		// No numbers at all — never fires.
		{"Судзуки шёл по коридору академии магии.", "Судзуки медленно шёл по длинному коридору."},
	}
	for _, c := range clean {
		if r := runRegressionGuard(c.draft, c.final); r.NumberDrift != 0 {
			t.Errorf("number-drift FALSE POSITIVE on %q→%q: %v", c.draft, c.final, r.Detail)
		}
	}
}

// TestRunCheapGatesRegressionOptIn pins that the guard is OFF by default and folds into the
// observability total only when enabled (a book that does not opt in serialises identically).
func TestRunCheapGatesRegressionOptIn(t *testing.T) {
	draft := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 12)
	final := "Судзуки шёл."
	off := runCheapGates("", draft, final, cheapGateConfig{yoPolicy: "auto", allowlist: map[string]bool{}})
	if off.LengthCollapse != 0 || off.NumberDrift != 0 {
		t.Errorf("regression guard fired while disabled: %+v", off)
	}
	on := runCheapGates("", draft, final, cheapGateConfig{yoPolicy: "auto", allowlist: map[string]bool{}, regressionEnabled: true})
	if on.LengthCollapse == 0 {
		t.Errorf("regression guard did not fire while enabled: %+v", on)
	}
	if on.total() <= off.total() {
		t.Errorf("enabled guard must raise the observability total: off=%d on=%d", off.total(), on.total())
	}
}

// TestRegressionGuardDeterministic — a pure function of (draft, final), resume-safe.
func TestRegressionGuardDeterministic(t *testing.T) {
	d := strings.Repeat("текст с числом 44 и 128. ", 20)
	f := "короткий финал без чисел"
	a, b := runRegressionGuard(d, f), runRegressionGuard(d, f)
	if a.LengthCollapse != b.LengthCollapse || a.NumberDrift != b.NumberDrift ||
		strings.Join(a.Detail, "|") != strings.Join(b.Detail, "|") {
		t.Fatalf("runRegressionGuard not deterministic: %+v vs %+v", a, b)
	}
}
