package com.correx.testing.contracts.fixtures.events.store

import com.correx.core.events.events.StoredEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.testing.contracts.utils.ConcurrencyRunner
import com.correx.testing.fixtures.EventFixtures
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

abstract class EventStoreContractTest {
    protected abstract fun store(): EventStore

    @Test
    fun `appends preserve ordering`(): Unit = runBlocking {
        val store = store()

        store.append(EventFixtures.newEvent(EventId("e1"), SessionId("s1")))
        store.append(EventFixtures.newEvent(EventId("e2"), SessionId("s1")))

        val events = store.read(SessionId("s1"))

        Assertions.assertEquals(2, events.size)
        Assertions.assertEquals("e1", events[0].metadata.eventId.value)
        Assertions.assertEquals("e2", events[1].metadata.eventId.value)
    }

    @Test
    fun `idempotency prevents duplicates`(): Unit = runBlocking {
        val store = store()
        val e = EventFixtures.newEvent(EventId("dup"), SessionId("s1"))

        store.append(e)
        assertThrows<Exception> {
            store.append(e)
        }
        Assertions.assertEquals(1, store.read(SessionId("s1")).size)
    }

    @Test
    fun `sessions are isolated`(): Unit = runBlocking {
        val store = store()

        store.append(EventFixtures.newEvent(EventId("a"), SessionId("s1")))
        store.append(EventFixtures.newEvent(EventId("b"), SessionId("s2")))

        Assertions.assertEquals(1, store.read(SessionId("s1")).size)
        Assertions.assertEquals(1, store.read(SessionId("s2")).size)
    }

    @Test
    fun `readFrom respects sequence cursor`(): Unit = runBlocking {
        val store = store()

        store.append(EventFixtures.newEvent(EventId("1"), SessionId("s1")))
        store.append(EventFixtures.newEvent(EventId("2"), SessionId("s1")))
        store.append(EventFixtures.newEvent(EventId("3"), SessionId("s1")))

        val partial = store.readFrom(SessionId("s1"), 2)

        Assertions.assertEquals(1, partial.size)
    }

    @Test
    fun `event ids are globally unique under concurrent appends`() {
        val store = store()

        ConcurrencyRunner.run(20, 100) { t, i ->
            runBlocking {
                store.append(
                    EventFixtures.newEvent(EventId("e-$i-$t"), SessionId("s1")),
                )
            }
        }

        val all = store.read(SessionId("s1"))

        val ids = all.map { it.metadata.eventId }.toSet()

        Assertions.assertEquals(all.size, ids.size)
    }

    @Test
    fun `sessionSequences are strictly increasing per session`(): Unit = runBlocking {
        val store = store()

        repeat(100) {
            store.append(EventFixtures.newEvent(EventId("e$it"), SessionId("s1")))
        }

        val sessionSeqs = store.read(SessionId("s1")).map { it.sessionSequence }
        sessionSeqs.zipWithNext().forEach { (a, b) -> Assertions.assertTrue(a < b) }

        val globalSeqs = store.read(SessionId("s1")).map { it.sequence }
        globalSeqs.zipWithNext().forEach { (a, b) -> Assertions.assertTrue(a < b) }
    }

    @Test
    fun `sessionSequences are isolated per session and global sequence spans all`(): Unit = runBlocking {
        val store = store()

        repeat(50) {
            store.append(EventFixtures.newEvent(EventId("a$it"), SessionId("A")))
            store.append(EventFixtures.newEvent(EventId("b$it"), SessionId("B")))
        }

        val aSessionSeq = store.read(SessionId("A")).map { it.sessionSequence }
        val bSessionSeq = store.read(SessionId("B")).map { it.sessionSequence }

        Assertions.assertEquals(aSessionSeq.sorted(), aSessionSeq)
        Assertions.assertEquals(bSessionSeq.sorted(), bSessionSeq)
        Assertions.assertEquals(1L, aSessionSeq.first())
        Assertions.assertEquals(1L, bSessionSeq.first())

        val allGlobal = (store.read(SessionId("A")) + store.read(SessionId("B")))
            .map { it.sequence }.sorted()
        allGlobal.zipWithNext().forEach { (a, b) -> Assertions.assertTrue(a < b) }
    }

