package dispatcher

import (
	"context"
	"go/ast"
	"go/parser"
	"go/token"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeevents"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/manager"
)

type parsedEventRuntimeMappingCase struct {
	name       string
	parsedKind string
	event      drivercontract.ParsedEvent
	wantKind   runtimeevents.Kind
	wantData   any
	wantError  bool
	check      func(t *testing.T, ev runtimeevents.Event)
}

type metricsRequestSnapshot struct {
	apiKey string
	model  string
	input  uint64
	cached uint64
	output uint64
	dur    time.Duration
}

type recordingMetricsRequest struct {
	mu sync.Mutex
	metricsRequestSnapshot
}

func (r *recordingMetricsRequest) record(apiKey, model string, inputTokens, cachedInputTokens, outputTokens uint64, duration time.Duration) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.apiKey = apiKey
	r.model = model
	r.input = inputTokens
	r.cached = cachedInputTokens
	r.output = outputTokens
	r.dur = duration
}

func (r *recordingMetricsRequest) snapshot() metricsRequestSnapshot {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.metricsRequestSnapshot
}

func parsedEventRuntimeMappingManifest() []parsedEventRuntimeMappingCase {
	return []parsedEventRuntimeMappingCase{
		{
			name:       "session init",
			parsedKind: consts.ParsedEventKindSessionInit,
			event: drivercontract.ParsedEvent{
				Kind:      consts.ParsedEventKindSessionInit,
				SessionID: "sess-1",
			},
			wantKind: runtimeevents.KindTurnSessionInit,
		},
		{
			name:       "thinking text",
			parsedKind: consts.ParsedEventKindThinking,
			event: drivercontract.ParsedEvent{
				Kind:      consts.ParsedEventKindThinking,
				Text:      "thinking",
				Title:     "Checking",
				SectionID: "reasoning-1:summary:0",
			},
			wantKind: runtimeevents.KindTurnThinking,
			wantData: &runtimeevents.TurnTextData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.TurnTextData](ev)
				require.True(t, ok)
				require.Equal(t, "thinking", data.Text)
				require.Equal(t, "Checking", data.Title)
				require.Equal(t, "reasoning-1:summary:0", data.SectionID)
			},
		},
		{
			name:       "assistant text",
			parsedKind: consts.ParsedEventKindText,
			event: drivercontract.ParsedEvent{
				Kind: consts.ParsedEventKindText,
				Text: "hello",
			},
			wantKind: runtimeevents.KindTurnText,
			wantData: &runtimeevents.TurnTextData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.TurnTextData](ev)
				require.True(t, ok)
				require.Equal(t, "hello", data.Text)
			},
		},
		{
			name:       "tool call",
			parsedKind: consts.ParsedEventKindToolCall,
			event: drivercontract.ParsedEvent{
				Kind:       consts.ParsedEventKindToolCall,
				ToolName:   "exec_command",
				ToolCallID: "call-1",
				ToolInput:  `{"cmd":"go test ./..."}`,
			},
			wantKind: runtimeevents.KindTurnToolCall,
			wantData: &runtimeevents.ToolCallData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.ToolCallData](ev)
				require.True(t, ok)
				require.Equal(t, "exec_command", data.ToolName)
				require.Equal(t, "call-1", data.ToolCallID)
				require.Equal(t, `{"cmd":"go test ./..."}`, data.ToolInput)
			},
		},
		{
			name:       "tool output",
			parsedKind: consts.ParsedEventKindToolOutput,
			event: drivercontract.ParsedEvent{
				Kind:       consts.ParsedEventKindToolOutput,
				ToolName:   "exec_command",
				ToolCallID: "call-1",
				Metadata: map[string]any{
					"customFlag":  true,
					"customCount": 1,
				},
				Text: "ok",
			},
			wantKind: runtimeevents.KindTurnToolOutput,
			wantData: &runtimeevents.ToolCallData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.ToolCallData](ev)
				require.True(t, ok)
				require.Equal(t, "exec_command", data.ToolName)
				require.Equal(t, "call-1", data.ToolCallID)
				require.Equal(t, "ok", data.ToolInput)
				require.Equal(t, map[string]any{
					"customFlag":  true,
					"customCount": 1,
				}, data.Metadata)
			},
		},
		{
			name:       "subagent activity",
			parsedKind: consts.ParsedEventKindSubagentActivity,
			event: drivercontract.ParsedEvent{
				Kind:      consts.ParsedEventKindSubagentActivity,
				Message:   "Started /root/reviewer",
				SessionID: "thread-primary",
				Runtime:   "opencode",
				EventType: "runtime_debug",
				Metadata: map[string]any{
					"activityKind":  "started",
					"agentThreadId": "thread-child",
					"agentPath":     "/root/reviewer",
				},
			},
			wantKind: runtimeevents.KindTurnSubagentActivity,
			wantData: &runtimeevents.LifecycleData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.LifecycleData](ev)
				require.True(t, ok)
				require.Equal(t, "working", data.Activity)
				require.Equal(t, "Started /root/reviewer", data.Detail)
				require.Equal(t, "runtime_debug", data.EventType)
				require.Equal(t, "thread-child", data.Metadata["agentThreadId"])
			},
		},
		{
			name:       "turn end",
			parsedKind: consts.ParsedEventKindTurnEnd,
			event: drivercontract.ParsedEvent{
				Kind: consts.ParsedEventKindTurnEnd,
			},
			wantKind: runtimeevents.KindTurnEnded,
		},
		{
			name:       "runtime idle",
			parsedKind: consts.ParsedEventKindRuntimeIdle,
			event: drivercontract.ParsedEvent{
				Kind: consts.ParsedEventKindRuntimeIdle,
			},
			wantKind: runtimeevents.KindTurnIdle,
		},
		{
			name:       "runtime compacting",
			parsedKind: consts.ParsedEventKindRuntimeCompacting,
			event: drivercontract.ParsedEvent{
				Kind: consts.ParsedEventKindRuntimeCompacting,
			},
			wantKind: runtimeevents.KindTurnCompacting,
		},
		{
			name:       "runtime compacted",
			parsedKind: consts.ParsedEventKindRuntimeCompacted,
			event: drivercontract.ParsedEvent{
				Kind:    consts.ParsedEventKindRuntimeCompacted,
				Message: "Context compacted (152355 → 4234 tokens).",
				Metadata: map[string]any{
					"context_compaction_source": "agentteam_proactive",
				},
			},
			wantKind: runtimeevents.KindTurnCompacted,
			wantData: &runtimeevents.LifecycleData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.LifecycleData](ev)
				require.True(t, ok)
				require.Equal(t, "working", data.Activity)
				require.Equal(t, "Context compacted (152355 → 4234 tokens).", data.Detail)
				require.Equal(t, "agentteam_proactive", data.Metadata["context_compaction_source"])
			},
		},
		{
			name:       "runtime error",
			parsedKind: consts.ParsedEventKindError,
			event: drivercontract.ParsedEvent{
				Kind:    consts.ParsedEventKindError,
				Message: "boom",
				Metadata: map[string]any{
					"will_retry":    true,
					"retry_attempt": float64(11),
				},
			},
			wantKind:  runtimeevents.KindTurnError,
			wantData:  &runtimeevents.TurnTextData{},
			wantError: true,
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.TurnTextData](ev)
				require.True(t, ok)
				require.Equal(t, "boom", data.Text)
				require.Equal(t, map[string]any{
					"will_retry":    true,
					"retry_attempt": float64(11),
				}, data.Metadata)
				agentEvents := mapRuntimeEventToAgentEvents(ev)
				require.Len(t, agentEvents, 1)
				trajectory, ok := agentEvents[0].(types.TrajectoryEvent)
				require.True(t, ok)
				require.Len(t, trajectory.Entries, 1)
				require.Equal(t, data.Metadata, trajectory.Entries[0].Metadata)
			},
		},
		{
			name:       "usage tokens",
			parsedKind: consts.ParsedEventKindUsage,
			event: drivercontract.ParsedEvent{
				Kind:              consts.ParsedEventKindUsage,
				Model:             "gpt-test",
				InputTokens:       12,
				CachedInputTokens: 3,
				OutputTokens:      7,
			},
			wantKind: runtimeevents.KindUsageTokens,
			wantData: &runtimeevents.UsageTokensData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.UsageTokensData](ev)
				require.True(t, ok)
				require.Equal(t, "gpt-test", ev.Context.Model)
				require.Equal(t, uint64(12), data.InputTokens)
				require.Equal(t, uint64(3), data.CachedInputTokens)
				require.Equal(t, uint64(7), data.OutputTokens)
			},
		},
		{
			name:       "prompt input",
			parsedKind: consts.ParsedEventKindPromptInput,
			event: drivercontract.ParsedEvent{
				Kind:       consts.ParsedEventKindPromptInput,
				Runtime:    "codex",
				Text:       "prompt text",
				PromptKind: "turn_input",
				EventType:  "wake",
			},
			wantKind: runtimeevents.KindPromptInput,
			wantData: &runtimeevents.PromptInputData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.PromptInputData](ev)
				require.True(t, ok)
				require.Equal(t, "codex", ev.Context.Runtime)
				require.Equal(t, "prompt text", data.Text)
				require.Equal(t, "turn_input", data.PromptKind)
				require.Equal(t, "wake", data.EventType)
			},
		},
		{
			name:       "runtime input pending",
			parsedKind: consts.ParsedEventKindRuntimeInputPending,
			event: drivercontract.ParsedEvent{
				Kind:           consts.ParsedEventKindRuntimeInputPending,
				Runtime:        "claude",
				SessionID:      "sess-1",
				RuntimeInputID: "msg-2",
				Text:           "New message received:\n\n[target=#agentteam msg=msg-2] @owner: hello",
				PromptKind:     consts.PromptKindDeliveryText,
				EventType:      "routed_message_delivery",
				Message:        "New message delivered to agent.",
			},
			wantKind: runtimeevents.KindDeliveryPending,
			wantData: &runtimeevents.LifecycleData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.LifecycleData](ev)
				require.True(t, ok)
				require.Equal(t, "claude", ev.Context.Runtime)
				require.Equal(t, "sess-1", ev.Context.SessionID)
				require.Equal(t, "msg-2", ev.Trace.MessageID)
				require.Equal(t, "msg-2", ev.Trace.CorrelationID)
				require.Equal(t, "message delivered", data.Activity)
				require.Equal(t, "New message delivered to agent.", data.Detail)
				require.Contains(t, data.Text, "@owner: hello")
				require.Equal(t, consts.PromptKindDeliveryText, data.PromptKind)
				require.Equal(t, "routed_message_delivery", data.EventType)
			},
		},
		{
			name:       "runtime input acked",
			parsedKind: consts.ParsedEventKindRuntimeInputAcked,
			event: drivercontract.ParsedEvent{
				Kind:           consts.ParsedEventKindRuntimeInputAcked,
				Runtime:        "claude",
				SessionID:      "sess-1",
				RuntimeInputID: "msg-2",
			},
			wantKind: runtimeevents.KindDeliveryAcked,
			wantData: &runtimeevents.LifecycleData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.LifecycleData](ev)
				require.True(t, ok)
				require.Equal(t, "claude", ev.Context.Runtime)
				require.Equal(t, "sess-1", ev.Context.SessionID)
				require.Equal(t, "msg-2", ev.Trace.MessageID)
				require.Equal(t, "msg-2", ev.Trace.CorrelationID)
				require.Equal(t, "Agent acknowledged message input.", data.Detail)
				require.Equal(t, "agent ack", data.Activity)
			},
		},
		{
			name:       "runtime input uncertain",
			parsedKind: consts.ParsedEventKindRuntimeInputUncertain,
			event: drivercontract.ParsedEvent{
				Kind:           consts.ParsedEventKindRuntimeInputUncertain,
				Runtime:        "codex",
				SessionID:      "sess-1",
				RuntimeInputID: "agentteam-tool-surface-v1:receipt",
			},
			wantKind: runtimeevents.KindDeliveryPending,
			wantData: &runtimeevents.LifecycleData{},
			check: func(t *testing.T, ev runtimeevents.Event) {
				data, ok := runtimeevents.DataAs[*runtimeevents.LifecycleData](ev)
				require.True(t, ok)
				require.Equal(t, "delivery uncertain", data.Activity)
				require.Equal(t, "Message delivery is awaiting reconciliation.", data.Detail)
				require.Equal(t, "agentteam-tool-surface-v1:receipt", ev.Trace.CorrelationID)
			},
		},
	}
}

