package com.correx.apps.server.ws

import com.correx.core.events.events.ArtifactContentStoredEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactValidatedEvent
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.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.UUID

class StreamQueriesPlanArtifactsTest {

    private val sessionId = SessionId("s1")
    private var seq = 0L

    private fun event(payload: EventPayload): StoredEvent = StoredEvent(
        metadata = EventMetadata(
            eventId = EventId(UUID.randomUUID().toString()),
            sessionId = sessionId,
            timestamp = Clock.System.now(),
            schemaVersion = 1,
            causationId = null,
            correlationId = null,
        ),
        sequence = seq++,
        sessionSequence = seq,
        payload = payload,
    )

    // Regression: ArtifactContentStored is emitted at inference time, BEFORE ArtifactCreated (which
    // fires at validation). The fold must still capture the content hash — folding only on creation
    // dropped it and blanked every artifact in the viewer.
    @Test
    fun `captures content hash even when ArtifactContentStored precedes ArtifactCreated`() {
        val hash = ArtifactId("83ef7566f2d82b5e")
        val events = listOf(
            event(ArtifactContentStoredEvent(ArtifactId("analysis"), hash, sessionId, StageId("analyst"))),
            event(ArtifactCreatedEvent(ArtifactId("analysis"), sessionId, StageId("analyst"), schemaVersion = 1)),
            event(ArtifactValidatedEvent(ArtifactId("analysis"), sessionId, StageId("analyst"))),
        )

        val plans = StreamQueries.planArtifacts(events)

        assertEquals(1, plans.size)
        val plan = plans.single()
        assertEquals("analysis", plan.artifactId)
        assertEquals(hash, plan.contentHash)
        assertEquals("VALIDATED", plan.phase)
        assertEquals("analyst", plan.stageId)
    }

    @Test
    fun `dereferenceEnvelope returns inner content when envelope is valid`(): Unit = runBlocking {
        val innerHash = ArtifactId("abc123")
        val envelope = """{"path":"/src/Foo.kt","contentHash":"abc123","size":42,"mode":"0644"}"""
        val innerContent = "fun foo() = 42"
        val result = StreamQueries.dereferenceEnvelope(
            json = Json { ignoreUnknownKeys = true; isLenient = true },
            rawContent = envelope,
        ) { id -> if (id == innerHash) innerContent else null }
        assertEquals(innerContent, result)
    }

    @Test
    fun `dereferenceEnvelope returns null when inner hash is missing from CAS`(): Unit = runBlocking {
        val envelope = """{"path":"/src/Foo.kt","contentHash":"missing","size":42,"mode":"0644"}"""
        val result = StreamQueries.dereferenceEnvelope(
            json = Json { ignoreUnknownKeys = true; isLenient = true },
            rawContent = envelope,
        ) { null }
        assertEquals(null, result)
    }

    @Test
    fun `dereferenceEnvelope returns null when path is a nested object`(): Unit = runBlocking {
        val envelope = """{"path":{"nested":"object"},"contentHash":"abc123"}"""
        val result = StreamQueries.dereferenceEnvelope(
            json = Json { ignoreUnknownKeys = true; isLenient = true },
            rawContent = envelope,
        ) { id -> if (id.value == "abc123") "content" else null }
        assertEquals(null, result)
    }
}
