package com.correx.infrastructure.inference.llama.cpp

import com.correx.core.events.EventDispatcher
import com.correx.core.events.events.ModelLoadedEvent
import com.correx.core.events.events.ModelUnloadedEvent
import com.correx.infrastructure.inference.commons.ModelDescriptor
import com.correx.infrastructure.inference.commons.ModelLoadException
import com.correx.infrastructure.inference.commons.ResidencyMode
import com.correx.infrastructure.persistence.InMemoryEventStore
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.headersOf
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull

/**
 * Tests for [DefaultModelManager] lifecycle using a stub process and a mock HTTP health endpoint.
 *
 * Stub process: uses the system "sleep" binary called with llama-server-style args.
 * The process may exit quickly (args are not understood by sleep), but the mock HTTP
 * client returns "healthy" before the manager ever checks process liveness, so load()
 * succeeds.  This approach intentionally avoids any dependency on a real llama-server
 * binary.
 */
class DefaultModelManagerTest {

    private fun healthyHttpClient(): HttpClient {
        val engine = MockEngine {
            respond(
                content = """{"status":"healthy"}""",
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
            )
        }
        return HttpClient(engine)
    }

    private fun unhealthyHttpClient(): HttpClient {
        val engine = MockEngine {
            respond(
                content = """{"status":"loading"}""",
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
            )
        }
        return HttpClient(engine)
    }

    private fun descriptor(id: String = "test-model") = ModelDescriptor(
        modelId = id,
        modelPath = "/dev/null",
        residencyMode = ResidencyMode.PERSISTENT,
        contextSize = 512,
    )

    @Test
    fun `load spawns process, polls health, emits ModelLoadedEvent, returns provider`(): Unit = runBlocking {
        val eventStore = InMemoryEventStore()
        val dispatcher = EventDispatcher(eventStore)
        val manager = DefaultModelManager(
            llamaServerBin = "sleep",
            host = "127.0.0.1",
            port = 29999,
            healthTimeoutMs = 5_000L,
            eventStore = eventStore,
            httpClient = healthyHttpClient(),
            eventDispatcher = dispatcher,
        )

        val provider = manager.load(descriptor())

        assertNotNull(provider)
        assertEquals("test-model", manager.currentModel()?.modelId)

        val events = eventStore.allEvents().toList()
        val loadedEvent = events.firstOrNull { it.payload is ModelLoadedEvent }
        assertNotNull(loadedEvent, "ModelLoadedEvent must have been emitted")
        assertEquals("test-model", (loadedEvent!!.payload as ModelLoadedEvent).modelId)
        assertEquals("correx:model-manager", loadedEvent.metadata.sessionId.value)

        manager.unload("test-model")
    }

    @Test
    fun `currentModel returns null before any load`() {
        val eventStore = InMemoryEventStore()
        val dispatcher = EventDispatcher(eventStore)
        val manager = DefaultModelManager(
            llamaServerBin = "sleep",
            host = "127.0.0.1",
            port = 29998,
            healthTimeoutMs = 5_000L,
            eventStore = eventStore,
            httpClient = healthyHttpClient(),
            eventDispatcher = dispatcher,
        )

        assertNull(manager.currentModel())
    }

    @Test
    fun `loading same model twice returns provider without re-spawning`(): Unit = runBlocking {
        val eventStore = InMemoryEventStore()
        val dispatcher = EventDispatcher(eventStore)
        val manager = DefaultModelManager(
            llamaServerBin = "sleep",
            host = "127.0.0.1",
            port = 29997,
            healthTimeoutMs = 5_000L,
            eventStore = eventStore,
            httpClient = healthyHttpClient(),
            eventDispatcher = dispatcher,
        )

        manager.load(descriptor("model-a"))
        manager.load(descriptor("model-a"))

        // Only one ModelLoadedEvent because the second load is a no-op
        val loadedEvents = eventStore.allEvents().filter { it.payload is ModelLoadedEvent }.toList()
        assertEquals(1, loadedEvents.size)

        manager.unload("model-a")
    }

    @Test
    fun `unload emits ModelUnloadedEvent and clears currentModel`(): Unit = runBlocking {
        val eventStore = InMemoryEventStore()
        val dispatcher = EventDispatcher(eventStore)
        val manager = DefaultModelManager(
            llamaServerBin = "sleep",
            host = "127.0.0.1",
            port = 29996,
            healthTimeoutMs = 5_000L,
            eventStore = eventStore,
            httpClient = healthyHttpClient(),
            eventDispatcher = dispatcher,
        )

        manager.load(descriptor("model-b"))
        manager.unload("model-b")

        assertNull(manager.currentModel())

        val unloadedEvents = eventStore.allEvents().filter { it.payload is ModelUnloadedEvent }.toList()
        assertEquals(1, unloadedEvents.size)
        assertEquals("model-b", (unloadedEvents[0].payload as ModelUnloadedEvent).modelId)
        assertEquals("correx:model-manager", unloadedEvents[0].metadata.sessionId.value)
    }

    @Test
    fun `unload without prior load throws ModelLoadException`(): Unit = runBlocking {
        val eventStore = InMemoryEventStore()
        val dispatcher = EventDispatcher(eventStore)
        val manager = DefaultModelManager(
            llamaServerBin = "sleep",
            host = "127.0.0.1",
            port = 29995,
            healthTimeoutMs = 5_000L,
            eventStore = eventStore,
            httpClient = healthyHttpClient(),
            eventDispatcher = dispatcher,
        )

        assertThrows<ModelLoadException> { runBlocking { manager.unload("nonexistent") } }
    }

    @Test
    fun `load throws ModelLoadException on health timeout`(): Unit = runBlocking {
        val eventStore = InMemoryEventStore()
        val dispatcher = EventDispatcher(eventStore)
        val manager = DefaultModelManager(
            llamaServerBin = "sleep",
            host = "127.0.0.1",
            port = 29994,
            healthTimeoutMs = 100L,
            eventStore = eventStore,
            httpClient = unhealthyHttpClient(),
            eventDispatcher = dispatcher,
        )

        assertThrows<ModelLoadException> { runBlocking { manager.load(descriptor("timeout-model")) } }
    }
}
