import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
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.transitions.resolution.TransitionOrdering
import com.correx.core.validation.approval.ApprovalTrigger
import com.correx.core.validation.graph.GraphValidator
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.pipeline.ValidationOutcome
import com.correx.core.validation.pipeline.ValidationPipeline
import com.correx.core.validation.semantic.SemanticValidator
import com.correx.core.validation.semantic.rules.CycleExitRule
import com.correx.core.validation.semantic.rules.MissingToolRule
import com.correx.core.validation.transition.TransitionValidator
import com.correx.testing.fixtures.CycleFixtures
import com.correx.testing.fixtures.WorkflowFixtures
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class ValidationPipelineIntegrationTest {

    private val pipeline = ValidationPipeline(
        validators = listOf(
            GraphValidator(),
            TransitionValidator(TransitionOrdering.comparator),
            SemanticValidator(
                rules = listOf(
                    CycleExitRule(requirePolicyForCycles = true),
                ),
            ),
        ),
        approvalTrigger = ApprovalTrigger()
    )

    @Test
    fun `full validation pipeline returns NeedsApproval when cycles are unbound`(): Unit = runBlocking {

        val graph = WorkflowFixtures.simpleGraph()

        val cycles = listOf(CycleFixtures.simpleCycle())

        val context = ValidationContext(
            graph = graph,
            detectedCycles = cycles,
            cyclePolicies = emptySet(),
        )

        val outcome = pipeline.validate(context)

        assertInstanceOf(ValidationOutcome.NeedsApproval::class.java, outcome)
    }

    @Test
    fun `rejected outcome retryable is false - set by validator, not orchestrator`(): Unit = runBlocking {
        // A dangling transition triggers GraphValidator ERROR → Rejected(retryable=false).
        // Ownership rule: the validator sets retryable; the orchestrator must only read it.
        val a = StageId("A")
        val ghost = StageId("GHOST")
        val graphWithDanglingTransition = WorkflowGraph(
            id = "workflow-test",
            stages = mapOf(a to StageConfig()),
            transitions = setOf(TransitionEdge(TransitionId("t1"), from = a, to = ghost) { true }),
            start = a,
        )
        val pipeline = ValidationPipeline(validators = listOf(GraphValidator()), approvalTrigger = null)
        val outcome = pipeline.validate(ValidationContext(graph = graphWithDanglingTransition))

        assertInstanceOf(ValidationOutcome.Rejected::class.java, outcome)
        assertFalse((outcome as ValidationOutcome.Rejected).retryable)
    }

    @Test
    fun `SemanticValidator with requirePolicyForCycles = false ignores unbound cycles`(): Unit = runBlocking {
        // Verify that when requirePolicyForCycles is false, SemanticValidator returns no issues
        // even if there are unbound cycles. This is the production default behavior.
        val graph = WorkflowFixtures.simpleGraph()
        val cycles = listOf(CycleFixtures.simpleCycle())

        val pipelineWithFalseFlagValidator = ValidationPipeline(
            validators = listOf(
                SemanticValidator(
                    rules = listOf(
                        CycleExitRule(requirePolicyForCycles = false),
                    ),
                ),
            ),
            approvalTrigger = null
        )

        val context = ValidationContext(
            graph = graph,
            detectedCycles = cycles,
            cyclePolicies = emptySet(),
        )

        val outcome = pipelineWithFalseFlagValidator.validate(context)

        // With requirePolicyForCycles = false, the rule returns no issues,
        // so the outcome should be Passed (not NeedsApproval).
        assertInstanceOf(ValidationOutcome.Passed::class.java, outcome)
    }

    @Test
    fun `MissingToolRule detects unregistered tool in stage`(): Unit = runBlocking {
        // A stage allows tools "bash" and "ghost", but only "bash" is registered.
        // The rule should emit exactly one MISSING_TOOL WARNING for "ghost".
        val stageA = StageId("A")
        val graph = WorkflowGraph(
            id = "workflow-test",
            stages = mapOf(stageA to StageConfig(allowedTools = setOf("bash", "ghost"))),
            transitions = emptySet(),
            start = stageA,
        )
        val validator = SemanticValidator(rules = listOf(MissingToolRule()))
        val context = ValidationContext(
            graph = graph,
            availableTools = setOf("bash"),
        )

        val section = validator.validate(context)

        assertEquals(1, section.issues.size)
        val issue = section.issues.first()
        assertEquals("MISSING_TOOL", issue.code)
        assertEquals("Stage 'A' allows unregistered tool 'ghost'", issue.message)
    }

    @Test
    fun `MissingToolRule returns empty when all allowed tools are registered`(): Unit = runBlocking {
        val stageA = StageId("A")
        val graph = WorkflowGraph(
            id = "workflow-test",
            stages = mapOf(stageA to StageConfig(allowedTools = setOf("bash", "python"))),
            transitions = emptySet(),
            start = stageA,
        )
        val validator = SemanticValidator(rules = listOf(MissingToolRule()))
        val context = ValidationContext(
            graph = graph,
            availableTools = setOf("bash", "python"),
        )

        val section = validator.validate(context)

        assertEquals(0, section.issues.size)
    }

    @Test
    fun `MissingToolRule returns empty when availableTools is null`(): Unit = runBlocking {
        // When tool registry is not available (null), the rule should skip validation.
        val stageA = StageId("A")
        val graph = WorkflowGraph(
            id = "workflow-test",
            stages = mapOf(stageA to StageConfig(allowedTools = setOf("ghost"))),
            transitions = emptySet(),
            start = stageA,
        )
        val validator = SemanticValidator(rules = listOf(MissingToolRule()))
        val context = ValidationContext(
            graph = graph,
            availableTools = null,
        )

        val section = validator.validate(context)

        assertEquals(0, section.issues.size)
    }

    @Test
    fun `MissingToolRule detects missing tools across multiple stages`(): Unit = runBlocking {
        val stageA = StageId("A")
        val stageB = StageId("B")
        val graph = WorkflowGraph(
            id = "workflow-test",
            stages = mapOf(
                stageA to StageConfig(allowedTools = setOf("bash", "ghost")),
                stageB to StageConfig(allowedTools = setOf("phantom", "bash")),
            ),
            transitions = setOf(TransitionEdge(TransitionId("t1"), from = stageA, to = stageB) { true }),
            start = stageA,
        )
        val validator = SemanticValidator(rules = listOf(MissingToolRule()))
        val context = ValidationContext(
            graph = graph,
            availableTools = setOf("bash"),
        )

        val section = validator.validate(context)

        // Should emit two issues: one for "ghost" in stage A, one for "phantom" in stage B.
        assertEquals(2, section.issues.size)
        val codes = section.issues.map { it.code }
        assertEquals(listOf("MISSING_TOOL", "MISSING_TOOL"), codes)
        val messages = section.issues.map { it.message }
        assertEquals(
            setOf(
                "Stage 'A' allows unregistered tool 'ghost'",
                "Stage 'B' allows unregistered tool 'phantom'"
            ),
            messages.toSet()
        )
    }
}
