package com.correx.apps.server

import com.correx.apps.server.memory.ArchitectContradictionChecker
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.ArtifactContentStoredEvent
import com.correx.core.events.events.ArtifactCreatedEvent
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.PossibleContradictionFlaggedEvent
import com.correx.core.events.events.SessionWorkspaceBoundEvent
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.ProviderId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.Embedder
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.risk.DefaultRiskAssessor
import com.correx.core.talkie.l3.L3Hit
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
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.persistence.InMemoryEventStore
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 kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
import java.util.UUID

/** Always returns the same vector so the canned L3 store fully controls the scores. */
private class OnesEmbedder(override val dimension: Int = 8) : Embedder {
    override suspend fun embed(text: String): FloatArray = FloatArray(dimension) { 1f }
}

/** Returns canned hits regardless of the query vector. */
private class CannedL3MemoryStore(private val hits: List<L3Hit>) : L3MemoryStore {
    override suspend fun store(entry: L3MemoryEntry) = Unit
    override suspend fun query(query: L3Query): List<L3Hit> = hits.sortedByDescending { it.score }.take(query.k)
    override suspend fun existsByTurnIdPrefix(prefix: String): Boolean = hits.any { it.entry.turnId.startsWith(prefix) }
    override suspend fun close() = Unit
}

/** Minimal in-memory CAS keyed by content hash. */
private class MapArtifactStore(private val bytesByHash: Map<String, ByteArray>) : ArtifactStore {
    override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("00".repeat(32))
    override suspend fun get(id: ArtifactId): ByteArray? = bytesByHash[id.value]
    override suspend fun flushBefore(commit: suspend () -> Unit) { commit() }
}

/**
 * Exercises [ServerModule.handleArchitectArtifact]: the extraction/dispatch helper that backs the
 * display-only architect contradiction subscription (BACKLOG §B-§4). A full subscription/integration
 * test is not required — these assert the helper fetches the design artifact text and emits (or
 * skips) the flag. The surrounding module is built with in-memory infra (no real model needed).
 */
class ArchitectContradictionHookTest {

    private val session = SessionId("new-session")
    private val priorSession = SessionId("prior-session")
    private val architectStage = StageId("architect")
    private val designArtifact = ArtifactId("design")
    private val contentHash = ArtifactId("cafebabe")

    private fun append(es: EventStore, payload: EventPayload, sid: SessionId = session) = runBlocking {
        es.append(
            NewEvent(
                EventMetadata(EventId(UUID.randomUUID().toString()), sid, Clock.System.now(), 1, null, null),
                payload,
            ),
        )
    }

    private fun priorDecisionHit(text: String, score: Float) = L3Hit(
        entry = L3MemoryEntry(
            id = UUID.randomUUID().toString(),
            sessionId = priorSession,
            turnId = "project:/repo",
            text = text,
            vector = FloatArray(8) { 1f },
            timestampMs = 0L,
        ),
        score = score,
    )

    private fun bindWorkspace(es: EventStore) = append(
        es,
        SessionWorkspaceBoundEvent(session, workspaceRoot = "/repo", allowedPaths = listOf("/repo")),
    )

    private fun flagsIn(es: EventStore): List<PossibleContradictionFlaggedEvent> =
        es.read(session).map { it.payload }.filterIsInstance<PossibleContradictionFlaggedEvent>()

    private fun buildModule(
        eventStore: EventStore,
        artifactStore: ArtifactStore,
        checker: ArchitectContradictionChecker?,
        tempDir: Path,
    ): ServerModule {
        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 toolExecutor = InfrastructureModule.createToolExecutor(
            registry = toolRegistry,
            eventDispatcher = EventDispatcher(eventStore),
            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
        }
        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(eventStore, artifactStore, bootRoots = setOf(tempDir)),
            resourceProbe = UnavailableProbe,
            architectContradictionChecker = checker,
        )
    }

    @Test
    fun `emits the flag when the checker surfaces a near prior decision`(@TempDir tempDir: Path) = runBlocking {
        val es = InMemoryEventStore()
        val designJson = """{"approach":"Use Postgres for the event store.","components":["db.kt"]}"""
        val artifacts = MapArtifactStore(mapOf(contentHash.value to designJson.toByteArray()))
        bindWorkspace(es)
        // ArtifactContentStored precedes ArtifactCreated for the same artifactId (inference-time).
        append(es, ArtifactContentStoredEvent(designArtifact, contentHash, session, architectStage))
        val created = ArtifactCreatedEvent(designArtifact, session, architectStage, schemaVersion = 1)
        append(es, created)
        val checker = ArchitectContradictionChecker(
            OnesEmbedder(),
            CannedL3MemoryStore(listOf(priorDecisionHit("Decided to use SQLite for the event store.", 0.9f))),
        )

        val flag = buildModule(es, artifacts, checker, tempDir).handleArchitectArtifact(checker, created)

        assertNotNull(flag, "expected a contradiction flag")
        assertEquals(session, flag!!.sessionId)
        assertEquals(architectStage, flag.stageId)
        // The design schema's `approach` field is preferred as the decision text.
        assertEquals("Use Postgres for the event store.", flag.decisionSummary)
        assertEquals("Decided to use SQLite for the event store.", flag.related.single().summary)
        // The flag is appended to the session stream (display-only, but recorded for the operator).
        assertEquals(1, flagsIn(es).size)
    }

    @Test
    fun `returns null and appends nothing when the checker finds no related decision`(
        @TempDir tempDir: Path,
    ) = runBlocking {
        val es = InMemoryEventStore()
        val designJson = """{"approach":"Use Postgres for the event store.","components":["db.kt"]}"""
        val artifacts = MapArtifactStore(mapOf(contentHash.value to designJson.toByteArray()))
        bindWorkspace(es)
        append(es, ArtifactContentStoredEvent(designArtifact, contentHash, session, architectStage))
        val created = ArtifactCreatedEvent(designArtifact, session, architectStage, schemaVersion = 1)
        append(es, created)
        val checker = ArchitectContradictionChecker(OnesEmbedder(), CannedL3MemoryStore(emptyList()))

        val flag = buildModule(es, artifacts, checker, tempDir).handleArchitectArtifact(checker, created)

        assertNull(flag)
        assertEquals(0, flagsIn(es).size)
    }

    @Test
    fun `returns null when no content hash was recorded for the artifact`(@TempDir tempDir: Path) = runBlocking {
        val es = InMemoryEventStore()
        val created = ArtifactCreatedEvent(designArtifact, session, architectStage, schemaVersion = 1)
        append(es, created)
        val checker = ArchitectContradictionChecker(
            OnesEmbedder(),
            CannedL3MemoryStore(listOf(priorDecisionHit("Decided to use SQLite.", 0.9f))),
        )

        val flag = buildModule(es, MapArtifactStore(emptyMap()), checker, tempDir)
            .handleArchitectArtifact(checker, created)

        assertNull(flag)
    }
}