func TestRuntimeEventFromParsedEventMappingManifest(t *testing.T) {
	manifest := parsedEventRuntimeMappingManifest()
	manifestKinds := make(map[string]struct{}, len(manifest))
	for _, tc := range manifest {
		t.Run(tc.name, func(t *testing.T) {
			manifestKinds[tc.parsedKind] = struct{}{}
			ev, ok := runtimeEventFromParsedEvent("agentteam", "agent-a", tc.event)
			require.True(t, ok)
			require.Equal(t, tc.wantKind, ev.Kind)
			require.Equal(t, "agentteam", ev.Context.ServerID)
			require.Equal(t, "agent-a", ev.Context.AgentID)
			require.Equal(t, "agent-a", ev.Context.ServerAgentID)
			if tc.event.SessionID != "" {
				require.Equal(t, tc.event.SessionID, ev.Context.SessionID)
			}
			if tc.wantData == nil {
				require.Nil(t, ev.Data)
			} else {
				require.IsType(t, tc.wantData, ev.Data)
			}
			if tc.wantError {
				require.Equal(t, runtimeevents.SeverityError, ev.Severity)
			}
			if tc.check != nil {
				tc.check(t, ev)
			}
		})
	}

	require.Equal(t, parsedEventKindsDeclaredInConsts(t), manifestKinds)
}

