package com.correx.apps.server.protocol

import com.correx.apps.server.health.HealthReport
import com.correx.apps.server.health.HealthSubjectReport
import com.correx.apps.server.metrics.FailureMetrics
import com.correx.apps.server.metrics.MetricsReport
import com.correx.apps.server.metrics.ProviderStatsDto
import com.correx.apps.server.metrics.TierApprovalStatsDto
import com.correx.apps.server.metrics.ToolStatsDto
import com.correx.core.events.types.SessionId
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class ServerMessageSerializationTest {

    private val json = Json {
        classDiscriminator = "type"
        ignoreUnknownKeys = true
    }

    @Test
    fun `SessionAnnounced encodes sequence and sessionSequence`() {
        val msg = ServerMessage.SessionAnnounced(
            sessionId = SessionId("sess-1"),
            workflowId = "wf-1",
            sequence = 42,
            sessionSequence = 7,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"session.announced\"")) { "expected type=session.announced" }
        assert(jsonStr.contains("\"sessionId\":\"sess-1\"")) { "expected sessionId" }
        assert(jsonStr.contains("\"workflowId\":\"wf-1\"")) { "expected workflowId" }
        assert(jsonStr.contains("\"sequence\":42")) { "expected sequence=42" }
        assert(jsonStr.contains("\"sessionSequence\":7")) { "expected sessionSequence=7" }
    }

    @Test
    fun `ChatTurn encodes role content and cursors`() {
        val msg = ServerMessage.ChatTurn(
            sessionId = SessionId("sess-1"),
            turnId = "turn-9",
            role = "ROUTER",
            content = "hi there",
            sequence = 5,
            sessionSequence = 2,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"chat.turn\"")) { "expected type=chat.turn" }
        assert(jsonStr.contains("\"turnId\":\"turn-9\"")) { "expected turnId" }
        assert(jsonStr.contains("\"role\":\"ROUTER\"")) { "expected role" }
        assert(jsonStr.contains("\"content\":\"hi there\"")) { "expected content" }
    }

    @Test
    fun `ChatTurn with metrics encodes latencyMs and totalTokens`() {
        val msg = ServerMessage.ChatTurn(
            sessionId = SessionId("sess-1"),
            turnId = "t9",
            role = "ROUTER",
            content = "hi",
            latencyMs = 3000,
            totalTokens = 943,
            sequence = 7,
            sessionSequence = 2,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"chat.turn\"")) { "expected type=chat.turn" }
        assert(jsonStr.contains("\"latencyMs\":3000")) { "expected latencyMs=3000" }
        assert(jsonStr.contains("\"totalTokens\":943")) { "expected totalTokens=943" }
    }

    @Test
    fun `ChatTurn without metrics encodes latencyMs and totalTokens as absent or null`() {
        val msg = ServerMessage.ChatTurn(
            sessionId = SessionId("sess-1"),
            turnId = "t10",
            role = "USER",
            content = "hello",
            sequence = 8,
            sessionSequence = 3,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"chat.turn\"")) { "expected type=chat.turn" }
        assert(!jsonStr.contains("\"latencyMs\":3000")) { "latencyMs should not be 3000" }
        assert(!jsonStr.contains("\"totalTokens\":943")) { "totalTokens should not be 943" }
    }

    @Test
    fun `SnapshotComplete encodes with only type field`() {
        val msg = ServerMessage.SnapshotComplete
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"snapshot_complete\"")) { "expected type=snapshot_complete" }
    }

    @Test
    fun `SessionSnapshot encodes lastSequence and lastSessionSequence`() {
        val msg = ServerMessage.SessionSnapshot(
            sessionId = SessionId("sess-1"),
            workflowId = "wf-abc",
            state = SessionStateDto(
                status = "running",
                currentStageId = "stage-1",
                pauseReason = null,
            ),
            pendingApprovals = emptyList(),
            lastSequence = 100,
            lastSessionSequence = 15,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"session_snapshot\"")) { "expected type=session_snapshot" }
        assert(jsonStr.contains("\"sessionId\":\"sess-1\"")) { "expected sessionId" }
        assert(jsonStr.contains("\"lastSequence\":100")) { "expected lastSequence=100" }
        assert(jsonStr.contains("\"lastSessionSequence\":15")) { "expected lastSessionSequence=15" }
    }

    @Test
    fun `Narration encodes type content stageId latencyMs and totalTokens`() {
        val msg = ServerMessage.Narration(
            sessionId = SessionId("sess-1"),
            content = "moving on",
            stageId = "validate",
            latencyMs = 2000,
            totalTokens = 120,
            sequence = 10,
            sessionSequence = 5,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"router.narration\"")) { "expected type=router.narration" }
        assert(jsonStr.contains("\"content\":\"moving on\"")) { "expected content" }
        assert(jsonStr.contains("\"latencyMs\":2000")) { "expected latencyMs=2000" }
        assert(jsonStr.contains("\"totalTokens\":120")) { "expected totalTokens=120" }
    }

    @Test
    fun `ConfigSnapshot encodes fields, restartRequired, and error`() {
        val msg = ServerMessage.ConfigSnapshot(
            fields = listOf(
                ConfigFieldDto("tui.theme", "ENUM", "dark", options = listOf("dark", "light"), restartRequired = false),
                ConfigFieldDto("server.port", "INT", "8080", restartRequired = true),
            ),
            restartRequired = listOf("server.port"),
            error = null,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"config.snapshot\"")) { "expected type=config.snapshot" }
        assert(jsonStr.contains("\"key\":\"tui.theme\"")) { "expected tui.theme field" }
        assert(jsonStr.contains("\"restartRequired\":[\"server.port\"]")) { "expected restartRequired list" }

        val decoded = json.decodeFromString<ServerMessage.ConfigSnapshot>(jsonStr)
        assertEquals(2, decoded.fields.size)
        assertEquals("8080", decoded.fields[1].value)
        assertEquals(listOf("server.port"), decoded.restartRequired)
    }

    @Test
    fun `SessionStats encodes type, sessionId and nested metrics`() {
        val msg = ServerMessage.SessionStats(
            sessionId = SessionId("sess-stats"),
            stats = MetricsReport(
                sessionId = "sess-stats",
                eventCount = 12,
                sessionDurationMs = 60_000,
                inferenceCount = 3,
                inferenceMs = 6000,
                promptTokens = 310,
                completionTokens = 170,
                tokensPerSecond = 28.3,
                perProvider = listOf(ProviderStatsDto("lfm:a", 2, 5000, 300, 150, 30.0)),
                toolCount = 3,
                toolMs = 1500,
                perTool = listOf(ToolStatsDto("shell.read", 2, 1200)),
                approvalsRequested = 2,
                approvalsResolved = 1,
                approvalsPending = 1,
                approvalWaitMs = 30_000,
                avgApprovalWaitMs = 30_000,
                perTier = listOf(TierApprovalStatsDto("T2", 2, 1, 30_000, 30_000)),
                failures = FailureMetrics(inferenceFailures = 1, toolFailures = 1, workflowFailures = 1),
                inferencePct = 10.0,
                toolPct = 2.5,
                approvalWaitPct = 50.0,
            ),
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"session.stats\"")) { "expected type=session.stats" }
        assert(jsonStr.contains("\"sessionId\":\"sess-stats\"")) { "expected sessionId" }
        assert(jsonStr.contains("\"completionTokens\":170")) { "expected nested metric field" }
        assert(jsonStr.contains("\"provider\":\"lfm:a\"")) { "expected per-provider row" }

        val decoded = json.decodeFromString<ServerMessage.SessionStats>(jsonStr)
        assertEquals("sess-stats", decoded.stats.sessionId)
        assertEquals(3, decoded.stats.inferenceCount)
        assertEquals(1, decoded.stats.perProvider.size)
        assertEquals(50.0, decoded.stats.approvalWaitPct)
    }

    @Test
    fun `HealthChecks encodes type and nested health report`() {
        val msg = ServerMessage.HealthChecks(
            health = HealthReport(
                overall = "DEGRADED",
                subjects = listOf(
                    HealthSubjectReport(
                        subject = "DISK",
                        status = "DEGRADED",
                        metric = "free_bytes",
                        observedValue = 512_000_000L,
                        detail = "disk free below 1 GB threshold",
                        since = "2026-06-14T10:00:00Z",
                    ),
                    HealthSubjectReport(
                        subject = "EVENT_STORE",
                        status = "HEALTHY",
                        metric = "write_latency_ms",
                        observedValue = 4L,
                        detail = "ok",
                        since = "2026-06-14T09:00:00Z",
                    ),
                ),
                checkedAt = "2026-06-14T10:05:00Z",
            ),
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"health.checks\"")) { "expected type=health.checks" }
        assert(jsonStr.contains("\"overall\":\"DEGRADED\"")) { "expected overall=DEGRADED" }
        assert(jsonStr.contains("\"subject\":\"DISK\"")) { "expected DISK subject" }
        assert(jsonStr.contains("\"status\":\"DEGRADED\"")) { "expected DEGRADED status" }
        assert(jsonStr.contains("\"checkedAt\":\"2026-06-14T10:05:00Z\"")) { "expected checkedAt" }
        // Pin every subject field name so a rename breaks the Go↔Kotlin wire contract here too.
        assert(jsonStr.contains("\"metric\":\"free_bytes\"")) { "expected metric field name" }
        assert(jsonStr.contains("\"observedValue\":512000000")) { "expected observedValue field name" }
        assert(jsonStr.contains("\"detail\":\"disk free below 1 GB threshold\"")) { "expected detail field name" }
        assert(jsonStr.contains("\"since\":\"2026-06-14T10:00:00Z\"")) { "expected since field name" }

        val decoded = json.decodeFromString<ServerMessage.HealthChecks>(jsonStr)
        assertEquals("DEGRADED", decoded.health.overall)
        assertEquals(2, decoded.health.subjects.size)
        assertEquals("DISK", decoded.health.subjects[0].subject)
        assertEquals(512_000_000L, decoded.health.subjects[0].observedValue)
        assertEquals("EVENT_STORE", decoded.health.subjects[1].subject)
        assertEquals("HEALTHY", decoded.health.subjects[1].status)
    }

    @Test
    fun `ClarificationRequired encodes type, stageId and structured questions`() {
        val msg = ServerMessage.ClarificationRequired(
            sessionId = SessionId("sess-c"),
            requestId = SessionId("req-1"),
            stageId = SessionId("analyst"),
            questions = listOf(
                com.correx.core.events.events.ClarificationQuestion(
                    id = "stack",
                    prompt = "Which stack?",
                    options = listOf("React", "Vue"),
                    header = "Stack",
                ),
            ),
            sequence = 9,
            sessionSequence = 4,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"clarification.required\"")) { "expected type=clarification.required" }
        assert(jsonStr.contains("\"prompt\":\"Which stack?\"")) { "expected question prompt" }
        assert(jsonStr.contains("\"React\"")) { "expected options" }
    }

    @Test
    fun `WorkflowProposed encodes type, candidates and original request`() {
        val msg = ServerMessage.WorkflowProposed(
            sessionId = SessionId("sess-w"),
            proposalId = "prop-1",
            prompt = "Run one of these?",
            candidates = listOf(
                com.correx.core.events.events.ProposedWorkflow(workflowId = "research", reason = "gather + report"),
                com.correx.core.events.events.ProposedWorkflow(workflowId = "role_pipeline"),
            ),
            originalRequest = "find papers on event sourcing",
            sequence = 9,
            sessionSequence = 4,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"workflow.proposed\"")) { "expected type=workflow.proposed" }
        assert(jsonStr.contains("\"workflowId\":\"research\"")) { "expected candidate id" }
        assert(
            jsonStr.contains("\"originalRequest\":\"find papers on event sourcing\""),
        ) { "expected original request" }

        val decoded = json.decodeFromString<ServerMessage.WorkflowProposed>(jsonStr)
        assertEquals(2, decoded.candidates.size)
        assertEquals("research", decoded.candidates.first().workflowId)
        assertEquals("gather + report", decoded.candidates.first().reason)
        assertEquals("", decoded.candidates[1].reason)
    }

    @Test
    fun `IdeaList encodes type and ideas`() {
        val msg = ServerMessage.IdeaList(
            ideas = listOf(
                IdeaDto(ideaId = "i1", text = "cache the repo map", capturedAtMs = 1000),
                IdeaDto(ideaId = "i2", text = "add --dry-run", capturedAtMs = 2000),
            ),
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"idea.list\"")) { "expected type=idea.list" }

        val decoded = json.decodeFromString<ServerMessage.IdeaList>(jsonStr)
        assertEquals(2, decoded.ideas.size)
        assertEquals("cache the repo map", decoded.ideas.first().text)
        assertEquals(2000, decoded.ideas[1].capturedAtMs)
    }

    @Test
    fun `DiscardIdea decodes from the client wire format`() {
        val wire = """{"type":"com.correx.apps.server.protocol.ClientMessage.DiscardIdea","ideaId":"i1"}"""
        val decoded = ProtocolSerializer.decodeClientMessage(wire)
        assert(decoded is ClientMessage.DiscardIdea) { "expected DiscardIdea, got $decoded" }
        assertEquals("i1", (decoded as ClientMessage.DiscardIdea).ideaId)
    }

    @Test
    fun `PromoteIdea decodes from the client wire format`() {
        val wire = """{"type":"com.correx.apps.server.protocol.ClientMessage.PromoteIdea","ideaId":"i1"}"""
        val decoded = ProtocolSerializer.decodeClientMessage(wire)
        assert(decoded is ClientMessage.PromoteIdea) { "expected PromoteIdea, got $decoded" }
        assertEquals("i1", (decoded as ClientMessage.PromoteIdea).ideaId)
    }

    @Test
    fun `FileList encodes type file_list and paths`() {
        val msg = ServerMessage.FileList(sessionId = SessionId("s1"), paths = listOf("README.md", "src/Main.kt"))
        val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
        assert(jsonStr.contains("\"type\":\"file.list\"")) { "expected type=file.list" }

        val decoded = json.decodeFromString<ServerMessage.FileList>(jsonStr)
        assertEquals(listOf("README.md", "src/Main.kt"), decoded.paths)
    }

    @Test
    fun `ListFiles decodes from the client wire format`() {
        val wire = """{"type":"com.correx.apps.server.protocol.ClientMessage.ListFiles","sessionId":"s1"}"""
        val decoded = ProtocolSerializer.decodeClientMessage(wire)
        assert(decoded is ClientMessage.ListFiles) { "expected ListFiles, got $decoded" }
        assertEquals("s1", (decoded as ClientMessage.ListFiles).sessionId.value)
    }

    @Test
    fun `ClarificationResponse decodes from the client wire format`() {
        val wire = """
            {"type":"com.correx.apps.server.protocol.ClientMessage.ClarificationResponse",
             "sessionId":"sess-c","stageId":"analyst","requestId":"req-1",
             "answers":[{"questionId":"stack","value":"React"}]}
        """.trimIndent()
        val decoded = ProtocolSerializer.decodeClientMessage(wire)
        assert(decoded is ClientMessage.ClarificationResponse) { "expected ClarificationResponse, got $decoded" }
        val response = decoded as ClientMessage.ClarificationResponse
        assertEquals("analyst", response.stageId.value)
        assertEquals(1, response.answers.size)
        assertEquals("stack", response.answers.single().questionId)
        assertEquals("React", response.answers.single().value)
    }

    @Test
    fun `SessionAnnounced round-trips through JSON`() {
        val original = ServerMessage.SessionAnnounced(
            sessionId = SessionId("sess-42"),
            workflowId = "wf-abc",
            sequence = 200,
            sessionSequence = 33,
        )
        val jsonStr = ProtocolSerializer.encodeServerMessage(original)
        val decoded = json.decodeFromString<ServerMessage.SessionAnnounced>(jsonStr)
        assertEquals(original.sessionId, decoded.sessionId)
        assertEquals(original.workflowId, decoded.workflowId)
        assertEquals(200L, decoded.sequence)
        assertEquals(33L, decoded.sessionSequence)
    }
}
