package com.correx.core.kernel.orchestration

import com.correx.core.events.events.CritiqueFinding
import com.correx.core.events.events.CritiqueFindingsRecordedEvent
import com.correx.core.events.events.CritiqueOutcome
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
import com.correx.core.events.events.CritiqueRole
import com.correx.core.events.events.CritiqueSeverity
import com.correx.core.events.events.CritiqueVerdict
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class CritiqueOutcomeCorrelatorTest {

    private val sessionId = SessionId("s1")

    private fun finding(id: String, severity: CritiqueSeverity = CritiqueSeverity.MAJOR) = CritiqueFinding(
        id = id,
        role = CritiqueRole.CODE_REVIEWER,
        severity = severity,
        category = "correctness",
        message = "msg-$id",
    )

    private fun review(
        iteration: Int,
        verdict: CritiqueVerdict,
        findings: List<CritiqueFinding>,
        role: CritiqueRole = CritiqueRole.CODE_REVIEWER,
        modelHash: String = "model-A",
    ) = CritiqueFindingsRecordedEvent(
        sessionId = sessionId,
        stageId = StageId("reviewer"),
        role = role,
        modelHash = modelHash,
        iteration = iteration,
        verdict = verdict,
        findings = findings,
    )

    private fun outcomeFor(events: List<CritiqueOutcomeCorrelatedEvent>, id: String) =
        events.single { it.findingId == id }.outcome

    @Test
    fun `finding fixed between iterations is UPHELD`() {
        val reviews = listOf(
            review(1, CritiqueVerdict.CHANGES_REQUESTED, listOf(finding("f1"))),
            review(2, CritiqueVerdict.APPROVED, emptyList()),
        )
        val out = CritiqueOutcomeCorrelator.correlate(sessionId, reviews)
        assertEquals(1, out.size)
        assertEquals(CritiqueOutcome.UPHELD, out.single().outcome)
    }

    @Test
    fun `finding persisting into an approved final review is DISMISSED`() {
        val reviews = listOf(
            review(1, CritiqueVerdict.CHANGES_REQUESTED, listOf(finding("f1"))),
            review(2, CritiqueVerdict.APPROVED, listOf(finding("f1"))),
        )
        val out = CritiqueOutcomeCorrelator.correlate(sessionId, reviews)
        assertEquals(CritiqueOutcome.DISMISSED, outcomeFor(out, "f1"))
    }

    @Test
    fun `finding still open at a non-approved terminal is INCONCLUSIVE`() {
        val reviews = listOf(
            review(1, CritiqueVerdict.CHANGES_REQUESTED, listOf(finding("f1"))),
            review(2, CritiqueVerdict.CHANGES_REQUESTED, listOf(finding("f1"))),
        )
        val out = CritiqueOutcomeCorrelator.correlate(sessionId, reviews)
        assertEquals(CritiqueOutcome.INCONCLUSIVE, outcomeFor(out, "f1"))
    }

    @Test
    fun `a single approved review with a finding dismisses it`() {
        val reviews = listOf(review(1, CritiqueVerdict.APPROVED, listOf(finding("f1"))))
        val out = CritiqueOutcomeCorrelator.correlate(sessionId, reviews)
        assertEquals(CritiqueOutcome.DISMISSED, outcomeFor(out, "f1"))
    }

    @Test
    fun `mixed fates in one loop are resolved independently per finding`() {
        val reviews = listOf(
            // f1 and f2 raised; f1 fixed next round, f2 persists; f3 raised late and persists.
            review(1, CritiqueVerdict.CHANGES_REQUESTED, listOf(finding("f1"), finding("f2"))),
            review(2, CritiqueVerdict.APPROVED, listOf(finding("f2"), finding("f3"))),
        )
        val out = CritiqueOutcomeCorrelator.correlate(sessionId, reviews)
        assertEquals(3, out.size)
        assertEquals(CritiqueOutcome.UPHELD, outcomeFor(out, "f1"))
        assertEquals(CritiqueOutcome.DISMISSED, outcomeFor(out, "f2"))
        assertEquals(CritiqueOutcome.DISMISSED, outcomeFor(out, "f3"))
    }

    @Test
    fun `findings carry their severity and the producing critic's identity`() {
        val reviews = listOf(
            review(
                1, CritiqueVerdict.APPROVED, listOf(finding("f1", CritiqueSeverity.BLOCKER)),
                role = CritiqueRole.PLAN_CRITIC, modelHash = "model-Z",
            ),
        )
        val event = CritiqueOutcomeCorrelator.correlate(sessionId, reviews).single()
        assertEquals(CritiqueSeverity.BLOCKER, event.severity)
        assertEquals(CritiqueRole.PLAN_CRITIC, event.role)
        assertEquals("model-Z", event.modelHash)
    }

    @Test
    fun `each critic (role + model) is calibrated separately`() {
        val planCritic = CritiqueRole.PLAN_CRITIC
        val reviewer = CritiqueRole.CODE_REVIEWER
        val reviews = listOf(
            review(1, CritiqueVerdict.CHANGES_REQUESTED, listOf(finding("a")), role = planCritic, modelHash = "m1"),
            review(2, CritiqueVerdict.APPROVED, emptyList(), role = planCritic, modelHash = "m1"),
            review(1, CritiqueVerdict.APPROVED, listOf(finding("b")), role = reviewer, modelHash = "m2"),
        )
        val out = CritiqueOutcomeCorrelator.correlate(sessionId, reviews)
        assertEquals(CritiqueOutcome.UPHELD, outcomeFor(out, "a")) // plan critic's finding fixed
        assertEquals(CritiqueOutcome.DISMISSED, outcomeFor(out, "b")) // reviewer's finding approved-through
    }

    @Test
    fun `no reviews yields no outcomes`() {
        assertTrue(CritiqueOutcomeCorrelator.correlate(sessionId, emptyList()).isEmpty())
    }
}