func TestRuntimeEventFromParsedEventUnknownFallbackIsTextOnly(t *testing.T) {
	ev, ok := runtimeEventFromParsedEvent("agentteam", "agent-a", drivercontract.ParsedEvent{
		Kind:    "runtime_private_status",
		Message: "private detail",
	})
	require.True(t, ok)
	require.Equal(t, runtimeevents.KindTurnText, ev.Kind)
	require.Equal(t, "private detail", payloadText(ev))

	_, ok = runtimeEventFromParsedEvent("agentteam", "agent-a", drivercontract.ParsedEvent{})
	require.False(t, ok)
}

func TestSubagentActivityEmitsTrajectoryWithoutOverwritingCurrentActivity(t *testing.T) {
	ev, ok := runtimeEventFromParsedEvent("agentteam", "agent-a", drivercontract.ParsedEvent{
		Kind:      consts.ParsedEventKindSubagentActivity,
		Message:   "Interacted with /root/reviewer",
		SessionID: "thread-primary",
		Runtime:   "opencode",
		EventType: "runtime_debug",
		Metadata: map[string]any{
			"activityKind":  "interacted",
			"agentThreadId": "thread-child",
			"agentPath":     "/root/reviewer",
		},
	})
	require.True(t, ok)

	activity, detail := runtimeEventActivity(ev)
	require.Empty(t, activity, "a parent-side projection must not replace the root session activity")
	require.Empty(t, detail)

	events := mapRuntimeEventToAgentEvents(ev)
	require.Len(t, events, 1)
	trajectory, ok := events[0].(types.TrajectoryEvent)
	require.True(t, ok)
	require.Equal(t, "thread-primary", trajectory.SessionID)
	require.Len(t, trajectory.Entries, 1)
	require.Equal(t, consts.TrajectoryKindStatus, trajectory.Entries[0].Kind)
	require.Equal(t, "working", trajectory.Entries[0].Activity)
	require.Equal(t, "Interacted with /root/reviewer", trajectory.Entries[0].Detail)
	require.Equal(t, "opencode", trajectory.Entries[0].Runtime)
	require.Equal(t, "runtime_debug", trajectory.Entries[0].EventType)
	require.Equal(t, "thread-child", trajectory.Entries[0].Metadata["agentThreadId"])
}

