package com.correx.core.tools.compression

import com.correx.core.tools.compression.CompressionRule.CollapseConsecutiveDuplicates
import com.correx.core.tools.compression.CompressionRule.DropMatching
import com.correx.core.tools.compression.CompressionRule.HeadTail
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
import com.correx.core.tools.compression.CompressionRule.StripLeadingWhitespace
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

class DeclarativeCompressorTest {

    private fun compress(spec: OutputCompressionSpec, raw: String, exitCode: Int = 0): String =
        DeclarativeCompressor(spec).compress(raw, ToolOutputContext(exitCode))

    @Test
    fun `StripBlankLines drops empty and whitespace-only lines`() {
        val out = compress(OutputCompressionSpec(listOf(StripBlankLines)), "a\n\n  \nb\n\t\nc")
        assertEquals("a\nb\nc", out)
    }

    @Test
    fun `StripLeadingWhitespace trims leading whitespace per line`() {
        val out = compress(OutputCompressionSpec(listOf(StripLeadingWhitespace)), "  a\n\tb\n   c")
        assertEquals("a\nb\nc", out)
    }

    @Test
    fun `DropMatching drops lines matching the pattern but keeps non-matching errors`() {
        val out = compress(
            OutputCompressionSpec(listOf(DropMatching("^Progress: "))),
            "Progress: 10%\nERROR: boom\nProgress: 50%\ndone",
        )
        assertEquals("ERROR: boom\ndone", out)
    }

    @Test
    fun `HeadTail elides the middle once line count exceeds head plus tail`() {
        val raw = (1..10).joinToString("\n") { "line$it" }
        val out = compress(OutputCompressionSpec(listOf(HeadTail(2, 2))), raw)
        assertEquals("line1\nline2\n… 6 lines elided …\nline9\nline10", out)
    }

    @Test
    fun `HeadTail with salience retains a matching middle line otherwise elided`() {
        val lines = (1..20).map { if (it == 10) "boom: ERROR occurred" else "line$it" }
        val out = compress(OutputCompressionSpec(listOf(HeadTail(2, 2, salience = "error"))), lines.joinToString("\n"))
        assertEquals(
            "line1\nline2\n… 7 lines elided …\nboom: ERROR occurred\n… 8 lines elided …\nline19\nline20",
            out,
        )
    }

    @Test
    fun `HeadTail with salience caps the number of retained salient lines`() {
        val lines = (1..20).map { "error$it" }
        val out = compress(OutputCompressionSpec(listOf(HeadTail(0, 0, salience = "error", salienceCap = 3))), lines.joinToString("\n"))
        assertEquals("error1\nerror2\nerror3\n… 17 lines elided …", out)
    }

    @Test
    fun `HeadTail with salience preserves order and behaves like plain HeadTail when nothing matches`() {
        val raw = (1..10).joinToString("\n") { "line$it" }
        val out = compress(OutputCompressionSpec(listOf(HeadTail(2, 2, salience = "error"))), raw)
        assertEquals("line1\nline2\n… 6 lines elided …\nline9\nline10", out)
    }

    @Test
    fun `HeadTail is a no-op at or under threshold`() {
        val raw = (1..4).joinToString("\n") { "line$it" }
        val out = compress(OutputCompressionSpec(listOf(HeadTail(2, 2))), raw)
        assertEquals(raw, out)
    }

    @Test
    fun `CollapseConsecutiveDuplicates collapses runs of identical adjacent lines with a count`() {
        val raw = "Error: timeout\nError: timeout\nError: timeout\nok\nError: timeout"
        val out = compress(OutputCompressionSpec(listOf(CollapseConsecutiveDuplicates)), raw)
        assertEquals("Error: timeout (×3)\nok\nError: timeout", out)
    }

    @Test
    fun `CollapseConsecutiveDuplicates leaves singletons and non-adjacent duplicates untouched`() {
        val raw = "a\nb\na"
        val out = compress(OutputCompressionSpec(listOf(CollapseConsecutiveDuplicates)), raw)
        assertEquals(raw, out)
    }

    @Test
    fun `read_file spec strips blanks then leading whitespace`() {
        val spec = OutputCompressionSpec(listOf(StripBlankLines, StripLeadingWhitespace))
        val out = compress(spec, "  fun main() {\n\n      println()\n  }\n")
        assertEquals("fun main() {\nprintln()\n}", out)
    }

    @Test
    fun `shell spec strips blanks then applies head-tail with bypassOnError`() {
        val spec = OutputCompressionSpec(listOf(StripBlankLines, HeadTail(2, 2)), bypassOnError = true)
        val body = (1..6).joinToString("\n") { "row$it" }
        val raw = "\n$body\n\n"
        val out = compress(spec, raw)
        assertEquals("row1\nrow2\n… 2 lines elided …\nrow5\nrow6", out)
    }

    @Test
    fun `bypassOnError returns raw unchanged on non-zero exit`() {
        val spec = OutputCompressionSpec(listOf(StripBlankLines, HeadTail(1, 1)), bypassOnError = true)
        val raw = "a\n\nb\n\nc\n\nd"
        assertEquals(raw, compress(spec, raw, exitCode = 1))
    }

    @Test
    fun `bypassOnError disabled still compresses on non-zero exit`() {
        val spec = OutputCompressionSpec(listOf(StripBlankLines), bypassOnError = false)
        assertEquals("a\nb", compress(spec, "a\n\nb", exitCode = 1))
    }

    @Test
    fun `empty spec is identity`() {
        val raw = "  a\n\n  b  "
        assertEquals(raw, compress(OutputCompressionSpec(), raw))
    }

    @Test
    fun `invalid DropMatching regex throws at construction`() {
        assertFailsWith<IllegalArgumentException> {
            DeclarativeCompressor(OutputCompressionSpec(listOf(DropMatching("("))))
        }
    }

    @Test
    fun `compress never throws on adversarial input`() {
        val spec = OutputCompressionSpec(listOf(StripBlankLines, StripLeadingWhitespace, HeadTail(1, 1)))
        val out = DeclarativeCompressor(spec).compress("", ToolOutputContext(0))
        assertTrue(out.isEmpty())
    }

    @Test
    fun `spec round-trips through JSON with every rule variant (config-reachable)`() {
        val spec = OutputCompressionSpec(
            rules = listOf(
                StripBlankLines, StripLeadingWhitespace, DropMatching("^Progress: "),
                HeadTail(3, 3), CollapseConsecutiveDuplicates,
            ),
            bypassOnError = false,
        )
        val encoded = Json.encodeToString(OutputCompressionSpec.serializer(), spec)
        val decoded = Json.decodeFromString(OutputCompressionSpec.serializer(), encoded)
        assertEquals(spec, decoded)
    }
}
