package runtimeevents

import (
	"encoding/json"
	"testing"
	"time"
)

func TestJSONRoundTripLifecycleData(t *testing.T) {
	orig := NewDataEvent(
		NewLifecycleData(KindSessionStarting, "working", "Starting…"),
		WithContext(EventContext{
			AgentID:  "agent1",
			ServerID: "srv1",
		}),
	)

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	if restored.Kind != KindSessionStarting {
		t.Errorf("Kind = %q, want %q", restored.Kind, KindSessionStarting)
	}
	if restored.Context.AgentID != "agent1" {
		t.Errorf("AgentID = %q, want %q", restored.Context.AgentID, "agent1")
	}

	d, ok := DataAs[*LifecycleData](restored)
	if !ok {
		t.Fatal("DataAs[*LifecycleData] failed")
	}
	if d.Activity != "working" {
		t.Errorf("Activity = %q, want %q", d.Activity, "working")
	}
	if d.Detail != "Starting…" {
		t.Errorf("Detail = %q, want %q", d.Detail, "Starting…")
	}
	if d.EventKind() != KindSessionStarting {
		t.Errorf("EventKind = %q, want %q", d.EventKind(), KindSessionStarting)
	}
}

func TestJSONRoundTripLifecycleInputData(t *testing.T) {
	orig := NewDataEvent(
		NewLifecycleInputData(
			KindDeliveryPending,
			"message delivered",
			"New message delivered to agent.",
			"New message received:\n\n[target=#agentteam msg=msg-1] @owner: hello",
			"delivery_text",
			"routed_message_delivery",
			map[string]any{"deliveryState": "pending"},
		),
		WithContext(EventContext{Runtime: "claude", SessionID: "sess-1"}),
	)

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	if restored.Kind != KindDeliveryPending {
		t.Errorf("Kind = %q, want %q", restored.Kind, KindDeliveryPending)
	}
	d, ok := DataAs[*LifecycleData](restored)
	if !ok {
		t.Fatal("DataAs[*LifecycleData] failed")
	}
	if d.Activity != "message delivered" {
		t.Errorf("Activity = %q", d.Activity)
	}
	if d.Detail != "New message delivered to agent." {
		t.Errorf("Detail = %q", d.Detail)
	}
	if d.Text != "New message received:\n\n[target=#agentteam msg=msg-1] @owner: hello" {
		t.Errorf("Text = %q", d.Text)
	}
	if d.PromptKind != "delivery_text" {
		t.Errorf("PromptKind = %q", d.PromptKind)
	}
	if d.EventType != "routed_message_delivery" {
		t.Errorf("EventType = %q", d.EventType)
	}
	if d.Metadata["deliveryState"] != "pending" {
		t.Errorf("Metadata = %#v", d.Metadata)
	}
}

func TestJSONRoundTripSubagentActivityData(t *testing.T) {
	orig := NewDataEvent(NewLifecycleInputData(
		KindTurnSubagentActivity,
		"working",
		"Started /root/reviewer",
		"",
		"",
		"",
		map[string]any{"agentThreadId": "thread-child", "activityKind": "started"},
	))

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}
	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}
	if restored.Kind != KindTurnSubagentActivity {
		t.Errorf("Kind = %q, want %q", restored.Kind, KindTurnSubagentActivity)
	}
	d, ok := DataAs[*LifecycleData](restored)
	if !ok {
		t.Fatal("DataAs[*LifecycleData] failed")
	}
	if d.Detail != "Started /root/reviewer" {
		t.Errorf("Detail = %q", d.Detail)
	}
	if d.Metadata["agentThreadId"] != "thread-child" {
		t.Errorf("Metadata = %#v", d.Metadata)
	}
}