func parsedEventKindsDeclaredInConsts(t *testing.T) map[string]struct{} {
	t.Helper()
	_, file, _, ok := runtime.Caller(0)
	require.True(t, ok)
	path := filepath.Join(filepath.Dir(file), "..", "consts", "events.go")
	parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
	require.NoError(t, err)

	kinds := make(map[string]struct{})
	for _, decl := range parsed.Decls {
		gen, ok := decl.(*ast.GenDecl)
		if !ok || gen.Tok != token.CONST {
			continue
		}
		for _, spec := range gen.Specs {
			valueSpec, ok := spec.(*ast.ValueSpec)
			if !ok {
				continue
			}
			for i, name := range valueSpec.Names {
				if !strings.HasPrefix(name.Name, "ParsedEventKind") || i >= len(valueSpec.Values) {
					continue
				}
				lit, ok := valueSpec.Values[i].(*ast.BasicLit)
				require.True(t, ok, "ParsedEventKind %s should have an explicit string literal", name.Name)
				value, err := strconv.Unquote(lit.Value)
				require.NoError(t, err)
				kinds[value] = struct{}{}
			}
		}
	}
	return kinds
}

func TestAgentTeamReporterSinkMapsWakeStatusEvent(t *testing.T) {
	reporter := &reportingStubChannel{id: "agentteam"}
	sink := NewAgentTeamReporterSink(func(serverID string) types.EventReporter {
		require.Equal(t, "agentteam", serverID)
		return reporter
	}, nil)

	sink.Handle(runtimeevents.NewDataEvent(
		runtimeevents.NewWakeData(runtimeevents.KindWakeReplySent, "sent"),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerID:      "agentteam",
			ServerAgentID: "server-agent",
		}),
		runtimeevents.WithTrace(runtimeevents.EventTrace{
			MessageID:      "wake-msg",
			WakeToken:      "wake-token",
			Target:         "#agentteam:abc123",
			ReplyMessageID: "reply-msg",
		}),
	))

	require.Len(t, reporter.events, 1)
	require.Equal(t, types.WakeStatusEvent{
		AgentID:        "server-agent",
		MessageID:      "wake-msg",
		WakeToken:      "wake-token",
		Target:         "#agentteam:abc123",
		Status:         consts.WakeStatusReplySent,
		ReplyMessageID: "reply-msg",
		Detail:         "sent",
	}, reporter.events[0])
}

