package com.correx.core.kernel.orchestration

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class ParseToolArgumentsTest {

    @Test
    fun `a string array becomes a List so list-param accessors read it`() {
        // Regression: arrays were stringified and read back empty, dropping a task's affected_paths.
        val params = parseToolArguments("""{"affected_paths":["apps/web/**","build.gradle"]}""")
        assertEquals(listOf("apps/web/**", "build.gradle"), params["affected_paths"])
    }

    @Test
    fun `primitives become their content strings`() {
        val params = parseToolArguments("""{"project":"webui","force":true,"limit":5}""")
        assertEquals("webui", params["project"])
        assertEquals("true", params["force"])
        assertEquals("5", params["limit"])
    }

    @Test
    fun `objects and arrays-of-objects are kept as JSON text for tools that re-parse them`() {
        val params = parseToolArguments(
            """{"parent":{"title":"Epic"},"tasks":[{"title":"A"},{"title":"B"}]}""",
        )
        assertTrue((params["parent"] as String).contains("\"title\""))
        // re-parseable JSON, not a Kotlin List#toString
        assertTrue((params["tasks"] as String).trim().startsWith("["))
        assertTrue((params["tasks"] as String).contains("\"title\":\"A\""))
    }

    @Test
    fun `malformed json yields an empty map`() {
        assertTrue(parseToolArguments("not json").isEmpty())
        assertTrue(parseToolArguments("[]").isEmpty()) // top-level non-object
    }

    @Test
    fun `lenient fallback recovers unquoted keys from gemma drift`() {
        // Small local models drift out of strict JSON on large args, most commonly bare keys.
        // Strict parse fails; the lenient fallback still yields both params so file_write no
        // longer sees an empty map and lies "Missing 'path'".
        val params = parseToolArguments("""{path: "styles/x.css", content: "body {}"}""")
        assertEquals("styles/x.css", params["path"])
        assertEquals("body {}", params["content"])
    }

    @Test
    fun `decompose preview renders the epic and children with dependency labels`() {
        // Args arrive flattened: nested tasks/parent are JSON text, as in a real call.
        val preview = renderDecomposePreview(
            mapOf(
                "project" to "webui",
                "parent" to """{"title":"Frontend web UI"}""",
                "tasks" to """
                    [
                      {"ref":"scaffold","title":"Scaffold app"},
                      {"title":"Auth view","depends_on":["scaffold"]},
                      {"title":"Dashboard","depends_on":["0"]}
                    ]
                """.trimIndent(),
            ),
        )!!
        assertTrue(preview.contains("epic: Frontend web UI"))
        assertTrue(preview.contains("1. Scaffold app"))
        // dependency labels resolve a ref and a numeric index back to the dependency's title.
        assertTrue(preview.contains("2. Auth view  (after: Scaffold app)"))
        assertTrue(preview.contains("3. Dashboard  (after: Scaffold app)"))
    }

    @Test
    fun `decompose preview is null when tasks are unparseable so the caller falls back`() {
        assertNull(renderDecomposePreview(mapOf("project" to "webui", "tasks" to "oops")))
        assertNull(renderDecomposePreview(mapOf("tasks" to "[]")))
    }
}
