import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalProjector
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.DefaultApprovalReducer
import com.correx.core.approvals.GrantScope
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.GrantId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.ValidationReportId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail

class ApprovalFuzzTest {

    private companion object {
        const val FUZZ_ITERATIONS = 500
        const val MAX_STEPS = 100
        val SEED: Long = System.getProperty("fuzz.seed")?.toLong() ?: System.currentTimeMillis()
        val BASE_TIMESTAMP: Instant = Instant.parse("2026-01-01T00:00:00Z")
        val SESSION_ID: SessionId = SessionId("fuzz-session")
    }

    private val reducer = DefaultApprovalReducer()
    private val projector = ApprovalProjector(reducer)

    @Test
    fun `reducer is total and policy-denial is terminal`() {
        val rng = kotlin.random.Random(SEED)
        repeat(FUZZ_ITERATIONS) { iteration ->
            val iterSeed = rng.nextLong()
            val sequence1 = randomApprovalSequence(kotlin.random.Random(iterSeed))
            val sequence2 = randomApprovalSequence(kotlin.random.Random(iterSeed))

            val state1 = runCatching { applySequence(sequence1) }.getOrElse { ex ->
                fail(
                    "Totality violated: reducer threw on a random ordering. " +
                        "error=${ex.message} seed=$SEED iterSeed=$iterSeed iteration=$iteration"
                )
            }
            val state2 = runCatching { applySequence(sequence2) }.getOrElse { ex ->
                fail(
                    "Totality violated: reducer threw on the replayed ordering. " +
                        "error=${ex.message} seed=$SEED iterSeed=$iterSeed iteration=$iteration"
                )
            }

            if (state1 != state2) {
                fail(
                    "Determinism violated: same seed produced different final states. " +
                        "seed=$SEED iterSeed=$iterSeed iteration=$iteration"
                )
            }

            assertPolicyDenialTerminal(state1, sequence1, SEED, iterSeed, iteration)
        }
    }

    private fun applySequence(events: List<StoredEvent>): com.correx.core.approvals.model.ApprovalState {
        var state = projector.initial()
        events.forEach { state = projector.apply(state, it) }
        return state
    }

    // Reducer-level terminality: for each decisionId, the *last* event wins (map put-semantics).
    // If the last event for a decisionId carries REJECTED, the final state must show REJECTED —
    // not APPROVED.  This is the reducer guarantee; cross-requestId sequencing is an engine concern
    // (invariant #4) enforced above the reducer layer.
    private fun assertPolicyDenialTerminal(
        state: com.correx.core.approvals.model.ApprovalState,
        events: List<StoredEvent>,
        seed: Long,
        iterSeed: Long,
        iteration: Int,
    ) {
        val lastOutcomeByDecisionId = events
            .map { it.payload }
            .filterIsInstance<ApprovalDecisionResolvedEvent>()
            .groupBy { it.decisionId }
            .mapValues { (_, decisions) -> decisions.last().outcome }

        lastOutcomeByDecisionId.forEach { (decisionId, lastOutcome) ->
            if (lastOutcome == ApprovalOutcome.REJECTED) {
                val finalDecision = state.decisions[decisionId]
                if (finalDecision != null && finalDecision.isApproved) {
                    fail(
                        "Denial terminality violated: decision $decisionId last seen REJECTED " +
                            "but final state is approved. " +
                            "seed=$seed iterSeed=$iterSeed iteration=$iteration"
                    )
                }
            }
        }
    }

    private fun randomApprovalSequence(rng: kotlin.random.Random): List<StoredEvent> {
        val requestCount = rng.nextInt(1, 5)
        val requestIds = (0 until requestCount).map { ApprovalRequestId("req-$it-${rng.nextInt(1000)}") }
        val payloads = mutableListOf<EventPayload>()

        requestIds.forEach { requestId ->
            payloads.add(randomRequestedEvent(requestId, rng))
        }

        val extraPayloadCount = rng.nextInt(0, requestCount * 2 + 1)
        repeat(extraPayloadCount) {
            payloads.add(randomExtraPayload(requestIds, rng))
        }

        payloads.shuffle(java.util.Random(rng.nextLong()))

        return payloads.take(MAX_STEPS).mapIndexed { idx, payload ->
            storedEvent(payload, idx + 1L)
        }
    }

    private fun randomRequestedEvent(requestId: ApprovalRequestId, rng: kotlin.random.Random): ApprovalRequestedEvent {
        val tiers = Tier.entries.toList()
        return ApprovalRequestedEvent(
            requestId = requestId,
            tier = tiers[rng.nextInt(tiers.size)],
            validationReportId = ValidationReportId("vr-${rng.nextInt(1000)}"),
            riskSummaryId = null,
            sessionId = SESSION_ID,
            stageId = null,
            projectId = null,
        )
    }

    private fun randomExtraPayload(requestIds: List<ApprovalRequestId>, rng: kotlin.random.Random): EventPayload {
        val requestId = requestIds[rng.nextInt(requestIds.size)]
        return when (rng.nextInt(4)) {
            0 -> rejectedDecisionEvent(requestId, rng)
            1 -> approvedDecisionEvent(requestId, rng)
            2 -> createdGrantEvent(rng)
            else -> expiredGrantEvent(rng)
        }
    }

    private fun rejectedDecisionEvent(requestId: ApprovalRequestId, rng: kotlin.random.Random): ApprovalDecisionResolvedEvent =
        ApprovalDecisionResolvedEvent(
            decisionId = ApprovalDecisionId("dec-rejected-${rng.nextInt(10000)}"),
            requestId = requestId,
            outcome = ApprovalOutcome.REJECTED,
            status = ApprovalStatus.COMPLETED,
            tier = Tier.T2,
            resolutionTimestamp = BASE_TIMESTAMP,
            reason = "policy denial",
        )

    private fun approvedDecisionEvent(requestId: ApprovalRequestId, rng: kotlin.random.Random): ApprovalDecisionResolvedEvent =
        ApprovalDecisionResolvedEvent(
            decisionId = ApprovalDecisionId("dec-approved-${rng.nextInt(10000)}"),
            requestId = requestId,
            outcome = ApprovalOutcome.APPROVED,
            status = ApprovalStatus.COMPLETED,
            tier = Tier.T2,
            resolutionTimestamp = BASE_TIMESTAMP,
            reason = null,
        )

    private fun createdGrantEvent(rng: kotlin.random.Random): ApprovalGrantCreatedEvent =
        ApprovalGrantCreatedEvent(
            grantId = GrantId("grant-${rng.nextInt(10000)}"),
            scope = GrantScope.SESSION(toolName = "shell"),
            permittedTiers = setOf(Tier.entries[rng.nextInt(Tier.entries.size)]),
            reason = "fuzz grant",
            expiresAt = null,
            sessionId = SESSION_ID,
            stageId = null,
            projectId = null,
            toolName = "shell",
        )

    private fun expiredGrantEvent(rng: kotlin.random.Random): ApprovalGrantExpiredEvent =
        ApprovalGrantExpiredEvent(grantId = GrantId("grant-${rng.nextInt(10000)}"))

    private fun storedEvent(payload: EventPayload, sequence: Long): StoredEvent =
        StoredEvent(
            metadata = EventMetadata(
                eventId = EventId("event-$sequence"),
                sessionId = SESSION_ID,
                timestamp = BASE_TIMESTAMP,
                schemaVersion = 1,
                causationId = null,
                correlationId = null,
            ),
            sequence = sequence,
            sessionSequence = sequence,
            payload = payload,
        )
}
