package com.correx.apps.server.metrics

import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.BriefEchoMismatchEvent
import com.correx.core.events.events.CapabilityGapReflectedEvent
import com.correx.core.events.events.CapabilityGapVerdict
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.LowQualityExtractionEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.events.RepoKnowledgeHitsFilteredEvent
import com.correx.core.events.events.RepoKnowledgeRetrievedEvent
import com.correx.core.events.events.SourceFetchedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolReceipt
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.inference.TokenUsage
import com.correx.core.utils.TypeId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class MetricsProjectionTest {

    private val sessionId: SessionId = TypeId("metrics-session-1")
    private val stageId = TypeId("stage-1")
    private val t0 = Instant.parse("2026-01-01T00:00:00Z")

    private fun at(seconds: Long): Instant = t0.plus(kotlin.time.Duration.parse("${seconds}s"))

    private fun stored(payload: EventPayload, seq: Long, at: Instant): StoredEvent =
        StoredEvent(
            metadata = EventMetadata(
                eventId = EventId("e$seq"),
                sessionId = sessionId,
                timestamp = at,
                schemaVersion = 1,
                causationId = null,
                correlationId = null,
            ),
            sequence = seq,
            sessionSequence = seq,
            payload = payload,
        )

    private fun inferenceDone(provider: String, latencyMs: Long, prompt: Int, completion: Int) =
        InferenceCompletedEvent(
            requestId = TypeId("inf-$provider-$latencyMs"),
            sessionId = sessionId,
            stageId = stageId,
            providerId = TypeId(provider),
            tokensUsed = TokenUsage(promptTokens = prompt, completionTokens = completion),
            latencyMs = latencyMs,
            responseArtifactId = TypeId("art-$provider-$latencyMs"),
        )

    private fun toolDone(name: String, durationMs: Long) =
        ToolExecutionCompletedEvent(
            invocationId = TypeId("inv-$name-$durationMs"),
            sessionId = sessionId,
            toolName = name,
            receipt = ToolReceipt(
                invocationId = TypeId("inv-$name-$durationMs"),
                toolName = name,
                exitCode = 0,
                outputSummary = "ok",
                durationMs = durationMs,
                tier = Tier.T1,
                timestamp = t0,
            ),
        )

    private val seed = listOf(
        stored(inferenceDone("lfm:a", 2000, 100, 50), 1L, at(0)),
        stored(inferenceDone("lfm:a", 3000, 200, 100), 2L, at(1)),
        stored(inferenceDone("lfm:b", 1000, 10, 20), 3L, at(2)),
        stored(toolDone("shell.read", 500), 4L, at(3)),
        stored(toolDone("shell.read", 700), 5L, at(4)),
        stored(toolDone("fs.write", 300), 6L, at(5)),
        stored(
            ApprovalRequestedEvent(
                requestId = TypeId("req-1"),
                tier = Tier.T2,
                validationReportId = TypeId("vr-1"),
                riskSummaryId = null,
                sessionId = sessionId,
                stageId = stageId,
                projectId = null,
            ),
            7L, at(10),
        ),
        stored(
            ApprovalDecisionResolvedEvent(
                decisionId = TypeId("dec-1"),
                requestId = TypeId("req-1"),
                outcome = ApprovalOutcome.APPROVED,
                status = ApprovalStatus.COMPLETED,
                tier = Tier.T2,
                resolutionTimestamp = at(40),
                reason = null,
            ),
            8L, at(40),
        ),
        stored(
            ApprovalRequestedEvent(
                requestId = TypeId("req-2"),
                tier = Tier.T2,
                validationReportId = TypeId("vr-2"),
                riskSummaryId = null,
                sessionId = sessionId,
                stageId = stageId,
                projectId = null,
            ),
            9L, at(50),
        ),
        stored(
            InferenceFailedEvent(
                requestId = TypeId("inf-fail"),
                sessionId = sessionId,
                stageId = stageId,
                providerId = TypeId("lfm:a"),
                reason = "boom",
            ),
            10L, at(51),
        ),
        stored(
            ToolExecutionFailedEvent(
                invocationId = TypeId("inv-fail"),
                sessionId = sessionId,
                toolName = "shell.read",
                reason = "nonzero exit",
            ),
            11L, at(52),
        ),
        stored(
            WorkflowFailedEvent(
                sessionId = sessionId,
                stageId = stageId,
                reason = "aborted",
                retryExhausted = true,
            ),
            12L, at(60),
        ),
    )

    private class SeededEventStore(private val events: List<StoredEvent>) : EventStore {
        override suspend fun append(event: NewEvent): StoredEvent = error("unused")
        override suspend fun appendAll(events: List<NewEvent>): List<StoredEvent> = error("unused")
        override fun read(sessionId: SessionId): List<StoredEvent> =
            events.filter { it.metadata.sessionId == sessionId }
        override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> =
            events.filter { it.metadata.sessionId == sessionId && it.sequence >= fromSequence }
        override fun lastSequence(sessionId: SessionId): Long? =
            events.filter { it.metadata.sessionId == sessionId }.lastOrNull()?.sequence
        override fun subscribe(sessionId: SessionId): Flow<StoredEvent> = emptyFlow()
        override fun subscribeAll(): Flow<StoredEvent> = emptyFlow()
        override suspend fun lastGlobalSequence(): Long = 0L
        override fun allEvents(): Sequence<StoredEvent> = emptySequence()
        override fun allSessionIds(): Set<SessionId> = events.map { it.metadata.sessionId }.toSet()
    }

    private fun report() = MetricsInspectionService(SeededEventStore(seed)).inspect(sessionId)

    @Test
    fun `session span and event count come from event metadata`() {
        val r = report()
        assertEquals(sessionId.value, r.sessionId)
        assertEquals(12L, r.eventCount)
        assertEquals(60_000L, r.sessionDurationMs)
    }

    @Test
    fun `inference totals and per-provider breakdown accumulate`() {
        val r = report()
        assertEquals(3L, r.inferenceCount)
        assertEquals(6000L, r.inferenceMs)
        assertEquals(310L, r.promptTokens)
        assertEquals(170L, r.completionTokens)

        val a = r.perProvider.single { it.provider == "lfm:a" }
        assertEquals(2L, a.completedCount)
        assertEquals(5000L, a.totalLatencyMs)
        assertEquals(300L, a.promptTokens)
        assertEquals(150L, a.completionTokens)

        val b = r.perProvider.single { it.provider == "lfm:b" }
        assertEquals(1L, b.completedCount)
        assertEquals(1000L, b.totalLatencyMs)
    }

    @Test
    fun `tool totals and per-tool breakdown accumulate`() {
        val r = report()
        assertEquals(3L, r.toolCount)
        assertEquals(1500L, r.toolMs)

        val read = r.perTool.single { it.toolName == "shell.read" }
        assertEquals(2L, read.completedCount)
        assertEquals(1200L, read.totalDurationMs)

        val write = r.perTool.single { it.toolName == "fs.write" }
        assertEquals(1L, write.completedCount)
        assertEquals(300L, write.totalDurationMs)
    }

    @Test
    fun `approval wait is correlated request-to-resolution and pending excludes wait`() {
        val r = report()
        assertEquals(2L, r.approvalsRequested)
        assertEquals(1L, r.approvalsResolved)
        assertEquals(1L, r.approvalsPending)
        // req-1: requested at t+10s, resolved at t+40s -> 30s. req-2 unresolved -> contributes 0.
        assertEquals(30_000L, r.approvalWaitMs)
        assertEquals(30_000L, r.avgApprovalWaitMs)

        val t2 = r.perTier.single { it.tier == "T2" }
        assertEquals(2L, t2.requestedCount)
        assertEquals(1L, t2.resolvedCount)
        assertEquals(30_000L, t2.totalWaitMs)
    }

    @Test
    fun `failure counts are classified by event type`() {
        val f = report().failures
        assertEquals(1L, f.inferenceFailures)
        assertEquals(1L, f.toolFailures)
        assertEquals(1L, f.workflowFailures)
        assertEquals(0L, f.inferenceTimeouts)
        assertEquals(0L, f.toolRejections)
        assertEquals(0L, f.stageFailures)
    }

    @Test
    fun `time accounting splits session wall time`() {
        val r = report()
        // inference 6000/60000, tools 1500/60000, approval-wait 30000/60000
        assertEquals(10.0, r.inferencePct, 0.001)
        assertEquals(2.5, r.toolPct, 0.001)
        assertEquals(50.0, r.approvalWaitPct, 0.001)
    }

    @Test
    fun `empty session yields zeroed report`() {
        val r = MetricsInspectionService(SeededEventStore(emptyList())).inspect(sessionId)
        assertEquals(0L, r.eventCount)
        assertEquals(0L, r.sessionDurationMs)
        assertEquals(0L, r.inferenceCount)
        assertEquals(0.0, r.tokensPerSecond, 0.001)
        assertEquals(0.0, r.inferencePct, 0.001)
    }

    @Test
    fun `quality events derive extraction quality, retrieval precision, and drift-gap counts`() {
        val hit = { p: String -> RepoKnowledgeHit(path = p, text = "t", score = 0.7f) }
        val qualitySeed = listOf(
            stored(SourceFetchedEvent(sessionId, stageId, "u1", "sha1", quality = "ok"), 1L, at(1)),
            stored(SourceFetchedEvent(sessionId, stageId, "u2", "sha2", quality = "ok"), 2L, at(2)),
            stored(SourceFetchedEvent(sessionId, stageId, "u3", "sha3", quality = "low"), 3L, at(3)),
            stored(LowQualityExtractionEvent(sessionId, stageId, "u3", "sha3", "paywall"), 4L, at(4)),
            stored(
                RepoKnowledgeRetrievedEvent(sessionId, stageId, "q", listOf(hit("a"), hit("b"), hit("c"))),
                5L, at(5),
            ),
            stored(
                RepoKnowledgeHitsFilteredEvent(sessionId, "q", threshold = 0.5f, dropped = listOf(hit("d"))),
                6L, at(6),
            ),
            stored(BriefEchoMismatchEvent(sessionId, stageId, listOf("req1"), emptyList(), listOf("Foo")), 7L, at(7)),
            stored(
                CapabilityGapReflectedEvent(sessionId, stageId, "file_edit", CapabilityGapVerdict.NEEDS_TOOL, "d", 0L),
                8L, at(8),
            ),
            // RESOLVED = self-corrected, not a genuine gap — must not count.
            stored(
                CapabilityGapReflectedEvent(sessionId, stageId, "file_write", CapabilityGapVerdict.RESOLVED, "d", 0L),
                9L, at(9),
            ),
        )
        val q = MetricsInspectionService(SeededEventStore(qualitySeed)).inspect(sessionId).quality

        assertEquals(3L, q.sourceFetches)
        assertEquals(1L, q.lowQualityExtractions)
        assertEquals(2.0 / 3.0, q.extractionQualityRate, 0.001)
        assertEquals(3L, q.retrievedHits)
        assertEquals(1L, q.droppedHits)
        assertEquals(0.75, q.retrievalPrecision, 0.001)
        assertEquals(1L, q.briefDriftCount)
        assertEquals(1L, q.capabilityGapCount)
    }
}