func TestJSONRoundTripWakeData(t *testing.T) {
	orig := NewDataEvent(
		NewWakeData(KindWakeReceived, "user sent a message"),
		WithTrace(EventTrace{MessageID: "msg123"}),
	)

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	if restored.Kind != KindWakeReceived {
		t.Errorf("Kind = %q, want %q", restored.Kind, KindWakeReceived)
	}
	if restored.Trace.MessageID != "msg123" {
		t.Errorf("MessageID = %q, want %q", restored.Trace.MessageID, "msg123")
	}

	d, ok := DataAs[*WakeData](restored)
	if !ok {
		t.Fatal("DataAs[*WakeData] failed")
	}
	if d.Detail != "user sent a message" {
		t.Errorf("Detail = %q, want %q", d.Detail, "user sent a message")
	}
	if d.EventKind() != KindWakeReceived {
		t.Errorf("EventKind = %q, want %q", d.EventKind(), KindWakeReceived)
	}
}

func TestJSONRoundTripRuntimeDiagnosticData(t *testing.T) {
	orig := NewDataEvent(
		NewRuntimeDiagnosticData(KindRuntimeRetrying, "runtime_retrying", "retrying transient error"),
	)

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*RuntimeDiagnosticData](restored)
	if !ok {
		t.Fatal("DataAs[*RuntimeDiagnosticData] failed")
	}
	if d.EventClass != "runtime_retrying" {
		t.Errorf("EventClass = %q, want %q", d.EventClass, "runtime_retrying")
	}
	if d.Detail != "retrying transient error" {
		t.Errorf("Detail = %q, want %q", d.Detail, "retrying transient error")
	}
	if d.EventKind() != KindRuntimeRetrying {
		t.Errorf("EventKind = %q, want %q", d.EventKind(), KindRuntimeRetrying)
	}
}

func TestJSONRoundTripTurnTextData(t *testing.T) {
	for _, kind := range []Kind{KindTurnThinking, KindTurnText, KindTurnError} {
		t.Run(string(kind), func(t *testing.T) {
			orig := NewDataEvent(NewTurnTextDataWithMeta(kind, "hello world", "Greeting", "section-1"))
			orig.Data.(*TurnTextData).Metadata = map[string]any{"will_retry": true}

			b, err := json.Marshal(orig)
			if err != nil {
				t.Fatalf("Marshal: %v", err)
			}

			var restored Event
			if err := json.Unmarshal(b, &restored); err != nil {
				t.Fatalf("Unmarshal: %v", err)
			}

			if restored.Kind != kind {
				t.Errorf("Kind = %q, want %q", restored.Kind, kind)
			}

			d, ok := DataAs[*TurnTextData](restored)
			if !ok {
				t.Fatal("DataAs[*TurnTextData] failed")
			}
			if d.Text != "hello world" {
				t.Errorf("Text = %q, want %q", d.Text, "hello world")
			}
			if d.Title != "Greeting" {
				t.Errorf("Title = %q, want %q", d.Title, "Greeting")
			}
			if d.SectionID != "section-1" {
				t.Errorf("SectionID = %q, want %q", d.SectionID, "section-1")
			}
			if d.Metadata["will_retry"] != true {
				t.Errorf("Metadata[will_retry] = %v, want true", d.Metadata["will_retry"])
			}
			if d.EventKind() != kind {
				t.Errorf("EventKind = %q, want %q", d.EventKind(), kind)
			}
		})
	}
}

func TestJSONRoundTripToolCallData(t *testing.T) {
	orig := NewDataEvent(NewToolCallData("bash", "ls -la"))

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*ToolCallData](restored)
	if !ok {
		t.Fatal("DataAs[*ToolCallData] failed")
	}
	if d.ToolName != "bash" {
		t.Errorf("ToolName = %q, want %q", d.ToolName, "bash")
	}
	if d.ToolInput != "ls -la" {
		t.Errorf("ToolInput = %q, want %q", d.ToolInput, "ls -la")
	}
}

