package metrics

import (
	"fmt"
	"strings"
	"testing"
	"unicode/utf8"
)

func findIssue(t *testing.T, issues []LogIssue, substr string) LogIssue {
	t.Helper()
	for _, is := range issues {
		if strings.Contains(is.Message, substr) {
			return is
		}
	}
	t.Fatalf("no issue containing %q in %+v", substr, issues)
	return LogIssue{}
}

// Part B — a synthetic scrape with an error mid-stream: Context must be EXACTLY the
// ±5 window, ordered as emitted. Red-proof: dropping the captureContext call leaves
// Context empty → this fails.
func TestAnalyzeLogLines_ErrorContextWindow(t *testing.T) {
	lines := []string{
		"boot line zero",
		"ctx minus five",
		"ctx minus four",
		"ctx minus three",
		"ctx minus two",
		"ctx minus one",
		"ERROR: nfs mount lost", // index 6
		"ctx plus one",
		"ctx plus two",
		"ctx plus three",
		"ctx plus four",
		"ctx plus five",
		"outside the window",
	}
	errC, warnC, issues := analyzeLogLines(lines)
	if errC != 1 || warnC != 0 {
		t.Fatalf("counts: errors=%d warns=%d, want 1/0", errC, warnC)
	}
	is := findIssue(t, issues, "nfs mount lost")
	want := []string{
		"ctx minus five", "ctx minus four", "ctx minus three", "ctx minus two", "ctx minus one",
		"ERROR: nfs mount lost",
		"ctx plus one", "ctx plus two", "ctx plus three", "ctx plus four", "ctx plus five",
	}
	if len(is.Context) != len(want) {
		t.Fatalf("context has %d lines, want %d: %#v", len(is.Context), len(want), is.Context)
	}
	for i := range want {
		if is.Context[i] != want[i] {
			t.Fatalf("context[%d] = %q, want %q (order must match emission)", i, is.Context[i], want[i])
		}
	}
}

// Repeat occurrence of the same fingerprint in one window: count aggregates but the
// context stays the FIRST occurrence's window — never re-captured on repeats.
func TestAnalyzeLogLines_RepeatKeepsFirstContext(t *testing.T) {
	lines := []string{
		"first neighborhood",
		"ERROR: db connection refused",
		"after the first",
		"filler a", "filler b", "filler c", "filler d", "filler e", "filler f",
		"second neighborhood",
		"ERROR: db connection refused",
		"after the second",
	}
	_, _, issues := analyzeLogLines(lines)
	is := findIssue(t, issues, "db connection refused")
	if is.Count != 2 {
		t.Fatalf("count = %d, want 2 (same fingerprint must dedupe)", is.Count)
	}
	joined := strings.Join(is.Context, "\n")
	if !strings.Contains(joined, "first neighborhood") {
		t.Fatalf("context lost the FIRST occurrence window: %#v", is.Context)
	}
	if strings.Contains(joined, "second neighborhood") {
		t.Fatalf("context re-captured on a repeat occurrence: %#v", is.Context)
	}
}

// Warn-severity issues carry NO context (message-only) — bounds the payload.
func TestAnalyzeLogLines_WarnHasNoContext(t *testing.T) {
	lines := []string{"before", "WARN: disk latency high", "after"}
	_, warnC, issues := analyzeLogLines(lines)
	if warnC != 1 {
		t.Fatalf("warns = %d, want 1", warnC)
	}
	is := findIssue(t, issues, "disk latency high")
	if len(is.Context) != 0 {
		t.Fatalf("warn issue must not carry context, got %#v", is.Context)
	}
}

// Caps: oversized context lines are truncated to 400 runes + "…" (rune-safe).
func TestAnalyzeLogLines_ContextLineTruncated(t *testing.T) {
	long := strings.Repeat("x", 450)
	lines := []string{long, "ERROR: it broke", "after"}
	_, _, issues := analyzeLogLines(lines)
	is := findIssue(t, issues, "it broke")
	if len(is.Context) == 0 {
		t.Fatalf("expected context")
	}
	got := is.Context[0]
	if utf8.RuneCountInString(got) != 401 || !strings.HasSuffix(got, "…") {
		t.Fatalf("oversized line not truncated to 400+ellipsis: len=%d suffix=%q",
			utf8.RuneCountInString(got), got[len(got)-3:])
	}
}

// Part E on the capture path: a secret in a NEIGHBOR line must ship redacted.
func TestAnalyzeLogLines_ContextRedacted(t *testing.T) {
	lines := []string{
		"connecting with password=hunter2 now",
		"ERROR: auth failed",
		"retrying",
	}
	_, _, issues := analyzeLogLines(lines)
	is := findIssue(t, issues, "auth failed")
	joined := strings.Join(is.Context, "\n")
	if strings.Contains(joined, "hunter2") {
		t.Fatalf("secret shipped in context: %#v", is.Context)
	}
	if !strings.Contains(joined, "password=[REDACTED]") {
		t.Fatalf("expected redaction marker in context: %#v", is.Context)
	}
}

// Error at the very start/end of the window: the ±5 clamps without panicking
// and still contains the error line itself.
func TestAnalyzeLogLines_WindowClamped(t *testing.T) {
	lines := []string{"ERROR: first line broke", "after one"}
	_, _, issues := analyzeLogLines(lines)
	is := findIssue(t, issues, "first line broke")
	if len(is.Context) != 2 {
		t.Fatalf("clamped context = %#v, want the 2 available lines", is.Context)
	}
}

// The 10-issue cap and count-DESC ordering still hold with contexts attached.
func TestAnalyzeLogLines_CapAndOrder(t *testing.T) {
	var lines []string
	for i := 0; i < 12; i++ {
		lines = append(lines, fmt.Sprintf("ERROR: distinct failure mode %s", strings.Repeat("z", i+1)))
	}
	// make one of them dominant
	lines = append(lines, "ERROR: distinct failure mode z", "ERROR: distinct failure mode z")
	_, _, issues := analyzeLogLines(lines)
	if len(issues) != 10 {
		t.Fatalf("issues = %d, want capped at 10", len(issues))
	}
	if issues[0].Count != 3 {
		t.Fatalf("top issue count = %d, want the dominant one (3) first", issues[0].Count)
	}
}
