package protocol

import (
	"encoding/json"
	"testing"
)

// Golden wire frames as emitted by the Kotlin server (kotlinx.serialization). These
// strings are the cross-language contract: the Kotlin side pins the encode in
// ServerMessageSerializationTest; this test pins the Go decode of the exact same bytes,
// so a discriminator (@SerialName) or field-name drift fails loudly on one side.
func TestDecodeGoldenServerFrames(t *testing.T) {
	cases := []struct {
		name      string
		json      string
		wantType  string
		eventBear bool
		check     func(t *testing.T, m ServerMessage)
	}{
		{
			name:      "session.announced",
			json:      `{"type":"session.announced","sessionId":"s1","workflowId":"healthcheck","sequence":6,"sessionSequence":1}`,
			wantType:  TypeSessionAnnounced,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.SessionID != "s1" || m.WorkflowID != "healthcheck" {
					t.Fatalf("session.announced fields: %+v", m)
				}
			},
		},
		{
			name:      "chat.turn",
			json:      `{"type":"chat.turn","sessionId":"s1","turnId":"t9","role":"ROUTER","content":"hi there","sequence":7,"sessionSequence":2}`,
			wantType:  TypeChatTurn,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.Role != "ROUTER" || m.Content != "hi there" || m.TurnID != "t9" {
					t.Fatalf("chat.turn fields: %+v", m)
				}
			},
		},
		{
			name:      "stage.started",
			json:      `{"type":"stage.started","sessionId":"s1","stageId":"write_script","occurredAt":123,"sequence":8,"sessionSequence":3}`,
			wantType:  TypeStageStarted,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.StageID != "write_script" {
					t.Fatalf("stage.started fields: %+v", m)
				}
			},
		},
		{
			name:      "tool.completed",
			json:      `{"type":"tool.completed","sessionId":"s1","toolName":"shell","outputSummary":"ok","occurredAt":1,"diff":null,"sequence":9,"sessionSequence":4}`,
			wantType:  TypeToolCompleted,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.ToolName != "shell" || m.Summary != "ok" {
					t.Fatalf("tool.completed fields: %+v", m)
				}
			},
		},
		{
			// Plane-2 rationale (verified preconditions) must survive decode — it is the
			// justification shown in the approval band, not the opaque tier.
			name:      "approval.required carries plane-2 rationale",
			json:      `{"type":"approval.required","sessionId":"s1","requestId":"req-1","tier":"T3","riskSummary":{"level":"MEDIUM","factors":[],"recommendedAction":"PROMPT_USER","rationale":["[PATH_OUTSIDE_WORKSPACE] outside: /tmp/x"]},"toolName":"file_write","preview":"--- a/x\n+++ b/x","sequence":12,"sessionSequence":3}`,
			wantType:  TypeApprovalRequired,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.RiskSummary == nil {
					t.Fatalf("approval.required: riskSummary decoded nil")
				}
				if len(m.RiskSummary.Rationale) != 1 || m.RiskSummary.Rationale[0] != "[PATH_OUTSIDE_WORKSPACE] outside: /tmp/x" {
					t.Fatalf("rationale not decoded: %+v", m.RiskSummary)
				}
			},
		},
		{
			name:      "session.workspace_bound carries the cwd",
			json:      `{"type":"session.workspace_bound","sessionId":"s1","workspaceRoot":"/home/kami/Programs/correx","sequence":4,"sessionSequence":1}`,
			wantType:  TypeWorkspaceBound,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.WorkspaceRoot != "/home/kami/Programs/correx" {
					t.Fatalf("workspaceRoot not decoded: %+v", m)
				}
			},
		},
		{
			name:      "chat.turn carries router metrics",
			json:      `{"type":"chat.turn","sessionId":"s1","turnId":"t9","role":"ROUTER","content":"hi","latencyMs":3000,"totalTokens":943,"sequence":7,"sessionSequence":2}`,
			wantType:  TypeChatTurn,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.LatencyMs == nil || *m.LatencyMs != 3000 || m.TotalTokens == nil || *m.TotalTokens != 943 {
					t.Fatalf("chat.turn metrics: %+v", m)
				}
			},
		},
		{
			name:      "router.narration carries content + metrics",
			json:      `{"type":"router.narration","sessionId":"s1","content":"moving on","stageId":"validate","latencyMs":2000,"totalTokens":120,"sequence":10,"sessionSequence":5}`,
			wantType:  TypeRouterNarration,
			eventBear: true,
			check: func(t *testing.T, m ServerMessage) {
				if m.Content != "moving on" || m.LatencyMs == nil || *m.LatencyMs != 2000 || m.TotalTokens == nil || *m.TotalTokens != 120 {
					t.Fatalf("router.narration: %+v", m)
				}
			},
		},
		{
			name:      "snapshot_complete (non-event control frame)",
			json:      `{"type":"snapshot_complete"}`,
			wantType:  TypeSnapshotComplete,
			eventBear: false,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			m, err := Decode([]byte(c.json))
			if err != nil {
				t.Fatalf("decode failed: %v", err)
			}
			if m.Type != c.wantType {
				t.Fatalf("type = %q, want %q", m.Type, c.wantType)
			}
			if m.IsEventBearing() != c.eventBear {
				t.Fatalf("IsEventBearing = %v, want %v", m.IsEventBearing(), c.eventBear)
			}
			if c.check != nil {
				c.check(t, m)
			}
		})
	}
}

// TestEncodeGoldenClientFrames pins the Go→Kotlin wire format for client messages.
// The discriminator is the fully-qualified Kotlin class name (kotlinx default for
// sealed classes without @SerialName), keyed under "type". These bytes must be
// accepted verbatim by the Kotlin server's ProtocolSerializer / Json decoder.
func TestEncodeGoldenClientFrames(t *testing.T) {
	cases := []struct {
		name     string
		frame    []byte
		wantType string
		check    func(t *testing.T, raw map[string]any)
	}{
		{
			// Hello is the first frame on every (re)connect — carries the client cwd so
			// the server can bind a workspace before any session starts.
			name:     "Hello carries workingDir",
			frame:    Hello("/home/kami/Projects/correx"),
			wantType: clientPrefix + "Hello",
			check: func(t *testing.T, raw map[string]any) {
				if raw["workingDir"] != "/home/kami/Projects/correx" {
					t.Fatalf("Hello workingDir = %v", raw["workingDir"])
				}
			},
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			var raw map[string]any
			if err := json.Unmarshal(c.frame, &raw); err != nil {
				t.Fatalf("unmarshal failed: %v", err)
			}
			if raw["type"] != c.wantType {
				t.Fatalf("type = %q, want %q", raw["type"], c.wantType)
			}
			if c.check != nil {
				c.check(t, raw)
			}
		})
	}
}