func TestJSONRoundTripToolOutputDataPreservesKind(t *testing.T) {
	orig := NewDataEvent(NewToolEventDataWithCallIDAndMetadata(KindTurnToolOutput, "shell", "line 1", "call-1", map[string]any{
		"customFlag":  true,
		"customCount": 2,
		"customState": "ok",
	}))

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*ToolCallData](restored)
	if !ok {
		t.Fatal("DataAs[*ToolCallData] failed")
	}
	if d.EventKind() != KindTurnToolOutput {
		t.Errorf("EventKind = %q, want %q", d.EventKind(), KindTurnToolOutput)
	}
	if d.ToolName != "shell" {
		t.Errorf("ToolName = %q, want %q", d.ToolName, "shell")
	}
	if d.ToolInput != "line 1" {
		t.Errorf("ToolInput = %q, want %q", d.ToolInput, "line 1")
	}
	if d.ToolCallID != "call-1" {
		t.Errorf("ToolCallID = %q, want %q", d.ToolCallID, "call-1")
	}
	if got := d.Metadata["customFlag"]; got != true {
		t.Errorf("Metadata[customFlag] = %#v, want true", got)
	}
	if got := d.Metadata["customCount"]; got != float64(2) {
		t.Errorf("Metadata[customCount] = %#v, want 2", got)
	}
	if got := d.Metadata["customState"]; got != "ok" {
		t.Errorf("Metadata[customState] = %#v, want ok", got)
	}
}

func TestJSONRoundTripPromptInputData(t *testing.T) {
	orig := NewDataEvent(NewPromptInputData("some prompt text", "wake", "prompt_input"))

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*PromptInputData](restored)
	if !ok {
		t.Fatal("DataAs[*PromptInputData] failed")
	}
	if d.Text != "some prompt text" {
		t.Errorf("Text = %q, want %q", d.Text, "some prompt text")
	}
	if d.PromptKind != "wake" {
		t.Errorf("PromptKind = %q, want %q", d.PromptKind, "wake")
	}
	if d.EventType != "prompt_input" {
		t.Errorf("EventType = %q, want %q", d.EventType, "prompt_input")
	}
}

func TestJSONRoundTripRecoveryPointData(t *testing.T) {
	orig := NewDataEvent(NewRecoveryPointData("thread1", "compact", "ref123"))

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*RecoveryPointData](restored)
	if !ok {
		t.Fatal("DataAs[*RecoveryPointData] failed")
	}
	if d.ThreadID != "thread1" {
		t.Errorf("ThreadID = %q, want %q", d.ThreadID, "thread1")
	}
	if d.Strategy != "compact" {
		t.Errorf("Strategy = %q, want %q", d.Strategy, "compact")
	}
	if d.RecoveryRef != "ref123" {
		t.Errorf("RecoveryRef = %q, want %q", d.RecoveryRef, "ref123")
	}
}

func TestJSONRoundTripUsageTokensData(t *testing.T) {
	dur := 5 * time.Second
	usage := NewUsageTokensData("sk-test", 100, 50, 200, dur)
	usage.ActiveContextTokens = 48_000
	usage.ContextWindowTokens = 258_400
	orig := NewDataEvent(usage)

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*UsageTokensData](restored)
	if !ok {
		t.Fatal("DataAs[*UsageTokensData] failed")
	}
	if d.APIKey != "sk-test" {
		t.Errorf("APIKey = %q, want %q", d.APIKey, "sk-test")
	}
	if d.InputTokens != 100 {
		t.Errorf("InputTokens = %d, want 100", d.InputTokens)
	}
	if d.ActiveContextTokens != 48_000 {
		t.Errorf("ActiveContextTokens = %d, want 48000", d.ActiveContextTokens)
	}
	if d.ContextWindowTokens != 258_400 {
		t.Errorf("ContextWindowTokens = %d, want 258400", d.ContextWindowTokens)
	}
	if d.CachedInputTokens != 50 {
		t.Errorf("CachedInputTokens = %d, want 50", d.CachedInputTokens)
	}
	if d.OutputTokens != 200 {
		t.Errorf("OutputTokens = %d, want 200", d.OutputTokens)
	}
	if d.Duration != dur {
		t.Errorf("Duration = %v, want %v", d.Duration, dur)
	}
}

func TestJSONRoundTripHeartbeatData(t *testing.T) {
	orig := NewEvent(KindRuntimeHeartbeat)
	orig.Data = &HeartbeatData{
		State:       "active",
		IsIdle:      false,
		InboxDepth:  3,
		AckedCount:  1,
		LastEventAt: time.Now().Truncate(time.Millisecond),
		PendingWake: true,
	}

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*HeartbeatData](restored)
	if !ok {
		t.Fatal("DataAs[*HeartbeatData] failed")
	}
	if d.State != "active" {
		t.Errorf("State = %q, want %q", d.State, "active")
	}
	if d.InboxDepth != 3 {
		t.Errorf("InboxDepth = %d, want 3", d.InboxDepth)
	}
	if !d.PendingWake {
		t.Errorf("PendingWake = false, want true")
	}
}

