package com.correx.apps.server.health

import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.SessionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class EventStoreHealthProbeTest {

    // ---------------------------------------------------------------------------
    // Fake store helpers
    // ---------------------------------------------------------------------------

    /** Minimal EventStore that returns a fixed global sequence or throws on demand. */
    private class FakeEventStore(
        private val globalSequence: Long = 0L,
        private val throwOnRead: Boolean = false,
    ) : 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? = null
        override fun subscribe(sessionId: SessionId): Flow<StoredEvent> = emptyFlow()
        override fun subscribeAll(): Flow<StoredEvent> = emptyFlow()
        override suspend fun lastGlobalSequence(): Long {
            if (throwOnRead) error("store unavailable")
            return globalSequence
        }
        override fun allEvents(): Sequence<StoredEvent> = emptySequence()
        override fun allSessionIds(): Set<SessionId> = emptySet()
    }

    /**
     * Fake nano-time source that reports [elapsedMs] milliseconds between the first and
     * second call — deterministically, with no real sleep. The first call (start) returns 0;
     * the second call (after the read) returns [elapsedMs] * NS_PER_MS.
     */
    private fun fakeClockMs(elapsedMs: Long): () -> Long {
        val ns = elapsedMs * 1_000_000L
        var call = 0
        return { if (call++ == 0) 0L else ns }
    }

    private fun probe(
        store: EventStore = FakeEventStore(),
        latencyWarnMs: Long = 500,
        nanoTimeSource: () -> Long = fakeClockMs(0L),
    ) = EventStoreHealthProbe(
        eventStore = store,
        latencyWarnMs = latencyWarnMs,
        nanoTimeSource = nanoTimeSource,
    )

    // ---------------------------------------------------------------------------
    // Tests
    // ---------------------------------------------------------------------------

    @Test
    fun `healthy when read completes within threshold`(): Unit = runBlocking {
        val obs = probe(
            store = FakeEventStore(globalSequence = 42L),
            latencyWarnMs = 500,
            nanoTimeSource = fakeClockMs(elapsedMs = 10),
        ).observe()

        assertEquals(HealthStatus.HEALTHY, obs.status)
        assertEquals("read_latency_ms", obs.metric)
        assertEquals(10L, obs.observedValue)
        assertEquals(500L, obs.threshold)
        assertTrue(obs.detail.contains("responsive"), "detail should mention 'responsive': ${obs.detail}")
    }

    @Test
    fun `degraded when read throws`(): Unit = runBlocking {
        val obs = probe(
            store = FakeEventStore(throwOnRead = true),
            latencyWarnMs = 500,
            nanoTimeSource = fakeClockMs(elapsedMs = 1),
        ).observe()

        assertEquals(HealthStatus.DEGRADED, obs.status)
        assertEquals("read_latency_ms", obs.metric)
        assertTrue(obs.detail.contains("threw"), "detail should mention 'threw': ${obs.detail}")
        assertTrue(obs.detail.contains("store unavailable"), "detail should contain error message: ${obs.detail}")
    }

    @Test
    fun `degraded when read exceeds latency threshold`(): Unit = runBlocking {
        val obs = probe(
            store = FakeEventStore(globalSequence = 0L),
            latencyWarnMs = 500,
            nanoTimeSource = fakeClockMs(elapsedMs = 600),
        ).observe()

        assertEquals(HealthStatus.DEGRADED, obs.status)
        assertEquals("read_latency_ms", obs.metric)
        assertEquals(600L, obs.observedValue)
        assertEquals(500L, obs.threshold)
        assertTrue(obs.detail.contains("600ms"), "detail should contain elapsed ms: ${obs.detail}")
        assertTrue(obs.detail.contains("500ms"), "detail should contain threshold: ${obs.detail}")
    }

    @Test
    fun `healthy when read latency equals threshold minus one`(): Unit = runBlocking {
        // Boundary: exactly one ms under the threshold is still HEALTHY
        val obs = probe(
            store = FakeEventStore(globalSequence = 1L),
            latencyWarnMs = 500,
            nanoTimeSource = fakeClockMs(elapsedMs = 499),
        ).observe()

        assertEquals(HealthStatus.HEALTHY, obs.status)
        assertEquals(499L, obs.observedValue)
    }

    @Test
    fun `degraded when read latency equals threshold exactly`(): Unit = runBlocking {
        // Boundary: exactly at the threshold trips DEGRADED (>= check)
        val obs = probe(
            store = FakeEventStore(globalSequence = 1L),
            latencyWarnMs = 500,
            nanoTimeSource = fakeClockMs(elapsedMs = 500),
        ).observe()

        assertEquals(HealthStatus.DEGRADED, obs.status)
        assertEquals(500L, obs.observedValue)
    }
}
