package com.correx.apps.server.routes

import com.correx.apps.server.ServerModule
import com.correx.apps.server.configureServer
import com.correx.apps.server.registry.ProviderRegistry
import com.correx.apps.server.registry.WorkflowRegistry
import com.correx.apps.server.registry.WorkflowSummary
import com.correx.apps.server.undo.SessionUndoService
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.context.builder.DefaultContextPackBuilder
import com.correx.core.context.compression.DefaultContextCompressor
import com.correx.core.events.EventDispatcher
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkspaceStateObservedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.inference.InferenceProjector
import com.correx.core.inference.InferenceProvider
import com.correx.core.inference.InferenceRepository
import com.correx.core.inference.ProviderHealth
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
import com.correx.core.kernel.orchestration.OrchestrationConfig
import com.correx.core.kernel.orchestration.OrchestrationProjector
import com.correx.core.kernel.orchestration.OrchestrationRepository
import com.correx.core.kernel.orchestration.OrchestratorEngines
import com.correx.core.kernel.orchestration.OrchestratorRepositories
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.events.types.ProviderId
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.resolution.DefaultTransitionResolver
import com.correx.core.validation.pipeline.ValidationPipeline
import com.correx.infrastructure.InfrastructureModule
import com.correx.infrastructure.inference.commons.UnavailableProbe
import com.correx.infrastructure.tools.FileEditConfig
import com.correx.infrastructure.tools.FileReadConfig
import com.correx.infrastructure.tools.FileWriteConfig
import com.correx.infrastructure.tools.ShellConfig
import com.correx.infrastructure.tools.ToolConfig
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.testApplication
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path

class EventInspectRoutesTest {

    private val testJson = Json { ignoreUnknownKeys = true }

    private val s1 = SessionId("s1")

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

