package com.correx.apps.server.bridge

import com.correx.apps.server.protocol.EventEntryDto
import com.correx.apps.server.protocol.ServerMessage
import com.correx.apps.server.registry.WorkflowRegistry
import com.correx.core.approvals.Tier
import com.correx.core.artifactstore.ArtifactStore
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.TalkieNarrationEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolReceipt
import com.correx.core.events.events.ClarificationAnsweredEvent
import com.correx.core.events.events.ClarificationAnswer
import com.correx.core.events.events.ClarificationQuestion
import com.correx.core.events.events.ClarificationRequestedEvent
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.orchestration.OrchestrationStatus
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ClarificationRequestId
import com.correx.core.inference.TokenUsage
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.ToolInvocationId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.kernel.orchestration.OrchestrationRepository
import com.correx.core.tools.contract.Tool
import com.correx.core.tools.registry.ToolRegistry
import com.correx.core.sessions.projections.replay.EventReplayer
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class SessionEventBridgeTest {

    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 noopArtifactStore: 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: EventPayload, seq: Long = 1L): StoredEvent =
        StoredEvent(
            metadata = EventMetadata(
                eventId = EventId("evt-$seq"),
                sessionId = sessionId,
                timestamp = timestamp,
                schemaVersion = 1,
                causationId = null,
                correlationId = null,
            ),
            sequence = seq,
            sessionSequence = seq,
            payload = payload,
        )

    private fun fakeEventStore(
        allEventsList: List<StoredEvent> = emptyList(),
        liveFlow: Flow<StoredEvent> = flow {},
    ): EventStore = object : EventStore {
        override suspend fun append(event: NewEvent): StoredEvent = error("unused")
        override suspend fun appendAll(events: List<NewEvent>): List<StoredEvent> = error("unused")
        override fun read(sessionId: SessionId): List<StoredEvent> = allEventsList
        override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> = allEventsList
        override fun lastSequence(sessionId: SessionId): Long =
            allEventsList.maxOfOrNull { it.sequence } ?: 0L

        override fun subscribe(sessionId: SessionId): Flow<StoredEvent> = liveFlow
        override fun allEvents(): Sequence<StoredEvent> = allEventsList.asSequence()
        override fun allSessionIds(): Set<SessionId> = allEventsList.map { it.metadata.sessionId }.toSet()
        override fun subscribeAll(): Flow<StoredEvent> = TODO("Not needed in this test context")
        override suspend fun lastGlobalSequence(): Long =
            allEventsList.maxOfOrNull { it.sequence } ?: 0L
    }

    private fun activeOrchestrationRepository(
        status: OrchestrationStatus = OrchestrationStatus.RUNNING,
    ): OrchestrationRepository = OrchestrationRepository(
        object : EventReplayer<OrchestrationState> {
            override fun rebuild(sessionId: SessionId): OrchestrationState = OrchestrationState(
                workflowId = workflowId,
                status = status,
                currentStageId = stageId,
                pendingApproval = false,
            )
        },
    )

    private fun clarificationPausedRepository(): OrchestrationRepository = OrchestrationRepository(
        object : EventReplayer<OrchestrationState> {
            override fun rebuild(sessionId: SessionId): OrchestrationState = OrchestrationState(
                workflowId = workflowId,
                status = OrchestrationStatus.PAUSED,
                currentStageId = stageId,
                // The reducer sets pendingApproval=true for every pause; the reason discriminates.
                pendingApproval = true,
                pauseReason = "CLARIFICATION_PENDING",
            )
        },
    )

    private val clarRequestId = ClarificationRequestId("req-1")

    // A stage parking on clarification records the pause then the request (see
    // SessionOrchestrator.requestClarificationIfNeeded) — the pause event is what stops the
    // stuck-session fixer from mistaking the clarification pause for a resolved approval.
    private fun clarParkEvents(answered: Boolean = false): List<StoredEvent> = buildList {
        add(storedEvent(OrchestrationPausedEvent(sessionId, stageId, "CLARIFICATION_PENDING"), seq = 1L))
        add(
            storedEvent(
                ClarificationRequestedEvent(
                    requestId = clarRequestId,
                    sessionId = sessionId,
                    stageId = stageId,
                    questions = listOf(ClarificationQuestion(id = "stack", prompt = "Which stack?")),
                ),
                seq = 2L,
            ),
        )
        if (answered) {
            add(
                storedEvent(
                    ClarificationAnsweredEvent(
                        requestId = clarRequestId,
                        sessionId = sessionId,
                        stageId = stageId,
                        answers = listOf(ClarificationAnswer(questionId = "stack", value = "React")),
                    ),
                    seq = 3L,
                ),
            )
        }
    }

    @Test
    fun `replaySnapshot re-pushes a pending clarification when the deferred is live`() = runTest {
        val store = fakeEventStore(allEventsList = clarParkEvents())
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store, noopArtifactStore, clarificationPausedRepository(), noopWorkflowRegistry, noopToolRegistry,
            livePendingClarificationIds = { setOf(clarRequestId.value) },
        ) { sent.add(it) }

        bridge.replaySnapshot()

        val clar = sent.filterIsInstance<ServerMessage.ClarificationRequired>()
        assertEquals(1, clar.size)
        assertEquals(clarRequestId, clar[0].requestId)
        assertEquals(stageId, clar[0].stageId)
    }

    @Test
    fun `replaySnapshot does not re-push a clarification whose deferred died with a restart`() = runTest {
        val store = fakeEventStore(allEventsList = clarParkEvents())
        val sent = mutableListOf<ServerMessage>()
        // Empty live set = no in-process deferred (bare restart) — showing the modal would trap the
        // operator since the answer could not be delivered.
        val bridge = SessionEventBridge(
            store, noopArtifactStore, clarificationPausedRepository(), noopWorkflowRegistry, noopToolRegistry,
            livePendingClarificationIds = { emptySet() },
        ) { sent.add(it) }

        bridge.replaySnapshot()

        assertEquals(0, sent.filterIsInstance<ServerMessage.ClarificationRequired>().size)
    }

    @Test
    fun `replaySnapshot does not re-push an already-answered clarification`() = runTest {
        val store = fakeEventStore(allEventsList = clarParkEvents(answered = true))
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store, noopArtifactStore, clarificationPausedRepository(), noopWorkflowRegistry, noopToolRegistry,
            livePendingClarificationIds = { setOf(clarRequestId.value) },
        ) { sent.add(it) }

        bridge.replaySnapshot()

        assertEquals(0, sent.filterIsInstance<ServerMessage.ClarificationRequired>().size)
    }

    private val noopWorkflowRegistry = object : WorkflowRegistry {
        override fun listAll(): List<com.correx.apps.server.registry.WorkflowSummary> = emptyList()
        override fun find(workflowId: String): com.correx.core.transitions.graph.WorkflowGraph? = null
    }

    private val noopToolRegistry = object : ToolRegistry {
        override fun resolve(name: String): Tool? = null
        override fun all(): List<Tool> = emptyList()
    }

    @Test
    fun `replaySnapshot sends mapped messages for all events`() = runTest {
        val events = listOf(
            storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
            storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
        )
        val store = fakeEventStore(allEventsList = events)
        val orchRepo = activeOrchestrationRepository()
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store,
            noopArtifactStore,
            orchRepo,
            noopWorkflowRegistry,
            noopToolRegistry,
        ) { sent.add(it) }

        bridge.replaySnapshot()

        assertEquals(2, sent.size)
        val snapshot = sent[0] as ServerMessage.SessionSnapshot
        assertEquals(sessionId, snapshot.sessionId)
        assertEquals(2L, snapshot.lastSequence)
        assertEquals(2L, snapshot.lastSessionSequence)
        assertEquals(ServerMessage.SnapshotComplete, sent[1])
    }

    @Test
    fun `replaySnapshot reconstructs the OUTPUT transcript with reasoning and a write row`() = runTest {
        val reasoningId = ArtifactId("reasoning-1")
        val artifactStore = object : ArtifactStore {
            override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("art-1")
            override suspend fun get(id: ArtifactId): ByteArray? =
                if (id == reasoningId) "thinking hard".toByteArray() else null
            override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
        }
        val diff = "--- a/app.css\n+++ b/app.css\n+body {}\n"
        val receipt = ToolReceipt(
            invocationId = ToolInvocationId("inv-1"),
            toolName = "file_write",
            exitCode = 0,
            outputSummary = "wrote 1 line",
            durationMs = 5L,
            tier = Tier.T2,
            timestamp = timestamp,
            diff = diff,
        )
        val events = listOf(
            storedEvent(ChatTurnEvent(sessionId, "t1", ChatTurnRole.USER, "hi", timestampMs = 0L), seq = 1L),
            storedEvent(
                TalkieNarrationEvent(sessionId, "n1", "stage", stageId.value, "narrating", timestampMs = 0L),
                seq = 2L,
            ),
            storedEvent(
                InferenceCompletedEvent(
                    requestId = InferenceRequestId("r1"),
                    sessionId = sessionId,
                    stageId = stageId,
                    providerId = ProviderId("p"),
                    tokensUsed = TokenUsage(1, 1),
                    latencyMs = 3L,
                    responseArtifactId = ArtifactId("resp-1"),
                    reasoningArtifactId = reasoningId,
                ),
                seq = 3L,
            ),
            storedEvent(
                ToolExecutionCompletedEvent(ToolInvocationId("inv-1"), sessionId, "file_write", receipt),
                seq = 4L,
            ),
        )
        val store = fakeEventStore(allEventsList = events)
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store, artifactStore, activeOrchestrationRepository(),
            noopWorkflowRegistry, noopToolRegistry,
        ) { sent.add(it) }

        bridge.replaySnapshot()

        val transcript = (sent[0] as ServerMessage.SessionSnapshot).transcript
        assertEquals(listOf("chat", "narration", "thinking", "tool"), transcript.map { it.kind })
        assertEquals("USER", transcript[0].role)
        assertEquals("thinking hard", transcript[2].content)
        assertEquals(diff, transcript[3].diff)
        assertEquals("wrote 1 line", transcript[3].summary)
    }

    @Test
    fun `replaySnapshot populates recentEvents from event store`() = runTest {
        val events = listOf(
            storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
            storedEvent(TransitionExecutedEvent(sessionId, stageId, StageId("stage-2"), TransitionId("t1")), seq = 2L),
            storedEvent(StageCompletedEvent(sessionId, stageId, TransitionId("t2")), seq = 3L),
            storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 4L),
        )
        val store = fakeEventStore(allEventsList = events)
        val orchRepo = activeOrchestrationRepository(status = OrchestrationStatus.COMPLETED)
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store,
            noopArtifactStore,
            orchRepo,
            noopWorkflowRegistry,
            noopToolRegistry,
        ) { sent.add(it) }

        bridge.replaySnapshot()

        assertEquals(2, sent.size)
        val snapshot = sent[0] as ServerMessage.SessionSnapshot
        // WorkflowStartedEvent is filtered out by eventToEntry (maps to null)
        assertEquals(3, snapshot.recentEvents.size)
        assertEquals(
            EventEntryDto(timestamp.toEpochMilliseconds(), "StageStarted", "stage-2"),
            snapshot.recentEvents[0],
        )
        assertEquals(
            EventEntryDto(timestamp.toEpochMilliseconds(), "StageCompleted", stageId.value),
            snapshot.recentEvents[1],
        )
        assertEquals(EventEntryDto(timestamp.toEpochMilliseconds(), "SessionCompleted", ""), snapshot.recentEvents[2])
    }

    @Test
    fun `replaySnapshot skips unmapped events`() = runTest {
        val events = listOf(
            storedEvent(OrchestrationResumedEvent(sessionId, stageId), seq = 1L),
            storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
        )
        val store = fakeEventStore(allEventsList = events)
        val orchRepo = activeOrchestrationRepository()
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store,
            noopArtifactStore,
            orchRepo,
            noopWorkflowRegistry,
            noopToolRegistry,
        ) { sent.add(it) }

        bridge.replaySnapshot()

        assertEquals(2, sent.size)
        val snapshot = sent[0] as ServerMessage.SessionSnapshot
        assertEquals(sessionId, snapshot.sessionId)
        assertEquals(ServerMessage.SnapshotComplete, sent[1])
    }

    @Test
    fun `replaySnapshot with no sessions emits only SnapshotComplete`() = runTest {
        val store = fakeEventStore(allEventsList = emptyList())
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store,
            noopArtifactStore,
            activeOrchestrationRepository(),
            noopWorkflowRegistry,
            noopToolRegistry,
        ) { sent.add(it) }
        bridge.replaySnapshot()
        assertEquals(1, sent.size)
        assertEquals(ServerMessage.SnapshotComplete, sent[0])
    }

    @Test
    fun `replaySnapshot emits SnapshotComplete as final message`() = runTest {
        val events = listOf(storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L))
        val store = fakeEventStore(allEventsList = events)
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store,
            noopArtifactStore,
            activeOrchestrationRepository(),
            noopWorkflowRegistry,
            noopToolRegistry,
        ) { sent.add(it) }
        bridge.replaySnapshot()
        assertEquals(ServerMessage.SnapshotComplete, sent.last())
    }

    @Test
    fun `replaySnapshot lastSequence reflects global cursor before projection read`() = runTest {
        val events = listOf(
            storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
            storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
        )
        val store = object : EventStore by fakeEventStore(allEventsList = events) {
            override suspend fun lastGlobalSequence(): Long = 5L
        }
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store,
            noopArtifactStore,
            activeOrchestrationRepository(),
            noopWorkflowRegistry,
            noopToolRegistry,
        ) { sent.add(it) }
        bridge.replaySnapshot()
        val snapshot = sent[0] as ServerMessage.SessionSnapshot
        assertEquals(5L, snapshot.lastSequence)
    }

    @Test
    fun `replaySnapshot lastSessionSequence equals per-session cursor`() = runTest {
        val events = listOf(
            storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
            storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 3L),
        )
        val store = fakeEventStore(allEventsList = events)
        val sent = mutableListOf<ServerMessage>()
        val bridge = SessionEventBridge(
            store,
            noopArtifactStore,
            activeOrchestrationRepository(),
            noopWorkflowRegistry,
            noopToolRegistry,
        ) { sent.add(it) }
        bridge.replaySnapshot()
        val snapshot = sent[0] as ServerMessage.SessionSnapshot
        assertEquals(3L, snapshot.lastSessionSequence)
    }

}
