package com.correx.apps.server.approval

import com.correx.apps.server.protocol.ApprovalDecision
import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.server.protocol.ServerMessage
import com.correx.core.approvals.Tier
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.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.risk.RiskAction
import com.correx.core.events.risk.RiskLevel
import com.correx.core.events.risk.RiskSignal
import com.correx.core.events.risk.RiskSummary
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ValidationReportId
import com.correx.core.kernel.orchestration.ApprovalGateway
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import kotlinx.coroutines.withTimeout
import kotlinx.datetime.Instant
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
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
import java.util.concurrent.CopyOnWriteArrayList

class ApprovalCoordinatorWiringTest {

    private val sessionId = SessionId("session-1")
    private val requestId = ApprovalRequestId("req-1")
    private val timestamp = Instant.parse("2026-01-01T00:00:00Z")

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    @AfterEach
    fun tearDown() {
        scope.cancel()
    }

    private class RecordingGateway(@Volatile var failNext: Boolean = false) : ApprovalGateway {
        val submissions = CopyOnWriteArrayList<Pair<ApprovalRequestId, DomainApprovalDecision>>()
        override suspend fun submitApprovalDecision(requestId: ApprovalRequestId, decision: DomainApprovalDecision) {
            if (failNext) {
                failNext = false
                error("submit boom")
            }
            submissions.add(requestId to decision)
        }
    }

    private fun approvalEvent(tier: Tier) = ApprovalRequestedEvent(
        requestId = requestId,
        tier = tier,
        validationReportId = ValidationReportId("vr-1"),
        riskSummaryId = null,
        sessionId = sessionId,
        stageId = null,
        projectId = null,
    )

    @Test
    fun `submit failure keeps request answerable and retry succeeds`(): Unit = runBlocking {
        val gateway = RecordingGateway(failNext = true)
        val coord = ApprovalCoordinator(gateway)
        coord.onApprovalRequested(approvalEvent(Tier.T2))
        val msg = ClientMessage.ApprovalResponse(requestId, ApprovalDecision.APPROVE, steeringNote = null)

        val first = coord.handleResponse(msg, sessionId)
        assertInstanceOf(ServerMessage.ProtocolError::class.java, first)
        assertTrue(gateway.submissions.isEmpty())

        // Retry must not be blocked by a stale resolved flag.
        val second = coord.handleResponse(msg, sessionId)
        assertNull(second)
        assertEquals(1, gateway.submissions.size)
    }

    @Test
    fun `recorded decision carries the request's real tier`(): Unit = runBlocking {
        val gateway = RecordingGateway()
        val coord = ApprovalCoordinator(gateway)
        coord.onApprovalRequested(approvalEvent(Tier.T3))
        val msg = ClientMessage.ApprovalResponse(requestId, ApprovalDecision.APPROVE, steeringNote = null)

        coord.handleResponse(msg, sessionId)

        assertEquals(Tier.T3, gateway.submissions[0].second.tier)
    }

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