func TestMapRuntimeEventToAgentEventsRuntimeClassCompatibility(t *testing.T) {
	events := mapRuntimeEventToAgentEvents(runtimeevents.NewDataEvent(
		runtimeevents.NewRuntimeDiagnosticData(runtimeevents.KindRuntimeRetrying, consts.RuntimeEventClassRuntimeRetrying, "retrying"),
		runtimeevents.WithContext(runtimeevents.EventContext{ServerAgentID: "server-agent"}),
		runtimeevents.WithTrace(runtimeevents.EventTrace{
			MessageID: "msg-1",
			WakeToken: "wake-token",
			Target:    "#agentteam",
		}),
	))

	require.Equal(t, []types.AgentEvent{types.RuntimeEvent{
		AgentID:    "server-agent",
		MessageID:  "msg-1",
		WakeToken:  "wake-token",
		EventClass: consts.RuntimeEventClassRuntimeRetrying,
		Target:     "#agentteam",
		Detail:     "retrying",
	}}, events)
}

func TestMapRuntimeEventToAgentEventsRecoveryPoint(t *testing.T) {
	events := mapRuntimeEventToAgentEvents(runtimeevents.NewDataEvent(
		runtimeevents.NewRecoveryPointData("thread-1", "fork_session", "session-123"),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerAgentID: "server-agent",
			SessionID:     "thread-fallback",
		}),
		runtimeevents.WithTrace(runtimeevents.EventTrace{MessageID: "msg-1"}),
	))

	require.Equal(t, []types.AgentEvent{types.RecoveryPointPreparedEvent{
		AgentID:     "server-agent",
		MessageID:   "msg-1",
		ThreadID:    "thread-1",
		Strategy:    "fork_session",
		RecoveryRef: "session-123",
	}}, events)
}

func TestRuntimeEventMapperMatchesBuildEventCallbackForThinking(t *testing.T) {
	oldEvents := eventsFromBuildEventCallback(t, drivercontract.ParsedEvent{
		Kind:      consts.ParsedEventKindThinking,
		SessionID: "sess-thinking",
		Text:      "Planning the next step",
	})

	newEvents := mapRuntimeEventToAgentEvents(runtimeevents.NewDataEvent(
		runtimeevents.NewTurnTextData(runtimeevents.KindTurnThinking, "Planning the next step"),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerAgentID: "agent-a",
			SessionID:     "sess-thinking",
		}),
	))

	require.Equal(t, clearTrajectoryEventTimestamps(oldEvents), clearTrajectoryEventTimestamps(newEvents))
}

