import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalProjector
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.DefaultApprovalReducer
import com.correx.core.approvals.DefaultApprovalRepository
import com.correx.core.approvals.Tier
import com.correx.core.approvals.domain.DefaultApprovalEngine
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalDecision
import com.correx.core.approvals.model.ApprovalScopeIdentity
import com.correx.core.artifacts.DefaultArtifactReducer
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.events.ApprovalRequestedEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.inference.InferenceRepository
import com.correx.core.inference.InferenceState
import com.correx.core.inference.ModelCapability
import com.correx.core.journal.DecisionJournalProjector
import com.correx.core.journal.DefaultDecisionJournalReducer
import com.correx.core.journal.DefaultDecisionJournalRepository
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.sessions.ApprovalMode
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.sessions.projections.replay.EventReplayer
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.validation.pipeline.ValidationPipeline
import com.correx.infrastructure.persistence.InMemoryEventStore
import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository
import com.correx.testing.contracts.fixtures.artifactstore.NoopArtifactStore
import com.correx.testing.fixtures.cyclePolicyMissingValidator
import com.correx.testing.fixtures.context.ContextFixtures
import com.correx.testing.fixtures.inference.MockInferenceProvider
import com.correx.testing.fixtures.transitions.TransitionFixtures
import com.correx.testing.kernel.MockSessionEventReplayer
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.yield
import kotlinx.datetime.Clock
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

/**
 * Integration test for Task 4.2: the approval gate on stages flagged `requiresApproval`.
 *
 * Drives analyst→architect with a sequenced stub provider. Asserts that:
 * 1. An [OrchestrationPausedEvent] is emitted before the architect runs, with the analyst
 *    summary in the paired [ApprovalRequestedEvent.preview].
 * 2. Submitting an approval unblocks the run and the workflow completes with the architect's
 *    execution_plan artifact.
 */
class FreestyleApprovalGateTest {