    private fun fakeEventStore(liveFlow: MutableSharedFlow<StoredEvent>): EventStore = object : 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> = emptyList()
        override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> = emptyList()
        override fun lastSequence(sessionId: SessionId): Long = 0L
        override fun subscribe(sessionId: SessionId): Flow<StoredEvent> = liveFlow
        override fun allEvents(): Sequence<StoredEvent> = emptySequence()
        override fun allSessionIds(): Set<SessionId> = emptySet()
        override fun subscribeAll(): Flow<StoredEvent> = liveFlow
        override suspend fun lastGlobalSequence(): Long = 0L
    }

    @Test
    fun `handleResponse forwards to gateway and returns null on success`(): Unit = runBlocking {
        val gateway = RecordingGateway()
        val coord = ApprovalCoordinator(gateway)
        val msg = ClientMessage.ApprovalResponse(
            requestId = requestId,
            decision = ApprovalDecision.APPROVE,
            steeringNote = null,
        )

        val reply = coord.handleResponse(msg, sessionId)

        assertNull(reply)
        assertEquals(1, gateway.submissions.size)
        assertEquals(requestId, gateway.submissions[0].first)
    }

    @Test
    fun `duplicate ApprovalResponse for same requestId returns ProtocolError`(): Unit = runBlocking {
        val gateway = RecordingGateway()
        val coord = ApprovalCoordinator(gateway)
        val msg = ClientMessage.ApprovalResponse(
            requestId = requestId,
            decision = ApprovalDecision.APPROVE,
            steeringNote = null,
        )

        val first = coord.handleResponse(msg, sessionId)
        val second = coord.handleResponse(msg, sessionId)

        assertNull(first)
        assertNotNull(second)
        assertInstanceOf(ServerMessage.ProtocolError::class.java, second)
        // Gateway only sees the first submission; dedupe blocks the second.
        assertEquals(1, gateway.submissions.size)
    }

    /**
     * Proves the ServerModule.start() subscription wiring shape: an [ApprovalRequestedEvent]
     * flowing through the event store reaches the wired-up callback, and non-approval events
     * are filtered out.
     */
    @Test
    fun `ApprovalRequestedEvent in store reaches coordinator via subscription`() = runBlocking {
        val liveFlow = MutableSharedFlow<StoredEvent>(extraBufferCapacity = 16)
        val store = fakeEventStore(liveFlow)

        val triggered = CompletableDeferred<ApprovalRequestedEvent>()
        // Mirror the exact wiring used in ServerModule.start().
        val subscription = store.subscribeAll()
            .filter { it.payload is ApprovalRequestedEvent }
            .onEach { triggered.complete(it.payload as ApprovalRequestedEvent) }
            .launchIn(scope)

        // Wait for subscription to attach so emit isn't dropped by the SharedFlow (replay=0).
        while (liveFlow.subscriptionCount.value == 0) {
            yield()
        }

        val event = ApprovalRequestedEvent(
            requestId = requestId,
            tier = Tier.T2,
            validationReportId = ValidationReportId("vr-1"),
            riskSummaryId = RiskSummaryId("rs-1"),
            sessionId = sessionId,
            stageId = null,
            projectId = null,
        )
        // Non-approval event must be filtered out.
        liveFlow.emit(storedEvent(WorkflowStartedEvent(sessionId, "wf", StageId("stage-x")), seq = 1L))
        liveFlow.emit(storedEvent(event, seq = 2L))

        val received = withTimeout(2_000L) { triggered.await() }
        assertEquals(requestId, received.requestId)
        subscription.cancel()
    }

    @Test
    fun `lookupSession returns mapping registered by onApprovalRequested and cleared by handleResponse`(): Unit = runBlocking {
        val gateway = RecordingGateway()
        val coord = ApprovalCoordinator(gateway)
        val otherSessionId = SessionId("other-session")

        // Initially no mapping
        assertNull(coord.lookupSession(requestId))

        // After onApprovalRequested, lookupSession returns the registered sessionId
        val event = ApprovalRequestedEvent(
            requestId = requestId,
            tier = Tier.T2,
            validationReportId = ValidationReportId("vr-1"),
            riskSummaryId = null,
            sessionId = otherSessionId,
            stageId = null,
            projectId = null,
        )
        coord.onApprovalRequested(event)
        assertEquals(otherSessionId, coord.lookupSession(requestId))

        // After handleResponse resolves the request, the mapping is cleaned up
        val msg = ClientMessage.ApprovalResponse(
            requestId = requestId,
            decision = ApprovalDecision.APPROVE,
            steeringNote = null,
        )
        coord.handleResponse(msg, otherSessionId)
        assertNull(coord.lookupSession(requestId))
    }

    @Test
    fun `onApprovalRequested with non-null riskSummary registers session without error`(): Unit = runBlocking {
        val gateway = RecordingGateway()
        val coord = ApprovalCoordinator(gateway)
        val riskSummary = RiskSummary(
            level = RiskLevel.HIGH,
            signals = listOf(RiskSignal.ValidationErrors(errorCount = 2)),
            recommendedAction = RiskAction.PROMPT_USER,
            rationale = listOf("[ERR001] Schema mismatch"),
        )
        val event = ApprovalRequestedEvent(
            requestId = requestId,
            tier = Tier.T3,
            validationReportId = ValidationReportId("vr-1"),
            riskSummaryId = null,
            riskSummary = riskSummary,
            sessionId = sessionId,
            stageId = null,
            projectId = null,
        )
        coord.onApprovalRequested(event)
        assertEquals(sessionId, coord.lookupSession(requestId))
    }

    /**
     * Approval gates block indefinitely — no auto-reject after a delay.
     * An unanswered request must not cause any decision to be submitted to the gateway.
     */
    @Test
    fun `unanswered approval request does not auto-reject after a delay`(): Unit = runBlocking {
        val gateway = RecordingGateway()
        val coord = ApprovalCoordinator(gateway)
        val event = ApprovalRequestedEvent(
            requestId = requestId,
            tier = Tier.T2,
            validationReportId = ValidationReportId("vr-1"),
            riskSummaryId = null,
            sessionId = sessionId,
            stageId = null,
            projectId = null,
        )

        coord.onApprovalRequested(event)
        // Give any hypothetical background job time to fire.
        delay(200L)

        assertTrue(gateway.submissions.isEmpty(), "No decision should be submitted for an unanswered approval")
        // The session mapping must still be present — the request is still pending.
        assertEquals(sessionId, coord.lookupSession(requestId))
    }
}
