package observer

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

func mkOp(success bool, source Source) ObservedOp {
	return ObservedOp{
		Timestamp:     time.Now().UTC().Format(time.RFC3339),
		Endpoint:      "/v1/test",
		InputSummary:  "test op",
		Success:       success,
		DurationMs:    42,
		OutputSummary: "ok",
		Source:        source,
	}
}

func TestRecord_RequiresEndpointAndTimestamp(t *testing.T) {
	s := NewStore(nil)
	bad := ObservedOp{Endpoint: ""} // EnsureTimestamp will fill, but Endpoint empty stays
	if err := s.Record(bad); err == nil {
		t.Error("expected error on empty endpoint")
	}

	good := mkOp(true, SourceMCP)
	if err := s.Record(good); err != nil {
		t.Errorf("good op: %v", err)
	}
}

func TestRecord_DefaultsTimestampAndSource(t *testing.T) {
	s := NewStore(nil)
	op := ObservedOp{
		Endpoint:     "/x",
		InputSummary: "no ts no source",
		Success:      true,
	}
	if err := s.Record(op); err != nil {
		t.Fatal(err)
	}
	stored := s.Recent()[0]
	if stored.Timestamp == "" {
		t.Error("Timestamp should be defaulted")
	}
	if stored.Source != SourceMCP {
		t.Errorf("Source: want %q, got %q", SourceMCP, stored.Source)
	}
}

func TestStats_Aggregates(t *testing.T) {
	s := NewStore(nil)
	for i := 0; i < 5; i++ {
		_ = s.Record(mkOp(true, SourceMCP))
	}
	for i := 0; i < 3; i++ {
		_ = s.Record(mkOp(false, SourceScenario))
	}
	for i := 0; i < 2; i++ {
		_ = s.Record(mkOp(true, SourceLangfuse))
	}

	st := s.Stats()
	if st.Total != 10 {
		t.Errorf("total: want 10, got %d", st.Total)
	}
	if st.Successes != 7 {
		t.Errorf("successes: want 7, got %d", st.Successes)
	}
	if st.Failures != 3 {
		t.Errorf("failures: want 3, got %d", st.Failures)
	}
	if st.BySource["mcp"] != 5 || st.BySource["scenario"] != 3 || st.BySource["langfuse"] != 2 {
		t.Errorf("by_source mismatch: %+v", st.BySource)
	}
	if len(st.RecentScenarios) != 3 {
		t.Errorf("recent scenarios: want 3, got %d", len(st.RecentScenarios))
	}
}

func TestStats_RecentScenariosCappedAndOrdered(t *testing.T) {
	s := NewStore(nil)
	// Record 15 scenario ops; only the last 10 should appear.
	for i := 0; i < 15; i++ {
		op := mkOp(true, SourceScenario)
		op.StafferID = "staffer-" + string(rune('a'+i))
		_ = s.Record(op)
		time.Sleep(time.Millisecond) // ensure timestamps order-distinguishable
	}
	st := s.Stats()
	if len(st.RecentScenarios) != DefaultRecentScenariosCap {
		t.Errorf("cap: want %d, got %d", DefaultRecentScenariosCap, len(st.RecentScenarios))
	}
	// Last entry should be the most recently added (staffer-o, the 15th).
	last := st.RecentScenarios[len(st.RecentScenarios)-1]
	if last.Staffer != "staffer-o" {
		t.Errorf("most recent: want staffer-o, got %q", last.Staffer)
	}
}

func TestRingBuffer_BoundedByDefaultCap(t *testing.T) {
	s := NewStore(nil)
	s.cap = 5 // shrink for testability
	for i := 0; i < 12; i++ {
		op := mkOp(true, SourceMCP)
		op.InputSummary = string(rune('a' + i))
		_ = s.Record(op)
	}
	r := s.Recent()
	if len(r) != 5 {
		t.Errorf("ring size: want 5, got %d", len(r))
	}
	// Oldest 7 dropped; first remaining should have InputSummary "h" (8th).
	if r[0].InputSummary != "h" {
		t.Errorf("oldest after rollover: want 'h', got %q", r[0].InputSummary)
	}
}

func TestPersistor_RoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "ops.jsonl")
	p, err := NewPersistor(path)
	if err != nil {
		t.Fatal(err)
	}
	s := NewStore(p)

	for i := 0; i < 4; i++ {
		op := mkOp(i%2 == 0, SourceMCP)
		op.InputSummary = string(rune('a' + i))
		if err := s.Record(op); err != nil {
			t.Fatal(err)
		}
	}

	// Sanity: file has 4 lines.
	bs, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSuffix(string(bs), "\n"), "\n")
	if len(lines) != 4 {
		t.Errorf("file lines: want 4, got %d", len(lines))
	}

	// Rehydrate into a fresh Store.
	s2 := NewStore(p)
	n, err := s2.Load()
	if err != nil {
		t.Fatal(err)
	}
	if n != 4 {
		t.Errorf("loaded: want 4, got %d", n)
	}
	r := s2.Recent()
	if len(r) != 4 {
		t.Errorf("rehydrated ring: want 4, got %d", len(r))
	}
	// Order preserved.
	for i, want := range []string{"a", "b", "c", "d"} {
		if r[i].InputSummary != want {
			t.Errorf("op %d: want %q, got %q", i, want, r[i].InputSummary)
		}
	}
}

func TestPersistor_CorruptionTolerant(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "ops.jsonl")
	// Pre-seed with one valid + one corrupt + one valid line.
	valid1 := `{"timestamp":"2026-04-29T12:00:00Z","endpoint":"/x","input_summary":"a","success":true,"duration_ms":1,"output_summary":"ok","source":"mcp"}`
	corrupt := `{this is not json`
	valid2 := `{"timestamp":"2026-04-29T12:00:01Z","endpoint":"/y","input_summary":"b","success":false,"duration_ms":2,"output_summary":"err","source":"scenario"}`
	if err := os.WriteFile(path, []byte(valid1+"\n"+corrupt+"\n"+valid2+"\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	p, err := NewPersistor(path)
	if err != nil {
		t.Fatal(err)
	}
	s := NewStore(p)
	n, err := s.Load()
	if err != nil {
		t.Fatal(err)
	}
	if n != 2 {
		t.Errorf("applied: want 2 (valid pair), got %d (corrupt should skip)", n)
	}
}