func TestJSONRoundTripNoData(t *testing.T) {
	orig := NewEvent(KindTurnSessionInit, WithContext(EventContext{
		AgentID: "a1",
	}))

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	if restored.Kind != KindTurnSessionInit {
		t.Errorf("Kind = %q, want %q", restored.Kind, KindTurnSessionInit)
	}
	if restored.Data != nil {
		t.Errorf("Data should be nil for no-data kind, got %T", restored.Data)
	}
}

func TestJSONRoundTripUnknownKind(t *testing.T) {
	// Unknown kind with map data should fall back to MapData.
	jsonStr := `{
		"id": "test123",
		"kind": "custom.unknown",
		"data": {"foo": "bar", "num": 42}
	}`

	var restored Event
	if err := json.Unmarshal([]byte(jsonStr), &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	if restored.Kind != "custom.unknown" {
		t.Errorf("Kind = %q, want %q", restored.Kind, "custom.unknown")
	}

	d, ok := DataAs[*MapData](restored)
	if !ok {
		t.Fatal("DataAs[*MapData] failed for unknown kind")
	}
	if d.Map["foo"] != "bar" {
		t.Errorf("foo = %v, want bar", d.Map["foo"])
	}
}

func TestJSONRoundTripOldPayloadCompat(t *testing.T) {
	// Simulate a legacy map payload with snake_case keys — it should still
	// decode via the typed data decoder.
	jsonStr := `{
		"id": "compat1",
		"kind": "usage.tokens",
		"data": {
			"api_key": "sk-legacy",
			"input_tokens": 500,
			"cached_input_tokens": 100,
			"output_tokens": 300,
			"duration": 2000000000
		}
	}`

	var restored Event
	if err := json.Unmarshal([]byte(jsonStr), &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*UsageTokensData](restored)
	if !ok {
		t.Fatal("DataAs[*UsageTokensData] failed")
	}
	if d.APIKey != "sk-legacy" {
		t.Errorf("APIKey = %q, want %q", d.APIKey, "sk-legacy")
	}
	if d.InputTokens != 500 {
		t.Errorf("InputTokens = %d, want 500", d.InputTokens)
	}
	if d.Duration != 2*time.Second {
		t.Errorf("Duration = %v, want 2s", d.Duration)
	}
}

func TestJSONLegacyPayloadFieldCompat(t *testing.T) {
	// Pre-typed-data events used a top-level "payload" field. Decode it as an
	// input-only compatibility path, while continuing to marshal new events as
	// "data".
	jsonStr := `{
		"id": "compat-payload",
		"kind": "usage.tokens",
		"payload": {
			"api_key": "sk-payload",
			"input": 9,
			"cached": 1,
			"output": 5,
			"duration_ms": 125
		}
	}`

	var restored Event
	if err := json.Unmarshal([]byte(jsonStr), &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*UsageTokensData](restored)
	if !ok {
		t.Fatalf("DataAs[*UsageTokensData] failed, got %T", restored.Data)
	}
	if d.APIKey != "sk-payload" {
		t.Errorf("APIKey = %q, want %q", d.APIKey, "sk-payload")
	}
	if d.InputTokens != 9 || d.CachedInputTokens != 1 || d.OutputTokens != 5 {
		t.Errorf("tokens = (%d,%d,%d), want (9,1,5)", d.InputTokens, d.CachedInputTokens, d.OutputTokens)
	}
	if d.Duration != 125*time.Millisecond {
		t.Errorf("Duration = %v, want 125ms", d.Duration)
	}

	b, err := json.Marshal(restored)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}
	var out map[string]json.RawMessage
	if err := json.Unmarshal(b, &out); err != nil {
		t.Fatalf("Unmarshal remarshal output: %v", err)
	}
	if _, ok := out["payload"]; ok {
		t.Fatalf("remarshal should not emit legacy payload field: %s", b)
	}
	if _, ok := out["data"]; !ok {
		t.Fatalf("remarshal should emit data field: %s", b)
	}
}

