package event

import (
	"sync"
	"testing"

	"reasonix/internal/evidence"
	"reasonix/internal/provider"
)

// Kind constants

func TestKindConstants(t *testing.T) {
	// Verify the iota sequence is stable and sequential for the original
	// core kinds. New kinds are appended before KindCount.
	kinds := []Kind{
		TurnStarted, Reasoning, Text, Message, ToolDispatch, ToolResult,
		Usage, Notice, Phase, ApprovalRequest, AskRequest, TurnDone,
	}
	for i, k := range kinds {
		if int(k) != i {
			t.Errorf("Kind %d: got %d", i, int(k))
		}
	}
	if TurnPhase >= KindCount || CompletionSummary >= KindCount {
		t.Fatal("new kinds must sit before KindCount")
	}
	if TurnPhaseName(TurnPhaseWorking) != "working" || TurnPhaseName(TurnPhaseReviewing) != "reviewing" {
		t.Fatal("turn phase names drifted")
	}
}

// Level constants

func TestLevelConstants(t *testing.T) {
	if LevelInfo != 0 {
		t.Errorf("LevelInfo = %d, want 0", LevelInfo)
	}
	if LevelWarn != 1 {
		t.Errorf("LevelWarn = %d, want 1", LevelWarn)
	}
}

func TestNoticeAudienceConstants(t *testing.T) {
	if NoticeAudienceDefault != "" {
		t.Errorf("NoticeAudienceDefault = %q, want empty for backward-compatible delivery", NoticeAudienceDefault)
	}
	if NoticeAudienceOperator != "operator" {
		t.Errorf("NoticeAudienceOperator = %q, want operator", NoticeAudienceOperator)
	}
}

// FuncSink

func TestFuncSinkEmit(t *testing.T) {
	var received Event
	fs := FuncSink(func(e Event) { received = e })
	e := Event{Kind: Text, Text: "hello"}
	fs.Emit(e)
	if received.Kind != Text || received.Text != "hello" {
		t.Errorf("FuncSink did not forward event: got %+v", received)
	}
}

func TestFuncSinkNilEmitIsNoop(t *testing.T) {
	var fs FuncSink

	fs.Emit(Event{Kind: Text, Text: "hello"})
}

type typedNilSink struct{}

func (*typedNilSink) Emit(Event) {}

func TestSyncTreatsTypedNilSinkAsDiscard(t *testing.T) {
	var base *typedNilSink

	Sync(base).Emit(Event{Kind: Text, Text: "hello"})
}

type readinessAuditRecorder struct {
	events     []evidence.ReadinessAudit
	recovery   []ProtocolRecoveryAudit
	workspace  []WorkspaceMutation
	completion []CompletionValidationInfo
	turns      int
}

func (r *readinessAuditRecorder) Emit(Event) {}

func (r *readinessAuditRecorder) RecordReadinessAudit(a evidence.ReadinessAudit) {
	r.events = append(r.events, a)
}

func (r *readinessAuditRecorder) RecordProtocolRecovery(a ProtocolRecoveryAudit) {
	r.recovery = append(r.recovery, a)
}

func (r *readinessAuditRecorder) RecordTurnCompletion() { r.turns++ }

func (r *readinessAuditRecorder) RecordWorkspaceMutation(m WorkspaceMutation) {
	r.workspace = append(r.workspace, m)
}

func (r *readinessAuditRecorder) RecordCompletionValidation(info CompletionValidationInfo) {
	r.completion = append(r.completion, info)
}

func TestSyncForwardsTurnCompletion(t *testing.T) {
	rec := &readinessAuditRecorder{}
	RecordTurnCompletion(Sync(rec))
	if rec.turns != 1 {
		t.Fatalf("turn completions = %d, want 1", rec.turns)
	}
}

func TestSyncForwardsWorkspaceMutationWithoutUIEvent(t *testing.T) {
	rec := &readinessAuditRecorder{}
	sink := Sync(rec)
	RecordWorkspaceMutation(sink, WorkspaceMutation{ToolName: "write_file", Paths: []string{"a.go"}, Content: true})
	if len(rec.workspace) != 1 || rec.workspace[0].ToolName != "write_file" || len(rec.workspace[0].Paths) != 1 {
		t.Fatalf("workspace mutation not forwarded through Sync: %+v", rec.workspace)
	}
}

func TestSyncForwardsCompletionValidationWithoutUIEvent(t *testing.T) {
	rec := &readinessAuditRecorder{}
	sink := Sync(rec)
	RecordCompletionValidation(sink, CompletionValidationInfo{Mode: "enforce", Outcome: "continue"})
	if len(rec.completion) != 1 || rec.completion[0].Outcome != "continue" {
		t.Fatalf("completion validation not forwarded through Sync: %+v", rec.completion)
	}
}

func TestSyncForwardsReadinessAuditReceipts(t *testing.T) {
	rec := &readinessAuditRecorder{}
	sink := Sync(rec)

	RecordReadinessAudit(sink, evidence.ReadinessAudit{
		Result:                 evidence.ReadinessBlocked,
		MissingProjectChecks:   1,
		CommandMismatchMissing: 1,
	})

	if len(rec.events) != 1 {
		t.Fatalf("readiness audit events = %d, want 1", len(rec.events))
	}
	if rec.events[0].Result != evidence.ReadinessBlocked || rec.events[0].MissingProjectChecks != 1 {
		t.Fatalf("readiness audit not forwarded through Sync: %+v", rec.events[0])
	}
}

