package com.correx.apps.server.replay

import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.NewEvent
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.events.StageCompletedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.utils.TypeId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class ReplayInspectionServiceTest {

    private val sessionId: SessionId = TypeId("test-session-1")
    private val stageId: StageId = TypeId("stage-1")
    private val toStageId: StageId = TypeId("stage-2")
    private val transitionId: TransitionId = TypeId("t-1")
    private val requestId: ApprovalRequestId = TypeId("req-1")
    private val decisionId: ApprovalDecisionId = TypeId("dec-1")

    private fun storedEvent(payload: EventPayload, seq: Long): StoredEvent =
        StoredEvent(
            metadata = EventMetadata(
                eventId = EventId("e$seq"),
                sessionId = sessionId,
                timestamp = Clock.System.now(),
                schemaVersion = 1,
                causationId = null,
                correlationId = null,
            ),
            sequence = seq,
            sessionSequence = seq,
            payload = payload,
        )

    private val seedEvents = listOf(
        storedEvent(WorkflowStartedEvent(sessionId = sessionId, workflowId = "wf-1", startStageId = stageId), 1L),
        storedEvent(StageCompletedEvent(sessionId = sessionId, stageId = stageId, transitionId = transitionId), 2L),
        storedEvent(ApprovalRequestedEvent(
            requestId = requestId,
            tier = Tier.T2,
            validationReportId = TypeId("vr-1"),
            riskSummaryId = null,
            sessionId = sessionId,
            stageId = stageId,
            projectId = null,
        ), 3L),
        storedEvent(ApprovalDecisionResolvedEvent(
            decisionId = decisionId,
            requestId = requestId,
            outcome = ApprovalOutcome.APPROVED,
            status = ApprovalStatus.COMPLETED,
            tier = Tier.T2,
            resolutionTimestamp = Instant.parse("2026-01-01T00:00:00Z"),
            reason = null,
        ), 4L),
        storedEvent(WorkflowCompletedEvent(sessionId = sessionId, terminalStageId = stageId, totalStages = 1), 5L),
        storedEvent(TransitionExecutedEvent(
            sessionId = sessionId,
            from = stageId,
            to = toStageId,
            transitionId = transitionId,
        ), 6L),
    )

    private class SeededEventStore(private val events: List<StoredEvent>) : 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> =
            events.filter { it.metadata.sessionId == sessionId }
        override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> =
            events.filter { it.metadata.sessionId == sessionId && it.sequence >= fromSequence }
        override fun lastSequence(sessionId: SessionId): Long? =
            events.filter { it.metadata.sessionId == sessionId }.lastOrNull()?.sequence
        override fun subscribe(sessionId: SessionId): Flow<StoredEvent> = emptyFlow()
        override fun subscribeAll(): Flow<StoredEvent> = emptyFlow()
        override suspend fun lastGlobalSequence(): Long = 0L
        override fun allEvents(): Sequence<StoredEvent> = emptySequence()
        override fun allSessionIds(): Set<SessionId> = events.map { it.metadata.sessionId }.toSet()
    }

    @Test
    fun `inspect returns correct event count and timeline`() {
        val service = ReplayInspectionService(SeededEventStore(seedEvents))
        val report = service.inspect(sessionId)

        assertEquals(sessionId.value, report.sessionId)
        assertEquals(6L, report.eventCount)
        assertEquals(6, report.timeline.size)
    }

    @Test
    fun `timeline type uses SerialName discriminator not simpleName`() {
        val service = ReplayInspectionService(SeededEventStore(seedEvents))
        val report = service.inspect(sessionId)

        assertEquals("WorkflowStarted", report.timeline[0].type)
        assertEquals("StageCompleted", report.timeline[1].type)
        assertEquals("WorkflowCompleted", report.timeline[4].type)
    }

    @Test
    fun `timeline entries carry stageId where applicable`() {
        val service = ReplayInspectionService(SeededEventStore(seedEvents))
        val report = service.inspect(sessionId)

        val workflowEntry = report.timeline[0]
        assertEquals(stageId.value, workflowEntry.stageId)

        val stageEntry = report.timeline[1]
        assertEquals(stageId.value, stageEntry.stageId)

        val approvalEntry = report.timeline[2]
        assertEquals(stageId.value, approvalEntry.stageId)

        val decisionEntry = report.timeline[3]
        assertEquals(null, decisionEntry.stageId)

        val completedEntry = report.timeline[4]
        assertEquals(stageId.value, completedEntry.stageId)
    }

    @Test
    fun `TransitionExecutedEvent summary includes from and to stages`() {
        val service = ReplayInspectionService(SeededEventStore(seedEvents))
        val report = service.inspect(sessionId)

        val transitionEntry = report.timeline[5]
        assertEquals("TransitionExecuted", transitionEntry.type)
        assertTrue(transitionEntry.summary.contains(stageId.value), "summary should mention from-stage")
        assertTrue(transitionEntry.summary.contains(toStageId.value), "summary should mention to-stage")
        assertFalse(transitionEntry.summary == "TransitionExecuted", "summary should not be the bare catch-all type")
    }

    @Test
    fun `digest is 64 hex characters`() {
        val service = ReplayInspectionService(SeededEventStore(seedEvents))
        val report = service.inspect(sessionId)

        assertEquals(64, report.digest.length)
        assertTrue(report.digest.matches(Regex("[0-9a-f]+")))
    }

    @Test
    fun `digest is stable across two inspect calls`() {
        val service = ReplayInspectionService(SeededEventStore(seedEvents))
        val report1 = service.inspect(sessionId)
        val report2 = service.inspect(sessionId)

        assertEquals(report1.digest, report2.digest)
    }

    @Test
    fun `deterministic is true for stable store`() {
        val service = ReplayInspectionService(SeededEventStore(seedEvents))
        val report = service.inspect(sessionId)

        assertTrue(report.deterministic)
    }

    @Test
    fun `deterministic is false when store returns different events per read`() {
        val unstableStore = object : EventStore by SeededEventStore(seedEvents) {
            private var reads = 0
            override fun read(sessionId: SessionId): List<StoredEvent> {
                reads += 1
                return if (reads % 2 == 0) seedEvents.dropLast(1) else seedEvents
            }
        }
        val service = ReplayInspectionService(unstableStore)
        val report = service.inspect(sessionId)

        assertFalse(report.deterministic)
    }
}
