package com.correx.core.kernel.orchestration

import com.correx.core.approvals.Tier
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.FailureTicketOpenedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.ToolRequest
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.ToolInvocationId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class RepeatedToolFailureLoopTest {

    private val stage = StageId("install_dependencies")

    @Test
    fun `same-signature failures trip the limit even when interleaved with successes`() {
        // The rejection-loop breaker would reset on the interleaved success; this must not.
        val events = buildList {
            repeat(3) { addAll(failure("npm install @types/vite@4.0.0 -> registry 404 not found")) }
            add(success()) // resets any CONSECUTIVE counter
            repeat(3) { addAll(failure("npm install @types/vite@5.1.2 -> registry 404 not found")) }
        }
        val reason = detectRepeatedToolFailure(events, stage, limit = 6)
        assertNotNull(reason)
        assertTrue(reason!!.contains("install_dependencies"))
    }

    @Test
    fun `distinct failures below the limit do not trip`() {
        val events = listOf(
            failure("npm install a -> 404"),
            failure("tsc -> type error in App.tsx"),
            failure("eslint -> no config found"),
        ).flatten()
        assertNull(detectRepeatedToolFailure(events, stage, limit = 6))
    }

    @Test
    fun `#304 - a FailureTicketOpenedEvent for the stage resets the window so stale failures don't re-trip`() {
        // 5 pre-recovery failures (below limit 6) + a ticket routing to recovery + 5 MORE post-recovery
        // failures of the SAME signature must not sum to 10 and trip the gate — recovery gets a clean
        // count, so this must stay null until 6 NEW failures accumulate after the ticket.
        val events = buildList {
            repeat(5) { addAll(failure("build gate: queries.ts(39,3): error TS1005: '}' expected")) }
            add(
                ev(
                    FailureTicketOpenedEvent(
                        sessionId = session,
                        stageId = stage,
                        gate = "stage_loop_break",
                        category = "implementation",
                        requiredCapability = "file_write",
                        routeTo = StageId("recovery"),
                        evidence = "stuck",
                        routeAttempt = 1,
                    ),
                ),
            )
            repeat(5) { addAll(failure("build gate: queries.ts(39,3): error TS1005: '}' expected")) }
        }
        assertNull(detectRepeatedToolFailure(events, stage, limit = 6))

        // A 6th post-ticket failure of the same signature DOES trip it — the reset only clears stale
        // pre-recovery count, it does not disable the breaker going forward.
        val tripped = events + failure("build gate: queries.ts(39,3): error TS1005: '}' expected")
        assertNotNull(detectRepeatedToolFailure(tripped, stage, limit = 6))
    }

    @Test
    fun `failures from other stages are not counted`() {
        val other = StageId("scaffold")
        val events = (1..8).flatMap { failure("npm install x -> 404", stageOf = other) }
        assertNull(detectRepeatedToolFailure(events, stage, limit = 6))
    }

    private var seq = 0L
    private val session = SessionId("s1")

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

    // A failure = the request (carries stageId) + the failure (carries only invocationId), correlated by id.
    private fun failure(reason: String, stageOf: StageId = stage): List<StoredEvent> {
        val id = ToolInvocationId("inv-${seq}")
        val req = ev(
            ToolInvocationRequestedEvent(
                invocationId = id, sessionId = session, stageId = stageOf,
                toolName = "shell", tier = Tier.T1,
                request = ToolRequest(id, session, stageOf, "shell"),
            ),
        )
        return listOf(req, ev(ToolExecutionFailedEvent(id, session, "shell", reason)))
    }

    private fun success() = ev(
        ToolInvocationRequestedEvent(
            invocationId = ToolInvocationId("ok-${seq}"), sessionId = session, stageId = stage,
            toolName = "shell", tier = Tier.T1,
            request = ToolRequest(ToolInvocationId("ok-$seq"), session, stage, "shell"),
        ),
    )
}
