package com.correx.infrastructure.workflow

import com.correx.core.artifacts.kind.ConfigArtifactKind
import com.correx.core.artifacts.kind.JsonSchema
import com.correx.core.artifacts.kind.TypedArtifactSlot
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.transitions.conditions.AlwaysTrue
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.transitions.graph.TransitionEdge
import com.correx.core.transitions.graph.WorkflowGraph
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class PlanLinterTest {

    private fun kind(id: String) =
        ConfigArtifactKind(id, JsonSchema(type = "object", properties = emptyMap(), additionalProperties = true))

    private fun stage(
        produces: List<String> = emptyList(),
        needs: List<String> = emptyList(),
        prompt: String = "do the thing",
    ) = StageConfig(
        produces = produces.map { TypedArtifactSlot(ArtifactId(it), kind(it)) },
        needs = needs.map { ArtifactId(it) }.toSet(),
        metadata = mapOf("promptInline" to prompt),
    )

    private fun graph(
        stages: Map<String, StageConfig>,
        edges: List<Pair<String, String>>,
        start: String,
    ) = WorkflowGraph(
        id = "test",
        stages = stages.mapKeys { StageId(it.key) },
        transitions = edges.mapIndexed { i, (f, t) ->
            TransitionEdge(TransitionId("e$i"), StageId(f), StageId(t), AlwaysTrue)
        }.toSet(),
        start = StageId(start),
    )

    private fun codes(findings: List<com.correx.core.events.events.PlanLintFinding>) = findings.map { it.code }.toSet()

    @Test
    fun `a clean linear plan passes with no findings`() {
        val g = graph(
            stages = mapOf(
                "a" to stage(produces = listOf("x"), prompt = "analyze"),
                "b" to stage(produces = listOf("y"), needs = listOf("x"), prompt = "build"),
            ),
            edges = listOf("a" to "b", "b" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertTrue(result.passed, "expected pass, hard=${result.hardFailures}")
        assertTrue(result.softFindings.isEmpty(), "expected no soft, got ${result.softFindings}")
        assertEquals(0, result.score)
    }

    @Test
    fun `a need produced by no stage is a hard failure`() {
        val g = graph(
            stages = mapOf(
                "a" to stage(produces = listOf("x"), prompt = "analyze"),
                "b" to stage(needs = listOf("x", "z"), prompt = "build"),
            ),
            edges = listOf("a" to "b", "b" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertFalse(result.passed)
        val h = result.hardFailures.single { it.code == "unproduced_need" }
        assertEquals("b", h.stageId)
        assertTrue(h.detail.contains("z"))
    }

    @Test
    fun `a need satisfied by the analysis seed is not a failure`() {
        val g = graph(
            stages = mapOf(
                "a" to stage(produces = listOf("x"), needs = listOf("analysis"), prompt = "research"),
                "b" to stage(needs = listOf("x"), prompt = "build"),
            ),
            edges = listOf("a" to "b", "b" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertTrue(result.passed, "analysis is a planning-phase seed, not an unproduced need")
    }

    @Test
    fun `a stage mentioning its own produced artifact in the prompt is not a failure`() {
        val g = graph(
            stages = mapOf(
                "a" to stage(produces = listOf("plan"), prompt = "write the plan artifact"),
                "b" to stage(needs = listOf("plan"), prompt = "build"),
            ),
            edges = listOf("a" to "b", "b" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertTrue(result.passed, "expected pass, hard=${result.hardFailures}")
    }

    @Test
    fun `a prompt using a seed name as an ordinary word is not a failure`() {
        // Repro: architect burned 3 retries because briefs mentioning "analysis"/"dod" as prose kept
        // tripping H3 on the seed IDs — words the model cannot reword away.
        val g = graph(
            stages = mapOf(
                "a" to stage(produces = listOf("scope"), prompt = "refine the analysis and meet the dod"),
                "b" to stage(needs = listOf("scope"), prompt = "build"),
            ),
            edges = listOf("a" to "b", "b" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertTrue(result.passed, "seed names in prose must not fire H3, hard=${result.hardFailures}")
    }

    @Test
    fun `a prompt mentioning another stage's artifact not in needs is a soft finding, not a block`() {
        val g = graph(
            stages = mapOf(
                "a" to stage(produces = listOf("plan"), prompt = "write the plan artifact"),
                "b" to stage(produces = listOf("report"), prompt = "use plan to build"),
            ),
            edges = listOf("a" to "b", "b" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertTrue(result.passed, "H3 is a prose heuristic — it scores but must not block the plan")
        val h = result.softFindings.single { it.code == "unreferenced_prompt_artifact" }
        assertEquals("b", h.stageId)
        assertTrue(h.detail.contains("plan"))
    }

    @Test
    fun `a cycle with no exit is a trap-state hard failure`() {
        val g = graph(
            stages = mapOf("a" to stage(), "b" to stage()),
            edges = listOf("a" to "b", "b" to "a"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertFalse(result.passed)
        assertEquals(setOf("a", "b"), result.hardFailures.filter { it.code == "trap_state" }.map { it.stageId }.toSet())
    }

    @Test
    fun `a verdict-gated review loop with an exit edge is NOT a trap`() {
        // implement <-> review, review -> done. The legitimate freestyle loop must pass.
        val g = graph(
            stages = mapOf(
                "implement" to stage(produces = listOf("patch"), prompt = "implement"),
                "review" to stage(produces = listOf("report"), needs = listOf("patch"), prompt = "review"),
            ),
            edges = listOf("implement" to "review", "review" to "implement", "review" to "done"),
            start = "implement",
        )
        val result = PlanLinter.lint(g)
        assertTrue(
            result.hardFailures.none { it.code == "trap_state" },
            "loop with exit must not trap: ${result.hardFailures}",
        )
        assertTrue(result.passed, "hard=${result.hardFailures}")
    }

    @Test
    fun `excess fan-out is a soft finding`() {
        val g = graph(
            stages = mapOf(
                "a" to stage(prompt = "fork"),
                "b" to stage(prompt = "b"), "c" to stage(prompt = "c"),
                "d" to stage(prompt = "d"), "e" to stage(prompt = "e"),
            ),
            edges = listOf("a" to "b", "a" to "c", "a" to "d", "a" to "e", "a" to "done",
                "b" to "done", "c" to "done", "d" to "done", "e" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertTrue(result.passed)
        val f = result.softFindings.single { it.code == "fan_out" }
        assertEquals("a", f.stageId)
    }

    @Test
    fun `empty and duplicate briefs are soft findings`() {
        val g = graph(
            stages = mapOf(
                "a" to stage(prompt = "  "),
                "b" to stage(prompt = "same work"),
                "c" to stage(prompt = "same work"),
            ),
            edges = listOf("a" to "b", "b" to "c", "c" to "done"),
            start = "a",
        )
        val result = PlanLinter.lint(g)
        assertTrue(result.passed)
        assertTrue(codes(result.softFindings).containsAll(setOf("empty_brief", "duplicate_brief")))
        val dup = result.softFindings.single { it.code == "duplicate_brief" }
        assertTrue(dup.detail.contains("b") && dup.detail.contains("c"))
        assertEquals(result.softFindings.size, result.score)
    }
}