func TestRuntimeEventMapperMatchesBuildEventCallbackForToolCall(t *testing.T) {
	oldEvents := eventsFromBuildEventCallback(t, drivercontract.ParsedEvent{
		Kind:      consts.ParsedEventKindToolCall,
		SessionID: "sess-3",
		ToolName:  "exec_command",
		ToolInput: `{"cmd":"make e2e-web"}`,
	})

	newEvents := mapRuntimeEventToAgentEvents(runtimeevents.NewDataEvent(
		runtimeevents.NewToolCallData("exec_command", `{"cmd":"make e2e-web"}`),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerAgentID: "agent-a",
			SessionID:     "sess-3",
		}),
	))

	require.Equal(t, clearTrajectoryEventTimestamps(oldEvents), clearTrajectoryEventTimestamps(newEvents))
}

func TestRuntimeEventMapperMatchesBuildEventCallbackForRuntimeIdle(t *testing.T) {
	oldEvents := eventsFromBuildEventCallback(t, drivercontract.ParsedEvent{
		Kind:      consts.ParsedEventKindRuntimeIdle,
		SessionID: "sess-1",
	})

	newEvents := mapRuntimeEventToAgentEvents(runtimeevents.NewEvent(
		runtimeevents.KindTurnIdle,
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerAgentID: "agent-a",
			SessionID:     "sess-1",
		}),
	))

	require.Equal(t, clearTrajectoryEventTimestamps(oldEvents), clearTrajectoryEventTimestamps(newEvents))
}

func TestRuntimeEventMapperMatchesBuildEventCallbackForPromptInput(t *testing.T) {
	oldEvents := eventsFromBuildEventCallback(t, drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindPromptInput,
		SessionID:  "sess-4",
		Text:       "full prompt text",
		PromptKind: consts.PromptKindTurnInput,
		Runtime:    "codex-app-server",
		EventType:  "wake",
	})

	newEvents := mapRuntimeEventToAgentEvents(runtimeevents.NewDataEvent(
		runtimeevents.NewPromptInputData("full prompt text", "turn_input", "wake"),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerAgentID: "agent-a",
			SessionID:     "sess-4",
			Runtime:       "codex-app-server",
		}),
	))

	require.Equal(t, clearTrajectoryEventTimestamps(oldEvents), clearTrajectoryEventTimestamps(newEvents))
}

func TestTrajectorySinkBroadcastsTrajectoryEntries(t *testing.T) {
	d := mustNewDispatcher("")
	t.Cleanup(d.Close)
	sink := NewTrajectorySink(d.trajectoryHub)

	sink.Handle(runtimeevents.NewDataEvent(
		runtimeevents.NewToolCallData("exec_command", `{"cmd":"go test ./..."}`),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerAgentID: "agent-a",
			SessionID:     "sess-1",
		}),
	))

	items := d.trajectoryHub.Recent("agent-a", 10)
	require.Len(t, items, 2)
	require.Equal(t, "agent-a", items[0].AgentID)
	require.Equal(t, consts.TrajectoryKindToolStart, items[0].Entry.Kind)
	require.Equal(t, "exec_command", items[0].Entry.ToolName)
	require.Equal(t, consts.TrajectoryKindStatus, items[1].Entry.Kind)
	require.Equal(t, "working", items[1].Entry.Activity)
	require.Equal(t, "Running command…", items[1].Entry.Detail)
}

func TestContextUsageMapsToSessionScopedTrajectorySnapshot(t *testing.T) {
	usage := runtimeevents.NewUsageTokensData("", 12, 3, 7, 0)
	usage.ActiveContextTokens = 48_000
	usage.ContextWindowTokens = 258_400
	events := mapRuntimeEventToAgentEvents(runtimeevents.NewDataEvent(
		usage,
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerAgentID: "agent-a",
			SessionID:     "sess-1",
			Runtime:       "codex",
		}),
	))

	require.Len(t, events, 1)
	trajectory, ok := events[0].(types.TrajectoryEvent)
	require.True(t, ok)
	require.Equal(t, "sess-1", trajectory.SessionID)
	require.Len(t, trajectory.Entries, 1)
	require.Equal(t, consts.TrajectoryKindContextUsage, trajectory.Entries[0].Kind)
	require.Equal(t, "sess-1", trajectory.Entries[0].SessionID)
	require.Equal(t, uint64(48_000), trajectory.Entries[0].ActiveContextTokens)
	require.Equal(t, uint64(258_400), trajectory.Entries[0].ContextWindowTokens)
}