    private val eventStore = InMemoryEventStore()
    private val sessionRepository = DefaultSessionRepository(MockSessionEventReplayer())
    private val orchestrationRepository = OrchestrationRepository(
        DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())),
    )
    private val inferenceRepository = InferenceRepository(
        object : EventReplayer<InferenceState> {
            override fun rebuild(sessionId: SessionId) = InferenceState()
        },
    )
    private val approvalRepository = DefaultApprovalRepository(
        DefaultEventReplayer(eventStore, ApprovalProjector(DefaultApprovalReducer())),
    )
    private val decisionJournalRepository = DefaultDecisionJournalRepository(
        DefaultEventReplayer(eventStore, DecisionJournalProjector(DefaultDecisionJournalReducer())),
    )

    private val repositories = OrchestratorRepositories(
        eventStore = eventStore,
        inferenceRepository = inferenceRepository,
        orchestrationRepository = orchestrationRepository,
        sessionRepository = sessionRepository,
        artifactRepository = LiveArtifactRepository(eventStore, DefaultArtifactReducer()),
        approvalRepository = approvalRepository,
    )

    private val analysisKind = ConfigArtifactKind(
        id = "analysis",
        schema = JsonSchema(type = "object", properties = emptyMap()),
        llmEmitted = true,
    )

    private val executionPlanKind = ConfigArtifactKind(
        id = "execution_plan",
        schema = JsonSchema(type = "object", properties = emptyMap()),
        llmEmitted = true,
    )

    private val analysisId = ArtifactId("analysis")
    private val executionPlanId = ArtifactId("execution_plan")
    private val analystStage = StageId("analyst")
    private val architectStage = StageId("architect")

    /** Graph mirrors freestyle_planning.toml: architect stage has requiresApproval metadata. */
    private fun freestyleGraph() = WorkflowGraph(
        id = "freestyle_planning",
        stages = mapOf(
            analystStage to StageConfig(
                produces = listOf(TypedArtifactSlot(name = analysisId, kind = analysisKind)),
            ),
            architectStage to StageConfig(
                produces = listOf(TypedArtifactSlot(name = executionPlanId, kind = executionPlanKind)),
                needs = setOf(analysisId),
                metadata = mapOf("requiresApproval" to "true"),
            ),
        ),
        transitions = setOf(
            TransitionEdge(
                id = TransitionId("analyst-to-architect"),
                from = analystStage,
                to = architectStage,
                condition = { true },
            ),
            TransitionEdge(
                id = TransitionId("architect-to-done"),
                from = architectStage,
                to = StageId("done"),
                condition = { true },
            ),
        ),
        start = analystStage,
    )

    /** Seeds a prior architect-gate request + resolved decision (the retry/resume situation). */
    private suspend fun seedArchitectDecision(sessionId: SessionId, outcome: ApprovalOutcome) {
        val requestId = com.correx.core.events.types.ApprovalRequestId("seeded-req-${outcome.name}")
        eventStore.append(
            com.correx.testing.fixtures.EventFixtures.newEvent(
                com.correx.core.events.types.EventId("seed-req-${outcome.name}"),
                sessionId,
                ApprovalRequestedEvent(
                    requestId = requestId,
                    tier = Tier.T2,
                    validationReportId = com.correx.core.events.types.ValidationReportId("vr"),
                    riskSummaryId = null,
                    sessionId = sessionId,
                    stageId = architectStage,
                    projectId = null,
                    toolName = null,
                ),
            ),
        )
        eventStore.append(
            com.correx.testing.fixtures.EventFixtures.newEvent(
                com.correx.core.events.types.EventId("seed-dec-${outcome.name}"),
                sessionId,
                com.correx.core.events.events.ApprovalDecisionResolvedEvent(
                    decisionId = com.correx.core.events.types.ApprovalDecisionId("seeded-dec-${outcome.name}"),
                    requestId = requestId,
                    outcome = outcome,
                    status = ApprovalStatus.COMPLETED,
                    tier = Tier.T2,
                    resolutionTimestamp = Clock.System.now(),
                    reason = null,
                ),
            ),
        )
    }

    private val analysisSummary = """{"summary":"open questions: feasibility of X, timeline for Y"}"""
    private val executionPlanJson = """{"plan":"step 1: do A, step 2: do B"}"""

    private fun buildOrchestrator(): DefaultSessionOrchestrator {
        // Sequenced provider: first call (analyst) returns the analysis JSON,
        // second call (architect) returns the execution plan JSON.
        var callCount = 0
        val router = object : com.correx.core.inference.InferenceRouter {
            override suspend fun route(
                stageId: StageId,
                requiredCapabilities: Set<ModelCapability>,
            ) = MockInferenceProvider(
                fixedResponse = if (callCount++ == 0) analysisSummary else executionPlanJson,
            )
        }

        return DefaultSessionOrchestrator(
            repositories = repositories,
            engines = OrchestratorEngines(
                transitionResolver = TransitionFixtures.simpleResolver(),
                contextPackBuilder = ContextFixtures.simpleBuilder(),
                inferenceRouter = router,
                validationPipeline = ValidationPipeline(validators = listOf(cyclePolicyMissingValidator())),
                approvalEngine = DefaultApprovalEngine(),
                riskAssessor = DefaultRiskAssessor(),
            ),
            retryCoordinator = DefaultRetryCoordinator(eventStore),
            artifactStore = NoopArtifactStore(),
            decisionJournalRepository = decisionJournalRepository,
        )
    }

    @Test
    fun `pause event is emitted with analyst summary before architect runs`(): Unit = runBlocking {
        val sessionId = SessionId("freestyle-gate-1")
        val orchestrator = buildOrchestrator()
        val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0))

        val runJob = launch { orchestrator.run(sessionId, freestyleGraph(), config) }

        // Wait for the pause event before architect
        withTimeout(5_000) {
            while (eventStore.read(sessionId).none { it.payload is OrchestrationPausedEvent }) {
                yield()
            }
        }

        val events = eventStore.read(sessionId)
        val pause = events.firstNotNullOfOrNull { it.payload as? OrchestrationPausedEvent }
        assertNotNull(pause, "Expected OrchestrationPausedEvent")
        assertTrue(pause!!.stageId == architectStage, "Pause should be for architect stage, got ${pause.stageId}")
        assertTrue(pause.reason == "APPROVAL_PENDING", "Expected reason APPROVAL_PENDING, got ${pause.reason}")

        val approvalRequest = events.firstNotNullOfOrNull { it.payload as? ApprovalRequestedEvent }
        assertNotNull(approvalRequest, "Expected ApprovalRequestedEvent")
        assertTrue(
            approvalRequest!!.preview?.contains("open questions") == true ||
                    approvalRequest.preview?.contains("summary") == true,
            "Approval preview should carry analyst summary, got: ${approvalRequest.preview}",
        )

        runJob.cancel()
        runJob.join()
    }

    @Test
    fun `a prior REJECTED decision does not satisfy the gate — it re-prompts, not runs unapproved`(): Unit =
        runBlocking {
            val sessionId = SessionId("freestyle-gate-reject")
            // Pre-seed the log as if the architect gate had already been prompted and REJECTED (the
            // retry/resume situation): a matching request + a REJECTED decision for it. The pre-fix
            // predicate matched any decision for the request and skipped the gate, running unapproved.
            val requestId = com.correx.core.events.types.ApprovalRequestId("seeded-req")
            eventStore.append(
                com.correx.testing.fixtures.EventFixtures.newEvent(
                    com.correx.core.events.types.EventId("seed-req"),
                    sessionId,
                    ApprovalRequestedEvent(
                        requestId = requestId,
                        tier = Tier.T2,
                        validationReportId = com.correx.core.events.types.ValidationReportId("vr"),
                        riskSummaryId = null,
                        sessionId = sessionId,
                        stageId = architectStage,
                        projectId = null,
                        toolName = null,
                    ),
                ),
            )
            eventStore.append(
                com.correx.testing.fixtures.EventFixtures.newEvent(
                    com.correx.core.events.types.EventId("seed-dec"),
                    sessionId,
                    com.correx.core.events.events.ApprovalDecisionResolvedEvent(
                        decisionId = com.correx.core.events.types.ApprovalDecisionId("seeded-dec"),
                        requestId = requestId,
                        outcome = ApprovalOutcome.REJECTED,
                        status = ApprovalStatus.COMPLETED,
                        tier = Tier.T2,
                        resolutionTimestamp = Clock.System.now(),
                        reason = "operator said no",
                    ),
                ),
            )

            val orchestrator = buildOrchestrator()
            val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0))
            val runJob = launch { orchestrator.run(sessionId, freestyleGraph(), config) }

            // The gate must re-prompt: a SECOND architect approval request appears (the seeded one is
            // the first). With the bug the seeded REJECTED decision satisfied the gate and no new
            // request was emitted — the architect ran and the workflow completed unapproved.
            withTimeout(5_000) {
                while (
                    eventStore.read(sessionId)
                        .mapNotNull { it.payload as? ApprovalRequestedEvent }
                        .count { it.stageId == architectStage && it.toolName == null } < 2
                ) {
                    yield()
                }
            }

            assertTrue(
                eventStore.read(sessionId).none { it.payload is WorkflowCompletedEvent },
                "Workflow must not complete off a REJECTED decision",
            )
            runJob.cancel()
            runJob.join()
        }

    @Test
    fun `runFrom architect reuses a prior APPROVED decision and does not re-prompt on grounding retry`(): Unit =
        runBlocking {
            // Return-to-architect loop (FreestyleDriver.rerunArchitect): a grounding-rejected plan re-runs
            // the architect via runFrom(architect). The operator already approved the analyst→architect
            // gate on the first pass, so the re-run must NOT re-park — the seeded APPROVED decision below
            // stands in for that first approval; the gate must reuse it.
            val sessionId = SessionId("freestyle-gate-rerun")
            seedArchitectDecision(sessionId, ApprovalOutcome.APPROVED)

            // Provider always yields the execution plan — we enter at architect, not analyst.
            val router = object : com.correx.core.inference.InferenceRouter {
                override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>) =
                    MockInferenceProvider(fixedResponse = executionPlanJson)
            }
            val orchestrator = DefaultSessionOrchestrator(
                repositories = repositories,
                engines = OrchestratorEngines(
                    transitionResolver = TransitionFixtures.simpleResolver(),
                    contextPackBuilder = ContextFixtures.simpleBuilder(),
                    inferenceRouter = router,
                    validationPipeline = ValidationPipeline(validators = listOf(cyclePolicyMissingValidator())),
                    approvalEngine = DefaultApprovalEngine(),
                    riskAssessor = DefaultRiskAssessor(),
                ),
                retryCoordinator = DefaultRetryCoordinator(eventStore),
                artifactStore = NoopArtifactStore(),
                decisionJournalRepository = decisionJournalRepository,
            )
            val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0))

            orchestrator.runFrom(sessionId, freestyleGraph(), config, architectStage)

            val architectRequests = eventStore.read(sessionId)
                .mapNotNull { it.payload as? ApprovalRequestedEvent }
                .count { it.stageId == architectStage && it.toolName == null }
            assertTrue(
                architectRequests == 1,
                "gate must reuse the prior approval, not re-prompt: got $architectRequests",
            )
            assertNotNull(
                eventStore.read(sessionId).find { it.payload is WorkflowCompletedEvent },
                "architect should run straight through on the reused approval",
            )
        }

    @Test
    fun `approval resumes workflow and architect produces execution plan`(): Unit = runBlocking {
        val sessionId = SessionId("freestyle-gate-2")
        val orchestrator = buildOrchestrator()
        val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0))

        val runJob = launch { orchestrator.run(sessionId, freestyleGraph(), config) }

        // Wait for the approval request
        val requestId = withTimeout(5_000) {
            var id: com.correx.core.events.types.ApprovalRequestId? = null
            while (id == null) {
                id = eventStore.read(sessionId)
                    .firstNotNullOfOrNull { it.payload as? ApprovalRequestedEvent }
                    ?.requestId
                if (id == null) yield()
            }
            id
        }

        val identity = ApprovalScopeIdentity(sessionId = sessionId, stageId = architectStage, projectId = null)
        val context = ApprovalContext(identity = identity, mode = ApprovalMode.PROMPT)
        val decision = ApprovalDecision(
            id = null,
            requestId = requestId,
            outcome = ApprovalOutcome.APPROVED,
            state = ApprovalStatus.COMPLETED,
            tier = Tier.T2,
            contextSnapshot = context,
            resolutionTimestamp = Clock.System.now(),
            reason = null,
        )
        orchestrator.submitApprovalDecision(requestId, decision)

        runJob.join()

        val events = eventStore.read(sessionId)
        assertNotNull(
            events.find { it.payload is WorkflowCompletedEvent },
            "Workflow should complete after approval",
        )
    }
}