func TestJSONMarshalPreservesTime(t *testing.T) {
	ts := time.Date(2026, 4, 27, 14, 30, 0, 0, time.UTC)
	orig := Event{
		ID:   "time-test",
		Time: ts,
		Kind: KindSessionStarted,
	}

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	// time.Time may lose precision in round-trip; check within 1ms.
	diff := restored.Time.Sub(ts)
	if diff < -time.Millisecond || diff > time.Millisecond {
		t.Errorf("Time = %v, want %v (diff=%v)", restored.Time, ts, diff)
	}
}

func TestJSONMapDataRoundTrip(t *testing.T) {
	// MapData wraps a map[string]any which serializes as {"map":{...}}.
	// When deserialized for a kind with a registered decoder, the typed
	// decoder takes priority. So for an unknown kind, MapData round-trips
	// as MapData.
	unknownKind := Kind("custom.test")
	orig := NewEvent(unknownKind, WithMapData(unknownKind, map[string]any{
		"activity": "testing",
		"count":    float64(42),
	}))

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	// Unknown kind falls back to MapData.
	d, ok := DataAs[*MapData](restored)
	if !ok {
		t.Fatalf("DataAs[*MapData] failed, got %T", restored.Data)
	}
	if d.Map["activity"] != "testing" {
		t.Errorf("activity = %v, want testing", d.Map["activity"])
	}
	if d.Map["count"] != float64(42) {
		t.Errorf("count = %v, want 42", d.Map["count"])
	}
}

func TestJSONKnownKindMapDataWrapperCompat(t *testing.T) {
	// MapData was the migration bridge for old map-style payloads. When it is
	// stored under a known kind, JSON decoding should still recover the typed
	// data instead of silently producing a zero-value concrete payload.
	orig := NewEvent(KindUsageTokens, WithMapData(KindUsageTokens, map[string]any{
		"api_key":             "sk-map",
		"input_tokens":        uint64(11),
		"cached_input_tokens": uint64(2),
		"output_tokens":       uint64(7),
		"duration":            150 * time.Millisecond,
	}))
	orig.Context.Model = "gpt-test"

	b, err := json.Marshal(orig)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var restored Event
	if err := json.Unmarshal(b, &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}

	d, ok := DataAs[*UsageTokensData](restored)
	if !ok {
		t.Fatalf("DataAs[*UsageTokensData] failed, got %T", restored.Data)
	}
	if d.APIKey != "sk-map" {
		t.Errorf("APIKey = %q, want %q", d.APIKey, "sk-map")
	}
	if d.InputTokens != 11 || d.CachedInputTokens != 2 || d.OutputTokens != 7 {
		t.Errorf("tokens = (%d,%d,%d), want (11,2,7)", d.InputTokens, d.CachedInputTokens, d.OutputTokens)
	}
	if d.Duration != 150*time.Millisecond {
		t.Errorf("Duration = %v, want 150ms", d.Duration)
	}
}

func TestJSONRawDataReMarshalPreservesRawPayload(t *testing.T) {
	// Decode failures intentionally fall back to RawData so the event can be
	// carried forward. Re-marshalling must keep the original raw data instead
	// of replacing it with an empty object.
	jsonStr := `{
		"id": "raw1",
		"kind": "usage.tokens",
		"data": "not-an-object"
	}`

	var restored Event
	if err := json.Unmarshal([]byte(jsonStr), &restored); err != nil {
		t.Fatalf("Unmarshal: %v", err)
	}
	if _, ok := DataAs[*RawData](restored); !ok {
		t.Fatalf("DataAs[*RawData] failed, got %T", restored.Data)
	}

	b, err := json.Marshal(restored)
	if err != nil {
		t.Fatalf("Marshal: %v", err)
	}

	var got struct {
		Data json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(b, &got); err != nil {
		t.Fatalf("Unmarshal remarshal output: %v", err)
	}
	if string(got.Data) != `"not-an-object"` {
		t.Errorf("raw data = %s, want %s", got.Data, `"not-an-object"`)
	}
}