    @Test
    fun `appendAll is atomic per session`(): Unit = runBlocking {
        val store = store()

        val batch = (1..50).map {
            EventFixtures.newEvent(EventId("e$it"), SessionId("s1"))
        }

        store.appendAll(batch)

        val result = store.read(SessionId("s1"))

        Assertions.assertEquals(50, result.size)

        val seqs = result.map { it.sessionSequence }
        Assertions.assertEquals(seqs.sorted(), seqs)
    }

    @Test
    fun `concurrent appendAll and append remain consistent`() {
        val store = store()

        ConcurrencyRunner.run(10, 50) { t, i ->
            runBlocking {
                if (i % 2 == 0) {
                    store.append(EventFixtures.newEvent(EventId("a-$t-$i"), SessionId("s1")))
                } else {
                    store.appendAll(
                        listOf(EventFixtures.newEvent(EventId("b-$t-$i"), SessionId("s1"))),
                    )
                }
            }
        }

        val all = store.read(SessionId("s1"))

        val seqs = all.map { it.sessionSequence }

        Assertions.assertEquals(seqs.sorted(), seqs)
    }

    @Test
    fun `read returns stable snapshot`(): Unit = runBlocking {
        val store = store()

        repeat(100) {
            store.append(EventFixtures.newEvent(EventId("e$it"), SessionId("s1")))
        }

        val r1 = store.read(SessionId("s1"))
        val r2 = store.read(SessionId("s1"))

        Assertions.assertEquals(r1, r2)
    }

    @Test
    fun `readFrom behaves like streaming cursor`(): Unit = runBlocking {
        val store = store()

        repeat(10) {
            store.append(EventFixtures.newEvent(EventId("$it"), SessionId("s1")))
        }

        val first = store.readFrom(SessionId("s1"), 3)
        val second = store.readFrom(SessionId("s1"), 7)

        Assertions.assertTrue(first.all { it.sessionSequence > 3 })
        Assertions.assertTrue(second.all { it.sessionSequence > 7 })
    }

    @Test
    fun `lastGlobalSequence returns 0 when store is empty`(): Unit = runBlocking {
        val store = store()
        Assertions.assertEquals(0L, store.lastGlobalSequence())
    }

    @Test
    fun `lastGlobalSequence equals highest sequence after appends`(): Unit = runBlocking {
        val store = store()

        store.append(EventFixtures.newEvent(EventId("x1"), SessionId("s1")))
        store.append(EventFixtures.newEvent(EventId("x2"), SessionId("s2")))
        store.append(EventFixtures.newEvent(EventId("x3"), SessionId("s1")))

        val allSeqs = (store.read(SessionId("s1")) + store.read(SessionId("s2")))
            .map { it.sequence }

        Assertions.assertEquals(allSeqs.max(), store.lastGlobalSequence())
    }

    @Test
    fun `subscribeAll emits events from all sessions`(): Unit = runBlocking {
        val store = store()
        val received = mutableListOf<StoredEvent>()

        val job = launch {
            store.subscribeAll().collect { received.add(it) }
        }
        yield()

        store.append(EventFixtures.newEvent(EventId("p1"), SessionId("alpha")))
        store.append(EventFixtures.newEvent(EventId("q1"), SessionId("beta")))
        delay(100)
        job.cancel()

        Assertions.assertEquals(2, received.size)
        val sessionIds = received.map { it.metadata.sessionId.value }.toSet()
        Assertions.assertTrue("alpha" in sessionIds)
        Assertions.assertTrue("beta" in sessionIds)
    }
}
