import com.correx.core.context.builder.DefaultContextPackBuilder
import com.correx.core.context.builder.DefaultDecisionPointBuilder
import com.correx.core.context.compression.DefaultContextCompressor
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class DefaultContextPackBuilderTest {

    private val compressor = DefaultContextCompressor()
    private val builder = DefaultContextPackBuilder(compressor)
    private val decisionBuilder = DefaultDecisionPointBuilder()

    private val sessionId = SessionId("s1")
    private val stageId = StageId("stage-1")
    private val packId = ContextPackId("pack-1")

    private fun entry(id: String, layer: ContextLayer, tokens: Int) = ContextEntry(
        id = ContextEntryId(id),
        layer = layer,
        content = "content-$id",
        sourceType = "test",
        sourceId = id,
        tokenEstimate = tokens
    )

    @Test
    fun `build groups entries by layer`() {
        val entries = listOf(
            entry("a", ContextLayer.L0, 50),
            entry("b", ContextLayer.L1, 60),
            entry("c", ContextLayer.L2, 40)
        )
        val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 4000))
        assertEquals(1, pack.layers[ContextLayer.L0]?.size)
        assertEquals(1, pack.layers[ContextLayer.L1]?.size)
        assertEquals(1, pack.layers[ContextLayer.L2]?.size)
    }

    @Test
    fun `budgetUsed reflects total token estimate of retained entries`() {
        val entries = listOf(
            entry("a", ContextLayer.L0, 100),
            entry("b", ContextLayer.L1, 200)
        )
        val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 4000))
        assertEquals(300, pack.budgetUsed)
    }

    @Test
    fun `pack budgetUsed never exceeds budgetLimit`() {
        val entries = (1..20).map { entry("e$it", ContextLayer.L2, 100) }
        val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 500))
        assertTrue(pack.budgetUsed <= 500)
    }

    @Test
    fun `DecisionPointBuilder returns only L0 and L1 entries`() {
        val entries = listOf(
            entry("a", ContextLayer.L0, 50),
            entry("b", ContextLayer.L1, 60),
            entry("c", ContextLayer.L2, 40)
        )
        val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 4000))
        val decisionEntries = decisionBuilder.build(pack)
        assertTrue(decisionEntries.all { it.layer == ContextLayer.L0 || it.layer == ContextLayer.L1 })
        assertEquals(2, decisionEntries.size)
    }

    @Test
    fun `build handles empty entries list`() {
        val pack = builder.build(packId, sessionId, stageId, emptyList(), TokenBudget(limit = 1000))
        assertEquals(0, pack.budgetUsed)
        assertEquals(0, pack.compressionMetadata.entriesDropped)
        assertTrue(pack.layers.isEmpty())
    }

    @Test
    fun `steeringNote and eventHistory entries are retained even when exceeding budget`() {
        val entries = listOf(
            entry("steering", ContextLayer.L0, 500).copy(sourceType = "steeringNote"),
            entry("history", ContextLayer.L1, 600).copy(sourceType = "eventHistory"),
            entry("normal", ContextLayer.L2, 200)
        )
        val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 800))
        val allRetained = pack.layers.values.flatten()
        assertTrue(allRetained.any { it.id == ContextEntryId("steering") }, "steeringNote must be retained")
        assertTrue(allRetained.any { it.id == ContextEntryId("history") }, "eventHistory must be retained")
    }

    @Test
    fun `tool dialogue keeps chronological order despite sourceType grouping`() {
        // A multi-round tool loop: assistant call then tool result, interleaved. Compression
        // groups by sourceType internally; the builder must restore input order so the L2
        // layer reads call, result, call, result — not all calls then all results.
        val entries = listOf(
            entry("task", ContextLayer.L1, 10).copy(sourceType = "agentPrompt"),
            entry("call1", ContextLayer.L2, 10).copy(sourceType = "assistantToolCall"),
            entry("result1", ContextLayer.L2, 10).copy(sourceType = "toolResult"),
            entry("call2", ContextLayer.L2, 10).copy(sourceType = "assistantToolCall"),
            entry("result2", ContextLayer.L2, 10).copy(sourceType = "toolResult"),
        )
        val pack = builder.build(packId, sessionId, stageId, entries, TokenBudget(limit = 4000))
        val l2Ids = pack.layers[ContextLayer.L2]!!.map { it.id.value }
        assertEquals(listOf("call1", "result1", "call2", "result2"), l2Ids)
        // ordinals are stamped from input order
        assertEquals(listOf(1, 2, 3, 4), pack.layers[ContextLayer.L2]!!.map { it.ordinal })
    }
}
