package com.correx.infrastructure.workflow

import com.correx.core.events.events.PlanGroundingVerdict
import com.correx.core.events.types.StageId
import com.correx.core.transitions.conditions.AlwaysTrue
import com.correx.core.transitions.graph.BuildExpectation
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.transitions.graph.TransitionEdge
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.events.types.TransitionId
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class PlanGrounderTest {

    private fun graph(vararg stages: Pair<String, StageConfig>) = WorkflowGraph(
        id = "test",
        stages = stages.associate { StageId(it.first) to it.second },
        transitions = stages.toList().zipWithNext { (f, _), (t, _) ->
            TransitionEdge(TransitionId("$f-$t"), StageId(f), StageId(t), AlwaysTrue)
        }.toSet(),
        start = StageId(stages.first().first),
    )

    private val cmds = mapOf("build" to "npm run build", "typecheck" to "tsc --noEmit")

    @Test
    fun `stage building against a missing prerequisite is returned to architect`() {
        val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT))
        val r = PlanGrounder.ground(g, repoMapPaths = setOf("src/main.ts"), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.RETURN_TO_ARCHITECT, r.verdict)
        assertTrue(r.findings.single().contains("impl"))
    }

    @Test
    fun `manifest present in repo map grounds the build`() {
        val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT))
        val r = PlanGrounder.ground(g, repoMapPaths = setOf("package.json"), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.PASS, r.verdict)
    }

    @Test
    fun `a plan stage that scaffolds the manifest grounds a later build`() {
        val g = graph(
            "scaffold" to StageConfig(writeManifest = listOf("package.json")),
            "impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT),
        )
        val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.PASS, r.verdict)
    }

    @Test
    fun `no configured command degrades to a safe pass`() {
        val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT))
        val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = emptyMap())
        assertEquals(PlanGroundingVerdict.PASS, r.verdict)
    }

    @Test
    fun `a stage scoped to a path that neither exists nor is created is ungrounded`() {
        val g = graph("impl" to StageConfig(touches = listOf("frontend/**")))
        val r = PlanGrounder.ground(g, repoMapPaths = setOf("backend/main.kt"), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.RETURN_TO_ARCHITECT, r.verdict)
        assertTrue(r.findings.single().contains("frontend/**"))
    }

    @Test
    fun `scanned=false skips scope grounding — a missing scan can't prove a path absent`() {
        val g = graph("impl" to StageConfig(touches = listOf("frontend/**", "apps/server/**")))
        val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds, scanned = false)
        assertEquals(PlanGroundingVerdict.PASS, r.verdict)
    }

    @Test
    fun `a scope satisfied by an existing repo path grounds`() {
        val g = graph("impl" to StageConfig(touches = listOf("frontend/**")))
        val r = PlanGrounder.ground(g, repoMapPaths = setOf("frontend/App.tsx"), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.PASS, r.verdict)
    }

    @Test
    fun `a scope satisfied by a file a plan stage creates grounds`() {
        val g = graph(
            "scaffold" to StageConfig(expectedFiles = listOf("frontend/index.html")),
            "impl" to StageConfig(touches = listOf("frontend/**")),
        )
        val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.PASS, r.verdict)
    }

    @Test
    fun `a file-writing scaffold stage grounds its own scope, a later scoped stage, and a build`() {
        // Repro session d038468a: scaffold stage runs `npm create vite` (file_write + shell) scoped to
        // frontend/, creating package.json + the tree at run time — invisible to declared writes/
        // expectedFiles. Grounding must credit that scope, or it false-rejects a plan that follows the
        // "use the real scaffolder" guidance (frontend/ "doesn't exist", verify stage has "no manifest").
        val g = graph(
            "scaffold" to StageConfig(allowedTools = setOf("shell", "file_write"), touches = listOf("frontend/")),
            "design" to StageConfig(touches = listOf("frontend/**")),
            "verify" to StageConfig(buildExpectation = BuildExpectation.PROJECT),
        )
        val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.PASS, r.verdict, "findings: ${r.findings}")
    }

    @Test
    fun `a read-only shell stage does NOT credit its scope (no file_write, creates nothing)`() {
        // A stage with shell for log inspection / test runs — not scaffolding — writes nothing, so its
        // scope must not satisfy a build's manifest prerequisite. Keeps the credit keyed on create-intent.
        val g = graph(
            "inspect" to StageConfig(allowedTools = setOf("shell"), touches = listOf("logs/")),
            "verify" to StageConfig(buildExpectation = BuildExpectation.PROJECT),
        )
        val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.RETURN_TO_ARCHITECT, r.verdict)
        assertTrue(r.findings.any { it.contains("verify") && it.contains("no build manifest") })
    }

    @Test
    fun `NONE expectation never grounds`() {
        val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.NONE))
        val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
        assertEquals(PlanGroundingVerdict.PASS, r.verdict)
    }
}
