package distillation

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// TestLoadLastBaseline_MissingFile: no baselines file yet → (nil, nil),
// not an error. First-run pipelines have no baseline.
func TestLoadLastBaseline_MissingFile(t *testing.T) {
	tmp := t.TempDir()
	got, err := LoadLastBaseline(filepath.Join(tmp, "nonexistent.jsonl"))
	if err != nil {
		t.Fatalf("expected nil error on missing file, got %v", err)
	}
	if got != nil {
		t.Errorf("expected nil baseline on missing file, got %+v", got)
	}
}

// TestLoadLastBaseline_LastLineWins locks the "last line is most
// recent" semantic — append-only log read backward.
func TestLoadLastBaseline_LastLineWins(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "baselines.jsonl")
	jsonl := `{"recorded_at":"2026-04-01T00:00:00Z","metrics":{"a":1}}
{"recorded_at":"2026-04-15T00:00:00Z","metrics":{"a":2}}
{"recorded_at":"2026-04-30T00:00:00Z","metrics":{"a":3}}
`
	if err := os.WriteFile(path, []byte(jsonl), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}
	got, err := LoadLastBaseline(path)
	if err != nil {
		t.Fatalf("LoadLastBaseline: %v", err)
	}
	if got == nil || got.RecordedAt != "2026-04-30T00:00:00Z" {
		t.Errorf("expected last-line baseline (2026-04-30), got %+v", got)
	}
	if got.Metrics["a"] != 3 {
		t.Errorf("expected metrics.a=3, got %d", got.Metrics["a"])
	}
}

// TestLoadLastBaseline_TolerateTrailingBlankLines: writers append
// "\n" — a stray blank line at the end mustn't trigger malformed-
// JSON errors.
func TestLoadLastBaseline_TolerateTrailingBlankLines(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "baselines.jsonl")
	jsonl := `{"recorded_at":"2026-04-30T00:00:00Z","metrics":{"a":3}}

`
	if err := os.WriteFile(path, []byte(jsonl), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}
	got, err := LoadLastBaseline(path)
	if err != nil {
		t.Fatalf("LoadLastBaseline: %v", err)
	}
	if got == nil || got.Metrics["a"] != 3 {
		t.Errorf("expected baseline with metrics.a=3, got %+v", got)
	}
}

// TestLoadLastBaseline_MalformedLastLineErrors: partial-write on
// disk-full would leave a truncated last line. Rather than
// silently skip back to a stale baseline, surface the error so
// operators don't lose drift signal.
func TestLoadLastBaseline_MalformedLastLineErrors(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "baselines.jsonl")
	jsonl := `{"recorded_at":"2026-04-15T00:00:00Z","metrics":{"a":2}}
{"recorded_at":"2026-04-30T0` // truncated
	if err := os.WriteFile(path, []byte(jsonl), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}
	_, err := LoadLastBaseline(path)
	if err == nil {
		t.Errorf("expected decode error on truncated last line, got nil")
	}
}

// TestAppendBaseline_RoundTrip: append one + read back via
// LoadLastBaseline. Critical for the "first audit-full run on a
// fresh box" path.
func TestAppendBaseline_RoundTrip(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "data", "_kb", "audit_baselines.jsonl")
	b := AuditBaseline{
		RecordedAt: "2026-05-01T12:00:00Z",
		GitCommit:  "deadbeef",
		Metrics: map[string]int64{
			"p3_accepted":   42,
			"p4_sft_rows":   17,
		},
	}
	if err := AppendBaseline(path, b); err != nil {
		t.Fatalf("AppendBaseline: %v", err)
	}
	got, err := LoadLastBaseline(path)
	if err != nil {
		t.Fatalf("LoadLastBaseline: %v", err)
	}
	if got == nil {
		t.Fatalf("expected non-nil baseline after append")
	}
	if got.RecordedAt != b.RecordedAt || got.GitCommit != b.GitCommit {
		t.Errorf("baseline header mismatch:\n  got:  %+v\n  want: %+v", got, b)
	}
	if got.Metrics["p3_accepted"] != 42 || got.Metrics["p4_sft_rows"] != 17 {
		t.Errorf("metrics roundtrip: got %+v", got.Metrics)
	}
}

// TestAppendBaseline_AppendsToExisting: existing file gets an
// extra line, prior contents preserved.
func TestAppendBaseline_AppendsToExisting(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "baselines.jsonl")
	if err := os.WriteFile(path,
		[]byte(`{"recorded_at":"2026-04-30T00:00:00Z","metrics":{"a":1}}`+"\n"),
		0o644); err != nil {
		t.Fatalf("seed: %v", err)
	}
	if err := AppendBaseline(path, AuditBaseline{
		RecordedAt: "2026-05-01T00:00:00Z",
		Metrics:    map[string]int64{"a": 2},
	}); err != nil {
		t.Fatalf("AppendBaseline: %v", err)
	}
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
	if len(lines) != 2 {
		t.Errorf("expected 2 lines after append, got %d", len(lines))
	}
}

// TestAppendBaseline_RejectsEmptyRecordedAt: schema invariant —
// every entry must carry a timestamp for ordering.
func TestAppendBaseline_RejectsEmptyRecordedAt(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "baselines.jsonl")
	err := AppendBaseline(path, AuditBaseline{Metrics: map[string]int64{"a": 1}})
	if err == nil {
		t.Errorf("expected error on empty RecordedAt")
	}
}