    private val seedEvents = listOf(
        storedEvent(
            WorkspaceStateObservedEvent(sessionId = s1, workspaceRoot = "/repo", stateKey = "git:abc", source = "git"),
            1L,
        ),
        storedEvent(
            WorkspaceStateObservedEvent(sessionId = s1, workspaceRoot = "/repo", stateKey = "git:def", source = "git"),
            2L,
        ),
        storedEvent(InitialIntentEvent(sessionId = s1, intent = "build the thing"), 3L),
    )

    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()
    }

    private fun buildModule(eventStore: EventStore, tempDir: Path): ServerModule {
        val artifactStore = object : ArtifactStore {
            override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("noop")
            override suspend fun get(id: ArtifactId): ByteArray? = null
            override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
        }
        val provider = InfrastructureModule.createLlamaCppProvider(
            modelId = "test-model",
            modelPath = "/dev/null",
            baseUrl = "http://127.0.0.1:1",
        )
        val providerRegistry = InfrastructureModule.createProviderRegistry(listOf(provider))
        val inferenceRouter = com.correx.core.inference.DefaultInferenceRouter(
            providerRegistry,
            com.correx.infrastructure.inference.FirstAvailableRoutingStrategy(),
        )
        val toolConfig = ToolConfig(
            shell = ShellConfig(enabled = false, allowedExecutables = emptySet(), workingDir = tempDir),
            fileRead = FileReadConfig(enabled = false, allowedPaths = setOf(tempDir)),
            fileWrite = FileWriteConfig(enabled = false, allowedPaths = setOf(tempDir), workingDir = tempDir),
            fileEdit = FileEditConfig(enabled = false, allowedPaths = setOf(tempDir), workingDir = tempDir),
        )
        val toolRegistry = InfrastructureModule.createToolRegistry(toolConfig)
        val eventDispatcher = EventDispatcher(eventStore)
        val toolExecutor = InfrastructureModule.createToolExecutor(
            registry = toolRegistry,
            eventDispatcher = eventDispatcher,
            workDir = tempDir,
            artifactStore = null,
        )
        val engines = OrchestratorEngines(
            transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
            contextPackBuilder = DefaultContextPackBuilder(DefaultContextCompressor()),
            inferenceRouter = inferenceRouter,
            validationPipeline = ValidationPipeline(validators = emptyList()),
            approvalEngine = com.correx.core.approvals.domain.DefaultApprovalEngine(),
            riskAssessor = DefaultRiskAssessor(),
            toolRegistry = toolRegistry,
            toolExecutor = toolExecutor,
        )
        val repositories = OrchestratorRepositories(
            eventStore = eventStore,
            inferenceRepository = InferenceRepository(DefaultEventReplayer(eventStore, InferenceProjector())),
            orchestrationRepository = OrchestrationRepository(
                DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())),
            ),
            sessionRepository = DefaultSessionRepository(
                DefaultEventReplayer(eventStore, SessionProjector(DefaultSessionReducer())),
            ),
            artifactRepository = InfrastructureModule.createArtifactRepository(eventStore),
            approvalRepository = InfrastructureModule.createApprovalRepository(eventStore),
        )
        val orchestrator = DefaultSessionOrchestrator(
            repositories = repositories,
            engines = engines,
            retryCoordinator = DefaultRetryCoordinator(eventStore),
            artifactStore = artifactStore,
            tokenizer = provider.tokenizer,
            decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore),
        )
        val routerFacade = InfrastructureModule.createTalkieFacade(
            eventStore = eventStore,
            inferenceRouter = inferenceRouter,
            config = TalkieConfig(),
            tokenizer = provider.tokenizer,
        )
        val noopProviderRegistry = object : ProviderRegistry {
            override fun listAll() = emptyList<InferenceProvider>()
            override suspend fun healthCheckAll() = emptyMap<ProviderId, ProviderHealth>()
        }
        val noopWorkflowRegistry = object : WorkflowRegistry {
            override fun listAll(): List<WorkflowSummary> = emptyList()
            override fun find(workflowId: String): WorkflowGraph? = null
        }
        val sessionUndoService = SessionUndoService(
            eventStore = eventStore,
            artifactStore = artifactStore,
            bootRoots = setOf(tempDir),
        )
        return ServerModule(
            orchestrator = orchestrator,
            eventStore = eventStore,
            artifactStore = artifactStore,
            sessionRepository = repositories.sessionRepository,
            workflowRegistry = noopWorkflowRegistry,
            providerRegistry = noopProviderRegistry,
            defaultOrchestrationConfig = OrchestrationConfig(sandboxRoot = tempDir),
            routerFacade = routerFacade,
            orchestrationRepository = repositories.orchestrationRepository,
            approvalRepository = repositories.approvalRepository,
            toolRegistry = toolRegistry,
            sessionUndoService = sessionUndoService,
            resourceProbe = UnavailableProbe,
        )
    }

    @Test
    fun `GET sessions id events returns all events ascending sequence`(@TempDir tempDir: Path) {
        val eventStore = SeededEventStore(seedEvents)
        val module = buildModule(eventStore, tempDir)

        testApplication {
            application { configureServer(module) }
            val client = createClient {}

            val response = client.get("/sessions/s1/events")
            assertEquals(HttpStatusCode.OK, response.status)
            val array = testJson.parseToJsonElement(response.bodyAsText()) as JsonArray
            assertEquals(3, array.size)
            assertEquals(1L, (array[0] as JsonObject)["sequence"]!!.jsonPrimitive.content.toLong())
            assertEquals(2L, (array[1] as JsonObject)["sequence"]!!.jsonPrimitive.content.toLong())
            assertEquals(3L, (array[2] as JsonObject)["sequence"]!!.jsonPrimitive.content.toLong())
        }
    }

    @Test
    fun `GET sessions id events with type filter returns matching events only`(@TempDir tempDir: Path) {
        val eventStore = SeededEventStore(seedEvents)
        val module = buildModule(eventStore, tempDir)

        testApplication {
            application { configureServer(module) }
            val client = createClient {}

            val response = client.get("/sessions/s1/events?type=InitialIntent")
            assertEquals(HttpStatusCode.OK, response.status)
            val array = testJson.parseToJsonElement(response.bodyAsText()) as JsonArray
            assertEquals(1, array.size)
            assertEquals("InitialIntent", (array[0] as JsonObject)["type"]!!.jsonPrimitive.content)
        }
    }

    @Test
    fun `GET sessions id events with fromSeq skips earlier sequences`(@TempDir tempDir: Path) {
        val eventStore = SeededEventStore(seedEvents)
        val module = buildModule(eventStore, tempDir)

        testApplication {
            application { configureServer(module) }
            val client = createClient {}

            val response = client.get("/sessions/s1/events?fromSeq=2")
            assertEquals(HttpStatusCode.OK, response.status)
            val array = testJson.parseToJsonElement(response.bodyAsText()) as JsonArray
            assertEquals(2, array.size)
            assertEquals(2L, (array[0] as JsonObject)["sequence"]!!.jsonPrimitive.content.toLong())
            assertEquals(3L, (array[1] as JsonObject)["sequence"]!!.jsonPrimitive.content.toLong())
        }
    }

    @Test
    fun `GET sessions id events with limit truncates after filtering`(@TempDir tempDir: Path) {
        val eventStore = SeededEventStore(seedEvents)
        val module = buildModule(eventStore, tempDir)

        testApplication {
            application { configureServer(module) }
            val client = createClient {}

            val response = client.get("/sessions/s1/events?limit=1")
            assertEquals(HttpStatusCode.OK, response.status)
            val array = testJson.parseToJsonElement(response.bodyAsText()) as JsonArray
            assertEquals(1, array.size)
            // Tail semantics: limit with no fromSeq returns the most recent events, so seq 3 (the
            // last seeded event), not the oldest. A pending approval lives at the tail — this is the
            // behaviour a headless operator needs.
            assertEquals(3L, (array[0] as JsonObject)["sequence"]!!.jsonPrimitive.content.toLong())

            val filtered = client.get("/sessions/s1/events?type=WorkspaceStateObserved&limit=1")
            val filteredArray = testJson.parseToJsonElement(filtered.bodyAsText()) as JsonArray
            assertEquals(1, filteredArray.size)
            assertEquals(
                "WorkspaceStateObserved",
                (filteredArray[0] as JsonObject)["type"]!!.jsonPrimitive.content,
            )
            // Truncation is applied AFTER filtering: the tail of the two WorkspaceStateObserved
            // events is seq 2, not the InitialIntent at seq 3.
            assertEquals(2L, (filteredArray[0] as JsonObject)["sequence"]!!.jsonPrimitive.content.toLong())
        }
    }

    @Test
    fun `GET sessions id events for unknown session returns empty array`(@TempDir tempDir: Path) {
        val eventStore = SeededEventStore(seedEvents)
        val module = buildModule(eventStore, tempDir)

        testApplication {
            application { configureServer(module) }
            val client = createClient {}

            val response = client.get("/sessions/no-such-session/events")
            assertEquals(HttpStatusCode.OK, response.status)
            val array = testJson.parseToJsonElement(response.bodyAsText()) as JsonArray
            assertEquals(0, array.size)
        }
    }
}