func TestMetricsSinkRecordsUsageAndChannelEvents(t *testing.T) {
	var request struct {
		apiKey string
		model  string
		input  uint64
		cached uint64
		output uint64
		dur    time.Duration
	}
	var received, sent []string

	sink := &MetricsSink{
		recordRequest: func(apiKey, model string, inputTokens, cachedInputTokens, outputTokens uint64, duration time.Duration) {
			request.apiKey = apiKey
			request.model = model
			request.input = inputTokens
			request.cached = cachedInputTokens
			request.output = outputTokens
			request.dur = duration
		},
		recordReceived: func(serverID, agentID string) {
			received = append(received, serverID+":"+agentID)
		},
		recordSent: func(serverID, agentID string) {
			sent = append(sent, serverID+":"+agentID)
		},
	}

	sink.Handle(runtimeevents.NewDataEvent(
		runtimeevents.NewUsageTokensData("sk-test", 12, 3, 7, 250*time.Millisecond),
		runtimeevents.WithContext(runtimeevents.EventContext{Model: "gpt-test"}),
	))
	sink.Handle(runtimeevents.NewEvent(
		runtimeevents.KindServerMessageReceived,
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerID:      "agentteam",
			ServerAgentID: "agent-a",
		}),
	))
	sink.Handle(runtimeevents.NewEvent(
		runtimeevents.KindServerMessageSent,
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerID:      "agentteam",
			ServerAgentID: "agent-a",
		}),
	))

	require.Equal(t, "sk-test", request.apiKey)
	require.Equal(t, "gpt-test", request.model)
	require.Equal(t, uint64(12), request.input)
	require.Equal(t, uint64(3), request.cached)
	require.Equal(t, uint64(7), request.output)
	require.Equal(t, 250*time.Millisecond, request.dur)
	require.Equal(t, []string{"agentteam:agent-a"}, received)
	require.Equal(t, []string{"agentteam:agent-a"}, sent)
}

func TestMetricsSinkRecordsUsageFromMapDataFallback(t *testing.T) {
	var request struct {
		apiKey string
		model  string
		input  uint64
		cached uint64
		output uint64
		dur    time.Duration
	}
	sink := &MetricsSink{
		recordRequest: func(apiKey, model string, inputTokens, cachedInputTokens, outputTokens uint64, duration time.Duration) {
			request.apiKey = apiKey
			request.model = model
			request.input = inputTokens
			request.cached = cachedInputTokens
			request.output = outputTokens
			request.dur = duration
		},
	}

	sink.Handle(runtimeevents.NewEvent(
		runtimeevents.KindUsageTokens,
		runtimeevents.WithContext(runtimeevents.EventContext{Model: "gpt-map"}),
		runtimeevents.WithMapData(runtimeevents.KindUsageTokens, map[string]any{
			"api_key":     "sk-map",
			"input":       float64(12),
			"cached":      float64(3),
			"output":      float64(7),
			"duration_ms": float64(250),
		}),
	))

	require.Equal(t, "sk-map", request.apiKey)
	require.Equal(t, "gpt-map", request.model)
	require.Equal(t, uint64(12), request.input)
	require.Equal(t, uint64(3), request.cached)
	require.Equal(t, uint64(7), request.output)
	require.Equal(t, 250*time.Millisecond, request.dur)
}

