package com.correx.apps.server.bridge

import com.correx.apps.server.protocol.AssessedIssueDto
import com.correx.apps.server.protocol.PauseReason
import com.correx.apps.server.protocol.ServerMessage
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ChatTurnEvent
import com.correx.core.events.events.ChatTurnRole
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolExecutionRejectedEvent
import com.correx.core.events.events.AssessedIssue
import com.correx.core.events.events.ToolCallAssessedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.ToolReceipt
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.risk.RiskAction
import com.correx.core.events.risk.RiskLevel
import com.correx.core.events.risk.RiskSignal
import com.correx.core.events.risk.RiskSummary
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.ProviderId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.events.types.TransitionId
import com.correx.core.events.types.ValidationReportId
import com.correx.core.inference.TokenUsage
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.core.LogEvent
import org.apache.logging.log4j.core.LoggerContext
import org.apache.logging.log4j.core.appender.AbstractAppender
import org.apache.logging.log4j.core.config.Property
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class DomainEventMapperTest {

    private val sessionId = SessionId("session-1")
    private val stageId = StageId("stage-1")
    private val workflowId = "workflow-test"
    private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
    private val occurredAt = timestamp.toEpochMilliseconds()
    private val noopStore: ArtifactStore = object : ArtifactStore {
        override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("art-1")
        override suspend fun get(id: ArtifactId): ByteArray? = null
        override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
    }

    private fun storedEvent(payload: com.correx.core.events.events.EventPayload): StoredEvent =
        StoredEvent(
            metadata = EventMetadata(
                eventId = EventId("evt-1"),
                sessionId = sessionId,
                timestamp = timestamp,
                schemaVersion = 1,
                causationId = null,
                correlationId = null,
            ),
            sequence = 1L,
            sessionSequence = 1L,
            payload = payload,
        )

    @Test
    fun `WorkflowStartedEvent maps to SessionAnnounced`(): Unit = runTest {
        val event =
            storedEvent(WorkflowStartedEvent(sessionId = sessionId, startStageId = stageId, workflowId = workflowId))
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.SessionAnnounced(sessionId, workflowId, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `ChatTurnEvent maps to ChatTurn`(): Unit = runTest {
        val event = storedEvent(
            ChatTurnEvent(
                sessionId = sessionId,
                turnId = "turn-1",
                role = ChatTurnRole.ROUTER,
                content = "hello",
                timestampMs = 1L,
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 3L)
        assertEquals(
            ServerMessage.ChatTurn(
                sessionId = sessionId,
                turnId = "turn-1",
                role = "ROUTER",
                content = "hello",
                sequence = event.sequence,
                sessionSequence = 3L,
            ),
            result,
        )
    }

    @Test
    fun `WorkflowCompletedEvent maps to SessionCompleted`(): Unit = runTest {
        val event = storedEvent(
            WorkflowCompletedEvent(
                sessionId = sessionId,
                terminalStageId = stageId,
                totalStages = 1,
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.SessionCompleted(sessionId, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `WorkflowFailedEvent maps to SessionFailed`(): Unit = runTest {
        val event = storedEvent(
            WorkflowFailedEvent(sessionId = sessionId, stageId = stageId, reason = "boom", retryExhausted = false),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.SessionFailed(sessionId, "boom", event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `TransitionExecutedEvent maps to StageStarted with to stageId`(): Unit = runTest {
        val from = StageId("from-stage")
        val to = StageId("to-stage")
        val event = storedEvent(
            TransitionExecutedEvent(sessionId = sessionId, from = from, to = to, transitionId = TransitionId("t1")),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.StageStarted(sessionId, to, occurredAt, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `StageCompletedEvent maps to StageCompleted`(): Unit = runTest {
        val event = storedEvent(
            StageCompletedEvent(sessionId = sessionId, stageId = stageId, transitionId = TransitionId("t1")),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.StageCompleted(sessionId, stageId, occurredAt, event.sequence, 0L),
            result,
        )
    }

   @Test
    fun `StageFailedEvent maps to StageFailed`(): Unit = runTest {
        val event = storedEvent(
            StageFailedEvent(
                sessionId = sessionId, stageId = stageId, transitionId = TransitionId("t1"), reason = "err",
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.StageFailed(sessionId, stageId, "err", occurredAt, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `OrchestrationPausedEvent with APPROVAL_PENDING maps to APPROVAL_PENDING`(): Unit = runTest {
        val event = storedEvent(
            OrchestrationPausedEvent(sessionId = sessionId, stageId = stageId, reason = "APPROVAL_PENDING"),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.SessionPaused(sessionId, PauseReason.APPROVAL_PENDING, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `OrchestrationPausedEvent with other reason maps to USER_REQUESTED`(): Unit = runTest {
        val event = storedEvent(
            OrchestrationPausedEvent(sessionId = sessionId, stageId = stageId, reason = "USER_REQUESTED"),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.SessionPaused(sessionId, PauseReason.USER_REQUESTED, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `InferenceStartedEvent maps to InferenceStarted`(): Unit = runTest {
        val event = storedEvent(
            InferenceStartedEvent(
                requestId = InferenceRequestId("req-1"),
                sessionId = sessionId,
                stageId = stageId,
                providerId = ProviderId("p1"),
                promptArtifactId = ArtifactId("art-prompt"),
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.InferenceStarted(sessionId, stageId, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `InferenceCompletedEvent reads artifact and maps to InferenceCompleted`(): Unit = runTest {
        val artifactId = ArtifactId("art-resp")
        val responseText = "hello world"
        val store = object : ArtifactStore {
            override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("x")
            override suspend fun get(id: ArtifactId): ByteArray? =
                if (id == artifactId) responseText.toByteArray(Charsets.UTF_8) else null

            override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
        }
        val event = storedEvent(
            InferenceCompletedEvent(
                requestId = InferenceRequestId("req-1"),
                sessionId = sessionId,
                stageId = stageId,
                providerId = ProviderId("p1"),
                tokensUsed = TokenUsage(10, 5),
                latencyMs = 100L,
                responseArtifactId = artifactId,
            ),
        )
        val result = domainEventToServerMessage(event, store, sessionSequence = 0L)
        assertEquals(
            ServerMessage.InferenceCompleted(
                sessionId, stageId, responseText, responseText, occurredAt = occurredAt,
                totalTokens = 15, sequence = event.sequence, sessionSequence = 0L,
            ),
            result,
        )
    }

    @Test
    fun `InferenceCompletedEvent falls back to empty string when artifact missing`(): Unit = runTest {
        val event = storedEvent(
            InferenceCompletedEvent(
                requestId = InferenceRequestId("req-1"),
                sessionId = sessionId,
                stageId = stageId,
                providerId = ProviderId("p1"),
                tokensUsed = TokenUsage(10, 5),
                latencyMs = 100L,
                responseArtifactId = ArtifactId("missing"),
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.InferenceCompleted(
                sessionId, stageId, "", "", occurredAt = occurredAt,
                totalTokens = 15, sequence = event.sequence, sessionSequence = 0L,
            ),
            result,
        )
    }

    @Test
    fun `InferenceTimeoutEvent maps to InferenceTimedOut`(): Unit = runTest {
        val event = storedEvent(
            InferenceTimeoutEvent(
                requestId = InferenceRequestId("req-1"),
                sessionId = sessionId,
                stageId = stageId,
                providerId = ProviderId("p1"),
                timeoutMs = 5000L,
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.InferenceTimedOut(sessionId, stageId, 5000L, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `ToolInvocationRequestedEvent maps to ToolStarted`(): Unit = runTest {
        val invId = ToolInvocationId("inv-1")
        val event = storedEvent(
            ToolInvocationRequestedEvent(
                invocationId = invId,
                sessionId = sessionId,
                stageId = stageId,
                toolName = "file_write",
                tier = Tier.T2,
                request = ToolRequest(
                    invocationId = invId, sessionId = sessionId, stageId = stageId, toolName = "file_write",
                ),
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.ToolStarted(
                sessionId, "file_write", Tier.T2, sequence = event.sequence, sessionSequence = 0L,
            ),
            result,
        )
    }

    @Test
    fun `ToolExecutionCompletedEvent maps to ToolCompleted`(): Unit = runTest {
        val invId = ToolInvocationId("inv-1")
        val receipt = ToolReceipt(
            invocationId = invId,
            toolName = "file_write",
            exitCode = 0,
            outputSummary = "wrote 3 lines",
            durationMs = 10L,
            tier = Tier.T2,
            timestamp = timestamp,
        )
        val event = storedEvent(
            ToolExecutionCompletedEvent(
                invocationId = invId, sessionId = sessionId, toolName = "file_write", receipt = receipt,
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.ToolCompleted(
                sessionId, "file_write", "wrote 3 lines", occurredAt,
                diff = null, affectedEntities = emptyList(), event.sequence, 0L,
            ),
            result,
        )
    }

    @Test
    fun `ToolExecutionFailedEvent maps to ToolFailed`(): Unit = runTest {
        val event = storedEvent(
            ToolExecutionFailedEvent(
                invocationId = ToolInvocationId("inv-1"),
                sessionId = sessionId,
                toolName = "file_write",
                reason = "disk full",
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.ToolFailed(sessionId, "file_write", "disk full", occurredAt, event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `ToolExecutionRejectedEvent maps to ToolRejected`(): Unit = runTest {
        val event = storedEvent(
            ToolExecutionRejectedEvent(
                invocationId = ToolInvocationId("inv-1"),
                sessionId = sessionId,
                toolName = "file_write",
                tier = Tier.T3,
                reason = "policy denied",
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.ToolRejected(sessionId, "file_write", "policy denied", event.sequence, 0L),
            result,
        )
    }

    @Test
    fun `ToolCallAssessedEvent maps to ToolAssessed`(): Unit = runTest {
        val event = storedEvent(
            ToolCallAssessedEvent(
                invocationId = ToolInvocationId("inv-1"),
                sessionId = sessionId,
                stageId = stageId,
                toolName = "file_write",
                issues = listOf(AssessedIssue(code = "PATH_ESCAPE", message = "escapes workspace", severity = "HIGH")),
                disposition = RiskAction.BLOCK,
                timestampMs = occurredAt,
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.ToolAssessed(
                sessionId = sessionId,
                stageId = stageId,
                toolName = "file_write",
                disposition = "BLOCK",
                issues = listOf(AssessedIssueDto("PATH_ESCAPE", "escapes workspace", "HIGH")),
                occurredAt = occurredAt,
                sequence = event.sequence,
                sessionSequence = 0L,
            ),
            result,
        )
    }

    @Test
    fun `ApprovalRequestedEvent maps to ApprovalRequired with null riskSummary falls back to tier name`(): Unit = runTest {
        val requestId = ApprovalRequestId("req-1")
        val event = storedEvent(
            ApprovalRequestedEvent(
                requestId = requestId,
                tier = Tier.T3,
                validationReportId = ValidationReportId("vr-1"),
                riskSummaryId = null,
                riskSummary = null,
                sessionId = sessionId,
                stageId = stageId,
                projectId = null,
            ),
        )
        val result =
            domainEventToServerMessage(event, noopStore, sessionSequence = 0L) as ServerMessage.ApprovalRequired
        assertEquals(requestId, result.requestId)
        assertEquals(Tier.T3, result.tier)
        assertNull(result.toolName)
        assertNull(result.preview)
        assertEquals(event.sequence, result.sequence)
        assertEquals("T3", result.riskSummary.level)
        assertEquals(emptyList<String>(), result.riskSummary.rationale)
    }

    @Test
    fun `ApprovalRequestedEvent maps to ApprovalRequired with riskSummary propagates rationale`(): Unit = runTest {
        val requestId = ApprovalRequestId("req-2")
        val riskSummary = RiskSummary(
            level = RiskLevel.HIGH,
            signals = listOf(
                RiskSignal.ValidationErrors(errorCount = 3),
                RiskSignal.RepeatedFailure(reason = "parse error", count = 2),
            ),
            recommendedAction = RiskAction.PROMPT_USER,
            rationale = listOf("[ERR001] Schema mismatch", "[ERR002] Missing required field"),
        )
        val event = storedEvent(
            ApprovalRequestedEvent(
                requestId = requestId,
                tier = Tier.T3,
                validationReportId = ValidationReportId("vr-2"),
                riskSummaryId = null,
                riskSummary = riskSummary,
                sessionId = sessionId,
                stageId = stageId,
                projectId = null,
            ),
        )
        val result =
            domainEventToServerMessage(event, noopStore, sessionSequence = 0L) as ServerMessage.ApprovalRequired
        assertEquals(requestId, result.requestId)
        assertEquals("HIGH", result.riskSummary.level)
        assertEquals("PROMPT_USER", result.riskSummary.recommendedAction)
        assertEquals(
            listOf("[ERR001] Schema mismatch", "[ERR002] Missing required field"),
            result.riskSummary.rationale,
        )
        assertEquals(2, result.riskSummary.factors.size)
        assertTrue(result.riskSummary.factors[0].contains("Validation errors"))
        assertTrue(result.riskSummary.factors[1].contains("Repeated failure"))
    }

    @Test
    fun `ApprovalDecisionResolvedEvent maps to ApprovalResolved with correct fields`(): Unit = runTest {
        val requestId = ApprovalRequestId("req-3")
        val event = storedEvent(
            ApprovalDecisionResolvedEvent(
                decisionId = ApprovalDecisionId("dec-1"),
                requestId = requestId,
                outcome = ApprovalOutcome.APPROVED,
                status = ApprovalStatus.COMPLETED,
                tier = Tier.T2,
                resolutionTimestamp = timestamp,
                reason = "user approved",
                userSteering = null,
            ),
        )
        val result =
            domainEventToServerMessage(event, noopStore, sessionSequence = 5L) as ServerMessage.ApprovalResolved
        assertEquals(sessionId, result.sessionId)
        assertEquals(requestId, result.requestId)
        assertEquals("APPROVED", result.outcome)
        assertEquals("user approved", result.reason)
        assertEquals(event.sequence, result.sequence)
        assertEquals(5L, result.sessionSequence)
    }

    @Test
    fun `ApprovalDecisionResolvedEvent maps to ApprovalResolved with null reason`(): Unit = runTest {
        val requestId = ApprovalRequestId("req-4")
        val event = storedEvent(
            ApprovalDecisionResolvedEvent(
                decisionId = ApprovalDecisionId("dec-2"),
                requestId = requestId,
                outcome = ApprovalOutcome.REJECTED,
                status = ApprovalStatus.COMPLETED,
                tier = Tier.T3,
                resolutionTimestamp = timestamp,
                reason = null,
                userSteering = null,
            ),
        )
        val result =
            domainEventToServerMessage(event, noopStore, sessionSequence = 0L) as ServerMessage.ApprovalResolved
        assertEquals("REJECTED", result.outcome)
        assertNull(result.reason)
    }

    @Test
    fun `ModelLoadedEvent maps to ModelChanged loaded=true`(): Unit = runTest {
        val event = storedEvent(
            com.correx.core.events.events.ModelLoadedEvent(
                modelId = "qwen-coder",
                providerId = ProviderId("llama-cpp:qwen-coder"),
                sessionId = sessionId,
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.ModelChanged("qwen-coder", "llama-cpp:qwen-coder", loaded = true),
            result,
        )
    }

    @Test
    fun `ModelUnloadedEvent maps to ModelChanged loaded=false`(): Unit = runTest {
        val event = storedEvent(
            com.correx.core.events.events.ModelUnloadedEvent(
                modelId = "qwen-coder",
                providerId = ProviderId("llama-cpp"),
                sessionId = sessionId,
                cancellationReason = null,
            ),
        )
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertEquals(
            ServerMessage.ModelChanged("qwen-coder", "llama-cpp", loaded = false),
            result,
        )
    }

    @Test
    fun `unmapped event returns null`(): Unit = runTest {
        val event = storedEvent(SteeringNoteAddedEvent(sessionId = sessionId, content = "test"))
        val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
        assertNull(result)
    }

    private class TestAppender : AbstractAppender("TestCapture", null, null, true, Property.EMPTY_ARRAY) {
        val events = mutableListOf<LogEvent>()
        override fun append(event: LogEvent) {
            events.add(event.toImmutable())
        }
    }

    @Test
    fun `unmapped event emits debug log`(): Unit = runTest {
        val ctx = LogManager.getContext(false) as LoggerContext
        val logger = ctx.getLogger("DomainEventMapper")
        val appender = TestAppender().also { it.start() }
        logger.addAppender(appender)
        val origLevel = logger.level
        logger.level = Level.DEBUG
        try {
            val event = storedEvent(SteeringNoteAddedEvent(sessionId = sessionId, content = "test"))
            val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
            assertNull(result)
            assertEquals(1, appender.events.size)
            assertEquals(Level.DEBUG, appender.events[0].level)
            assertTrue(appender.events[0].message.formattedMessage.contains("SteeringNoteAddedEvent"))
        } finally {
            logger.removeAppender(appender)
            appender.stop()
            logger.level = origLevel
        }
    }
}