func TestSyncForwardsProtocolRecoveryWithoutEmittingUIEvent(t *testing.T) {
	rec := &readinessAuditRecorder{}
	sink := Sync(rec)

	RecordProtocolRecovery(sink, ProtocolRecoveryAudit{Kind: ProtocolRecoveryMissingReasoningRetryReplaced})

	if len(rec.recovery) != 1 || rec.recovery[0].Kind != ProtocolRecoveryMissingReasoningRetryReplaced {
		t.Fatalf("protocol recovery not forwarded through Sync: %+v", rec.recovery)
	}
}

// Discard

func TestDiscardSink(t *testing.T) {
	// Discard should accept any event without panic.
	Discard.Emit(Event{Kind: TurnStarted})
	Discard.Emit(Event{Kind: Text, Text: "discarded"})
	Discard.Emit(Event{Kind: TurnDone})
}

// Event struct field access

func TestEventFields(t *testing.T) {
	usage := &provider.Usage{PromptTokens: 100, CompletionTokens: 50}
	pricing := &provider.Pricing{Input: 2.0, Output: 10.0, Currency: "$"}

	e := Event{
		Kind:        Usage,
		Usage:       usage,
		Pricing:     pricing,
		SessionHit:  80,
		SessionMiss: 20,
	}
	if e.Kind != Usage {
		t.Errorf("Kind = %d, want %d", e.Kind, Usage)
	}
	if e.Usage.PromptTokens != 100 {
		t.Errorf("PromptTokens = %d, want 100", e.Usage.PromptTokens)
	}
	if e.Pricing.Currency != "$" {
		t.Errorf("Currency = %q, want $", e.Pricing.Currency)
	}
	if e.SessionHit != 80 || e.SessionMiss != 20 {
		t.Errorf("SessionHit=%d, SessionMiss=%d", e.SessionHit, e.SessionMiss)
	}
}

// Tool struct

func TestToolStruct(t *testing.T) {
	tool := Tool{
		ID:       "call-1",
		Name:     "bash",
		Args:     `{"command":"echo hi"}`,
		ReadOnly: false,
		Partial:  true,
		ParentID: "parent-1",
	}
	if tool.ID != "call-1" || tool.Name != "bash" {
		t.Errorf("unexpected tool: %+v", tool)
	}
	if !tool.Partial {
		t.Error("Partial should be true")
	}
	if tool.ParentID != "parent-1" {
		t.Errorf("ParentID = %q", tool.ParentID)
	}

	result := Tool{
		ID:        "call-1",
		Name:      "bash",
		Output:    "hi\n",
		Err:       "",
		Truncated: false,
	}
	if result.Output != "hi\n" {
		t.Errorf("Output = %q", result.Output)
	}
}

// Approval struct

func TestApprovalStruct(t *testing.T) {
	a := Approval{ID: "42", Tool: "bash", Subject: "rm -rf /"}
	if a.ID != "42" || a.Tool != "bash" || a.Subject != "rm -rf /" {
		t.Errorf("unexpected approval: %+v", a)
	}
}

// Ask / AskQuestion / AskOption / AskAnswer

func TestAskStructs(t *testing.T) {
	q := AskQuestion{
		ID:     "q1",
		Header: "Confirm",
		Prompt: "Are you sure?",
		Options: []AskOption{
			{Label: "Yes", Description: "Proceed"},
			{Label: "No", Description: "Cancel"},
		},
		Multi: false,
	}
	ask := Ask{
		ID:        "ask-1",
		Questions: []AskQuestion{q},
	}
	if len(ask.Questions) != 1 {
		t.Fatalf("questions count = %d", len(ask.Questions))
	}
	if ask.Questions[0].Options[0].Label != "Yes" {
		t.Errorf("first option = %q", ask.Questions[0].Options[0].Label)
	}

	ans := AskAnswer{QuestionID: "q1", Selected: []string{"Yes"}}
	if len(ans.Selected) != 1 || ans.Selected[0] != "Yes" {
		t.Errorf("answer = %+v", ans)
	}
}

// Multiple Emit via channel-backed sink

func TestChannelBackedSink(t *testing.T) {
	ch := make(chan Event, 8)
	sink := FuncSink(func(e Event) { ch <- e })

	events := []Event{
		{Kind: TurnStarted},
		{Kind: Text, Text: "hello"},
		{Kind: ToolDispatch, Tool: Tool{Name: "bash"}},
		{Kind: ToolResult, Tool: Tool{Output: "ok"}},
		{Kind: Usage, Usage: &provider.Usage{TotalTokens: 42}},
		{Kind: Notice, Level: LevelWarn, Text: "heads up", Detail: "diagnostics"},
		{Kind: TurnDone},
	}
	for _, e := range events {
		sink.Emit(e)
	}

	for i, want := range events {
		got := <-ch
		if got.Kind != want.Kind {
			t.Errorf("event %d: Kind = %d, want %d", i, got.Kind, want.Kind)
		}
		if got.Detail != want.Detail {
			t.Errorf("event %d: Detail = %q, want %q", i, got.Detail, want.Detail)
		}
	}
}

// FuncSink forwards every concurrent Emit exactly once

// FuncSink.Emit forwards to the wrapped func with no synchronization of its own,
// so a concurrency-safe callback is the caller's responsibility (here a
// mutex-guarded counter). This verifies that N concurrent Emits produce exactly
// N forwarded calls, and under `go test -race` that the forwarding itself is
// race-free.
func TestFuncSinkForwardsEachConcurrentEmit(t *testing.T) {
	var mu sync.Mutex
	var count int
	sink := FuncSink(func(e Event) {
		mu.Lock()
		count++
		mu.Unlock()
	})
	var wg sync.WaitGroup
	for range 100 {
		wg.Go(func() {
			sink.Emit(Event{Kind: Text})
		})
	}
	wg.Wait()
	mu.Lock()
	defer mu.Unlock()
	if count != 100 {
		t.Errorf("count = %d, want 100", count)
	}
}
