import com.correx.core.approvals.ApprovalProjector
import com.correx.core.approvals.DefaultApprovalReducer
import com.correx.core.approvals.DefaultApprovalRepository
import com.correx.core.approvals.domain.DefaultApprovalEngine
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.ClarificationAnswer
import com.correx.core.events.events.ClarificationAnsweredEvent
import com.correx.core.events.events.ClarificationRequestedEvent
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.InferenceRouter
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.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 org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test

/**
 * Integration test for the producer-exit clarification handoff: a stage whose produced artifact
 * carries an unanswered `questions` array parks its run, surfaces a [ClarificationRequestedEvent],
 * and on operator answer the run ADVANCES to the next stage with the answers in context — the
 * questioning stage is not re-run (a re-run would restart inference with no prior reasoning and
 * make the stage re-explore; 2026-07-18). The answers reach later stages as session-wide events.
 */
class ClarificationLoopTest {

    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 analysisId = ArtifactId("analysis")
    private val analystStage = StageId("analyst")

    /** Single stage that produces `analysis`, then transitions to the terminal `done`. */
    private fun graph() = WorkflowGraph(
        id = "clarify",
        stages = mapOf(
            analystStage to StageConfig(
                produces = listOf(TypedArtifactSlot(name = analysisId, kind = analysisKind)),
            ),
        ),
        transitions = setOf(
            TransitionEdge(
                id = TransitionId("analyst-to-done"),
                from = analystStage,
                to = StageId("done"),
                condition = { true },
            ),
        ),
        start = analystStage,
    )

    private val withQuestions =
        """{"summary":"build a UI","questions":[{"id":"stack","prompt":"Which stack?","options":["React","Vue"]}]}"""
    private val clean = """{"summary":"React chosen — no open questions"}"""

    /** Provider returns [responses] in order, clamping to the last once exhausted. */
    private fun orchestrator(responses: List<String>): DefaultSessionOrchestrator {
        var index = 0
        val router = object : InferenceRouter {
            override suspend fun route(
                stageId: StageId,
                requiredCapabilities: Set<ModelCapability>,
            ) = MockInferenceProvider(fixedResponse = responses[minOf(index++, responses.lastIndex)])
        }
        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,
        )
    }

    private val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0))

    @Test
    fun `open questions park the stage, the answer advances to the next stage`():
        Unit = runBlocking {
        val sessionId = SessionId("clarify-happy")
        val orchestrator = orchestrator(listOf(withQuestions, clean))
        val runJob = launch { orchestrator.run(sessionId, graph(), config) }

        val request = withTimeout(5_000) {
            var r: ClarificationRequestedEvent? = null
            while (r == null) {
                r = eventStore.read(sessionId).firstNotNullOfOrNull { it.payload as? ClarificationRequestedEvent }
                if (r == null) yield()
            }
            r
        }
        assertEquals(analystStage, request.stageId)
        assertEquals("Which stack?", request.questions.single().prompt)

        orchestrator.submitClarification(
            sessionId, request.stageId, request.requestId,
            listOf(ClarificationAnswer(questionId = "stack", value = "React")),
        )
        runJob.join()

        val events = eventStore.read(sessionId)
        assertNotNull(
            events.firstNotNullOfOrNull { it.payload as? ClarificationAnsweredEvent },
            "Expected a ClarificationAnsweredEvent",
        )
        assertNotNull(
            events.find { it.payload is WorkflowCompletedEvent },
            "Workflow should complete once the operator answered and the run advanced",
        )
    }

    @Test
    fun `pendingClarificationFor resolves the live request server-side, then clears once answered`():
        Unit = runBlocking {
        // Backs the REST /clarify route (#42): a headless caller has no ClarificationRequestedEvent,
        // so the server must resolve (stageId, requestId) from just the session id, and stop
        // resolving it once answered.
        val sessionId = SessionId("clarify-rest")
        val orchestrator = orchestrator(listOf(withQuestions, clean))
        val runJob = launch { orchestrator.run(sessionId, graph(), config) }

        val pending = withTimeout(5_000) {
            var p: ClarificationRequestedEvent? = null
            while (p == null) {
                p = orchestrator.pendingClarificationFor(sessionId)
                if (p == null) yield()
            }
            p
        }
        assertEquals(analystStage, pending.stageId)
        assertEquals("Which stack?", pending.questions.single().prompt)

        orchestrator.submitClarification(
            sessionId, pending.stageId, pending.requestId,
            listOf(ClarificationAnswer(questionId = "stack", value = "React")),
        )
        runJob.join()

        assertEquals(
            null, orchestrator.pendingClarificationFor(sessionId),
            "Answered clarification must no longer resolve as pending",
        )
    }

    @Test
    fun `a stage that emits questions parks once and is never re-run`(): Unit = runBlocking {
        val sessionId = SessionId("clarify-no-rerun")
        // Provider ALWAYS returns questions. Under the old re-run loop this would park up to the
        // round cap; under the advance-don't-re-run contract it parks exactly once, then proceeds.
        val orchestrator = orchestrator(listOf(withQuestions))
        val runJob = launch { orchestrator.run(sessionId, graph(), config) }

        val handled = mutableSetOf<String>()
        withTimeout(10_000) {
            while (eventStore.read(sessionId).none { it.payload is WorkflowCompletedEvent }) {
                val pending = eventStore.read(sessionId)
                    .mapNotNull { it.payload as? ClarificationRequestedEvent }
                    .firstOrNull { it.requestId.value !in handled }
                if (pending != null) {
                    handled += pending.requestId.value
                    orchestrator.submitClarification(
                        sessionId, pending.stageId, pending.requestId,
                        listOf(ClarificationAnswer(questionId = "stack", value = "React")),
                    )
                }
                yield()
            }
        }
        runJob.join()

        val rounds = eventStore.read(sessionId).count { it.payload is ClarificationRequestedEvent }
        assertEquals(1, rounds, "Questioning stage must park exactly once (no re-run)")
        assertNotNull(
            eventStore.read(sessionId).find { it.payload is WorkflowCompletedEvent },
            "Workflow should advance to completion after the single answered clarification",
        )
    }
}