// TestBuildAuditDriftTable_FirstRun: no prior → every metric flagged
// "first_run", no PctChange.
func TestBuildAuditDriftTable_FirstRun(t *testing.T) {
	rows := BuildAuditDriftTable(nil, map[string]int64{"a": 10, "b": 20}, 0)
	if len(rows) != 2 {
		t.Fatalf("expected 2 rows, got %d", len(rows))
	}
	for _, r := range rows {
		if r.Flag != AuditDriftFlagFirstRun {
			t.Errorf("metric %s: expected first_run flag, got %s", r.Metric, r.Flag)
		}
		if r.PctChange != nil {
			t.Errorf("metric %s: PctChange should be nil on first run", r.Metric)
		}
	}
}

// TestBuildAuditDriftTable_ThresholdBoundary: exactly threshold = OK,
// just over = WARN. Locks the >|threshold| (strict) semantic.
func TestBuildAuditDriftTable_ThresholdBoundary(t *testing.T) {
	prior := &AuditBaseline{Metrics: map[string]int64{"a": 100, "b": 100}}
	current := map[string]int64{
		"a": 120, // +20% — exactly at threshold → OK
		"b": 121, // +21% — over threshold → WARN
	}
	rows := BuildAuditDriftTable(prior, current, 0.20)
	byMetric := map[string]AuditDriftRow{}
	for _, r := range rows {
		byMetric[r.Metric] = r
	}
	if byMetric["a"].Flag != AuditDriftFlagOK {
		t.Errorf("metric a (+20%% exactly): expected OK, got %s", byMetric["a"].Flag)
	}
	if byMetric["b"].Flag != AuditDriftFlagWarn {
		t.Errorf("metric b (+21%%): expected warn, got %s", byMetric["b"].Flag)
	}
}

// TestBuildAuditDriftTable_ZeroBaseline: prior=0 means we can't
// compute pct (div-by-0). PctChange stays nil; current=0 stays
// OK; current>0 escalates to WARN (zero→nonzero is always
// notable).
func TestBuildAuditDriftTable_ZeroBaseline(t *testing.T) {
	prior := &AuditBaseline{Metrics: map[string]int64{"stayed_zero": 0, "went_nonzero": 0}}
	current := map[string]int64{
		"stayed_zero":   0,
		"went_nonzero":  5,
	}
	rows := BuildAuditDriftTable(prior, current, 0.20)
	byMetric := map[string]AuditDriftRow{}
	for _, r := range rows {
		byMetric[r.Metric] = r
	}
	if byMetric["stayed_zero"].Flag != AuditDriftFlagOK {
		t.Errorf("0→0 should be OK, got %s", byMetric["stayed_zero"].Flag)
	}
	if byMetric["went_nonzero"].Flag != AuditDriftFlagWarn {
		t.Errorf("0→5 should be warn, got %s", byMetric["went_nonzero"].Flag)
	}
	if byMetric["stayed_zero"].PctChange != nil || byMetric["went_nonzero"].PctChange != nil {
		t.Errorf("zero-baseline rows must have nil PctChange (no division by zero)")
	}
}

// TestBuildAuditDriftTable_NewMetricInCurrent: a metric present in
// current but not in prior is flagged first_run, not "0%-change".
func TestBuildAuditDriftTable_NewMetricInCurrent(t *testing.T) {
	prior := &AuditBaseline{Metrics: map[string]int64{"old_only": 5}}
	current := map[string]int64{"old_only": 5, "brand_new": 10}
	rows := BuildAuditDriftTable(prior, current, 0)
	byMetric := map[string]AuditDriftRow{}
	for _, r := range rows {
		byMetric[r.Metric] = r
	}
	if byMetric["brand_new"].Flag != AuditDriftFlagFirstRun {
		t.Errorf("new metric should be first_run, got %s", byMetric["brand_new"].Flag)
	}
	if byMetric["old_only"].Flag != AuditDriftFlagOK {
		t.Errorf("unchanged metric should be OK, got %s", byMetric["old_only"].Flag)
	}
}

// TestBuildAuditDriftTable_SortedByMetric: deterministic JSON
// output requires stable sort — drift tables in CI runs need to
// diff cleanly.
func TestBuildAuditDriftTable_SortedByMetric(t *testing.T) {
	prior := &AuditBaseline{Metrics: map[string]int64{"zoo": 1, "alpha": 1, "midway": 1}}
	current := map[string]int64{"zoo": 1, "alpha": 1, "midway": 1}
	rows := BuildAuditDriftTable(prior, current, 0)
	want := []string{"alpha", "midway", "zoo"}
	for i, r := range rows {
		if r.Metric != want[i] {
			t.Errorf("rows[%d]: got %q, want %q", i, r.Metric, want[i])
		}
	}
}

// TestFormatAuditDriftTable_RendersFlags: stdout dump shape — we
// don't pin every byte but verify the metric names + flags appear.
func TestFormatAuditDriftTable_RendersFlags(t *testing.T) {
	rows := []AuditDriftRow{
		{Metric: "p3_accepted", Current: 50, Flag: AuditDriftFlagFirstRun},
		{Metric: "p4_sft_rows", Baseline: int64Ptr(10), Current: 13, PctChange: float64Ptr(0.30), Flag: AuditDriftFlagWarn},
	}
	out := FormatAuditDriftTable(rows)
	for _, want := range []string{"p3_accepted", "p4_sft_rows", "first_run", "warn", "+30.0%"} {
		if !strings.Contains(out, want) {
			t.Errorf("expected %q in output:\n%s", want, out)
		}
	}
}

// TestFormatAuditDriftTable_EmptyHeader: empty rows yields a
// single-line "(no metrics)" — operators see something instead of
// blank output.
func TestFormatAuditDriftTable_EmptyHeader(t *testing.T) {
	out := FormatAuditDriftTable(nil)
	if !strings.Contains(out, "no metrics") {
		t.Errorf("expected 'no metrics' notice on empty input, got %q", out)
	}
}

func int64Ptr(v int64) *int64       { return &v }
func float64Ptr(v float64) *float64 { return &v }