func TestRuntimeEventBusFanoutToReporterTrajectoryAndMetricsSinks(t *testing.T) {
	bus := runtimeevents.NewBus()
	defer bus.Close()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	reporter := &reportingStubChannel{id: "agentteam"}
	agentteamSink := NewAgentTeamReporterSink(func(channelID string) types.EventReporter {
		if channelID != "agentteam" {
			return nil
		}
		return reporter
	}, nil)
	agentteamSink.Subscribe(ctx, bus)
	d := mustNewDispatcher("")
	t.Cleanup(d.Close)
	trajectorySink := NewTrajectorySink(d.trajectoryHub)
	trajectorySink.Subscribe(ctx, bus)

	var request recordingMetricsRequest
	metricsSink := &MetricsSink{
		recordRequest: func(apiKey, model string, inputTokens, cachedInputTokens, outputTokens uint64, duration time.Duration) {
			request.record(apiKey, model, inputTokens, cachedInputTokens, outputTokens, duration)
		},
	}
	metricsSink.Subscribe(ctx, bus)

	bus.Publish(context.Background(), runtimeevents.NewDataEvent(
		runtimeevents.NewToolCallData("mcp__agentteam__message_send", `{"target":"#agentteam","content":"hi"}`),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerID:      "agentteam",
			ServerAgentID: "agent-a",
			SessionID:     "sess-1",
		}),
	))
	bus.Publish(context.Background(), runtimeevents.NewDataEvent(
		runtimeevents.NewUsageTokensData("sk-test", 12, 3, 7, 250*time.Millisecond),
		runtimeevents.WithContext(runtimeevents.EventContext{
			Model: "gpt-test",
		}),
	))

	require.Eventually(t, func() bool {
		snap := request.snapshot()
		return reporter.eventCount() == 2 &&
			len(d.trajectoryHub.Recent("agent-a", 10)) == 2 &&
			snap.apiKey == "sk-test"
	}, time.Second, 10*time.Millisecond)

	events := reporter.eventSnapshot()
	require.IsType(t, types.ActivityEvent{}, events[0])
	require.IsType(t, types.TrajectoryEvent{}, events[1])
	trajectory := d.trajectoryHub.Recent("agent-a", 10)
	reportedTrajectory := events[1].(types.TrajectoryEvent)
	require.Equal(t, trajectory[0].Timestamp, reportedTrajectory.Timestamp)
	require.Equal(t, consts.TrajectoryKindToolStart, trajectory[0].Entry.Kind)
	require.Equal(t, consts.TrajectoryKindStatus, trajectory[1].Entry.Kind)
	requestSnap := request.snapshot()
	require.Equal(t, "gpt-test", requestSnap.model)
	require.Equal(t, uint64(12), requestSnap.input)
	require.Equal(t, uint64(3), requestSnap.cached)
	require.Equal(t, uint64(7), requestSnap.output)
	require.Equal(t, 250*time.Millisecond, requestSnap.dur)
}

func TestAgentTeamReporterSinkSubscribeConsumesBusEvents(t *testing.T) {
	reporter := &reportingStubChannel{id: "agentteam"}
	bus := runtimeevents.NewBus()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sink := NewAgentTeamReporterSink(func(string) types.EventReporter { return reporter }, nil)
	sink.Subscribe(ctx, bus)

	bus.Publish(context.Background(), runtimeevents.NewDataEvent(
		runtimeevents.NewLifecycleData(runtimeevents.KindSessionSleeping, "sleeping", ""),
		runtimeevents.WithContext(runtimeevents.EventContext{
			ServerID:      "agentteam",
			ServerAgentID: "agent-a",
		}),
	))
	require.Eventually(t, func() bool { return reporter.eventCount() == 2 }, time.Second, 10*time.Millisecond)
	events := reporter.eventSnapshot()
	require.Equal(t, types.StatusEvent{
		AgentID: "agent-a",
		Status:  "sleeping",
	}, events[0])
	require.Equal(t, types.ActivityEvent{
		AgentID:  "agent-a",
		Activity: "sleeping",
	}, events[1])
}

func eventsFromBuildEventCallback(t *testing.T, ev drivercontract.ParsedEvent) []types.AgentEvent {
	t.Helper()
	d := mustNewDispatcher("")
	t.Cleanup(d.Close)
	mgr := channelManagerForRuntimeEventTest(d)
	ch := &reportingStubChannel{id: "agentteam"}
	mgr.Register(ch)
	d.serverConnections = mgr

	callback := d.buildEventCallback("agentteam", "agent-a", "agent-a", "")
	callback(ev)
	return ch.events
}

func clearTrajectoryEventTimestamps(events []types.AgentEvent) []types.AgentEvent {
	out := append([]types.AgentEvent(nil), events...)
	for i, event := range out {
		if trajectory, ok := event.(types.TrajectoryEvent); ok {
			trajectory.Timestamp = 0
			out[i] = trajectory
		}
	}
	return out
}

func channelManagerForRuntimeEventTest(d *Dispatcher) *manager.Manager {
	return manager.New(d, nil)
}
