package com.correx.core.critique

import com.correx.core.critique.model.CalibrationKey
import com.correx.core.critique.model.CriticCalibrationState
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.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import kotlinx.datetime.Instant
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

class DefaultCriticCalibrationReducerTest {

    private val reducer = DefaultCriticCalibrationReducer()
    private val projector = CriticCalibrationProjector(reducer)
    private var seq = 0L

    private fun stored(payload: EventPayload): StoredEvent {
        seq += 1
        return StoredEvent(
            metadata = EventMetadata(
                eventId = EventId("e-$seq"),
                sessionId = SessionId("s1"),
                timestamp = Instant.parse("2026-01-01T00:00:00Z"),
                schemaVersion = 1,
                causationId = null,
                correlationId = null,
            ),
            sequence = seq,
            sessionSequence = seq,
            payload = payload,
        )
    }

    private fun outcome(
        outcome: CritiqueOutcome,
        modelHash: String = "m1",
        role: CritiqueRole = CritiqueRole.CODE_REVIEWER,
        severity: CritiqueSeverity = CritiqueSeverity.MAJOR,
    ) = stored(
        CritiqueOutcomeCorrelatedEvent(
            sessionId = SessionId("s1"),
            findingId = "f-$seq",
            role = role,
            modelHash = modelHash,
            severity = severity,
            outcome = outcome,
        ),
    )

    private fun fold(events: List<StoredEvent>): CriticCalibrationState =
        events.fold(projector.initial(), projector::apply)

    @Test
    fun `initial state is empty`() {
        assertTrue(projector.initial().byKey.isEmpty())
    }

    @Test
    fun `single UPHELD increments upheld and yields precision 1`() {
        val state = fold(listOf(outcome(CritiqueOutcome.UPHELD)))
        val stats = state.byKey.getValue(CalibrationKey("m1", CritiqueRole.CODE_REVIEWER))
        assertEquals(1, stats.upheld)
        assertEquals(0, stats.dismissed)
        assertEquals(1, stats.total)
        assertEquals(1.0, stats.precision)
    }

    @Test
    fun `single DISMISSED increments dismissed and yields precision 0`() {
        val state = fold(listOf(outcome(CritiqueOutcome.DISMISSED)))
        val stats = state.byKey.getValue(CalibrationKey("m1", CritiqueRole.CODE_REVIEWER))
        assertEquals(1, stats.dismissed)
        assertEquals(0.0, stats.precision)
    }

    @Test
    fun `INCONCLUSIVE counts toward total but not precision`() {
        val state = fold(listOf(outcome(CritiqueOutcome.INCONCLUSIVE)))
        val stats = state.byKey.getValue(CalibrationKey("m1", CritiqueRole.CODE_REVIEWER))
        assertEquals(1, stats.inconclusive)
        assertEquals(1, stats.total)
        assertEquals(0, stats.decisive)
        assertNull(stats.precision, "precision is undefined with no decisive findings")
    }

    @Test
    fun `distinct model-role keys are tracked independently`() {
        val state = fold(
            listOf(
                outcome(CritiqueOutcome.UPHELD, modelHash = "m1", role = CritiqueRole.CODE_REVIEWER),
                outcome(CritiqueOutcome.DISMISSED, modelHash = "m2", role = CritiqueRole.CODE_REVIEWER),
                outcome(CritiqueOutcome.UPHELD, modelHash = "m1", role = CritiqueRole.PLAN_CRITIC),
            ),
        )
        assertEquals(3, state.byKey.size)
        assertEquals(1.0, state.byKey.getValue(CalibrationKey("m1", CritiqueRole.CODE_REVIEWER)).precision)
        assertEquals(0.0, state.byKey.getValue(CalibrationKey("m2", CritiqueRole.CODE_REVIEWER)).precision)
        assertEquals(1.0, state.byKey.getValue(CalibrationKey("m1", CritiqueRole.PLAN_CRITIC)).precision)
    }

    @Test
    fun `precision is computed over a mix of outcomes for one key`() {
        val state = fold(
            listOf(
                outcome(CritiqueOutcome.UPHELD),
                outcome(CritiqueOutcome.UPHELD),
                outcome(CritiqueOutcome.UPHELD),
                outcome(CritiqueOutcome.DISMISSED),
                outcome(CritiqueOutcome.INCONCLUSIVE),
            ),
        )
        val stats = state.byKey.getValue(CalibrationKey("m1", CritiqueRole.CODE_REVIEWER))
        assertEquals(3, stats.upheld)
        assertEquals(1, stats.dismissed)
        assertEquals(1, stats.inconclusive)
        assertEquals(5, stats.total)
        assertEquals(0.75, stats.precision) // 3 / (3 + 1)
    }

    @Test
    fun `unrelated events pass through unchanged`() {
        val state = fold(
            listOf(
                stored(
                    StageCompletedEvent(
                        sessionId = SessionId("s1"),
                        stageId = StageId("plan"),
                        transitionId = TransitionId("t1"),
                    ),
                ),
                outcome(CritiqueOutcome.UPHELD),
                stored(
                    StageCompletedEvent(
                        sessionId = SessionId("s1"),
                        stageId = StageId("review"),
                        transitionId = TransitionId("t2"),
                    ),
                ),
            ),
        )
        assertEquals(1, state.byKey.size)
        assertEquals(1, state.byKey.getValue(CalibrationKey("m1", CritiqueRole.CODE_REVIEWER)).upheld)
    }
}
