package com.correx.infrastructure.tools.shell

import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.compression.ToolOutputContext
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.contract.ValidationResult
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.util.*

class ShellToolTest {

    private val sessionId = SessionId(UUID.randomUUID().toString())
    private val stageId = StageId(UUID.randomUUID().toString())
    private val invocationId = ToolInvocationId(UUID.randomUUID().toString())

    private fun createRequest(argv: List<String>): ToolRequest {
        return ToolRequest(
            invocationId = invocationId,
            sessionId = sessionId,
            stageId = stageId,
            toolName = "shell",
            parameters = mapOf("argv" to argv),
        )
    }

    @Test
    fun `validateRequest returns Valid for allowed executable`(): Unit = runBlocking {
        val tool = ShellTool(allowedExecutables = setOf("echo"))
        val request = createRequest(listOf("echo", "hello"))
        assertEquals(ValidationResult.Valid, tool.validateRequest(request))
    }

    @Test
    fun `validateRequest returns Invalid for disallowed executable`(): Unit = runBlocking {
        val tool = ShellTool(allowedExecutables = setOf("echo"))
        val request = createRequest(listOf("ls"))
        val result = tool.validateRequest(request)
        assertTrue(result is ValidationResult.Invalid)
        assertEquals("Executable 'ls' is not in the allowed list.", (result as ValidationResult.Invalid).reason)
    }

    @Test
    fun `validateRequest returns Invalid for empty argv`(): Unit = runBlocking {
        val tool = ShellTool(allowedExecutables = setOf("echo"))
        val request = createRequest(emptyList())
        val result = tool.validateRequest(request)
        assertTrue(result is ValidationResult.Invalid)
        assertEquals(
            "Missing or empty 'argv' parameter. Expected List<String>.",
            (result as ValidationResult.Invalid).reason,
        )
    }

    @Test
    fun `validateRequest returns Invalid for null argv`(): Unit = runBlocking {
        val tool = ShellTool(allowedExecutables = setOf("echo"))
        val request = ToolRequest(
            invocationId = invocationId,
            sessionId = sessionId,
            stageId = stageId,
            toolName = "shell",
            parameters = emptyMap(),
        )
        val result = tool.validateRequest(request)
        assertTrue(result is ValidationResult.Invalid)
        assertEquals(
            "Missing or empty 'argv' parameter. Expected List<String>.",
            (result as ValidationResult.Invalid).reason,
        )
    }

    @Test
    fun `execute returns Success for exit code 0`(): Unit = runBlocking {
        val tool = ShellTool(allowedExecutables = setOf("echo"))
        val request = createRequest(listOf("echo", "hello"))
        val result = tool.execute(request)

        assertTrue(result is ToolResult.Success)
        val success = result as ToolResult.Success
        assertTrue(success.output.startsWith("hello"))
        assertEquals(0, success.exitCode)
        assertEquals(invocationId, success.invocationId)
    }

    @Test
    fun `execute returns Failure with non-zero exit code for failed process`(): Unit = runBlocking {
        val tool = ShellTool(allowedExecutables = setOf("false"))
        val request = createRequest(listOf("false"))
        val result = tool.execute(request)

        assertTrue(result is ToolResult.Failure)
        val failure = result as ToolResult.Failure
        assertTrue(failure.reason.contains("exited with code 1"))
        assertEquals(invocationId, failure.invocationId)
    }

    @Test
    fun `execute returns Failure on timeout`(): Unit = runBlocking {
        // Using sleep command to simulate timeout
        val tool = ShellTool(allowedExecutables = setOf("sleep"), timeoutMs = 100L)
        val request = createRequest(listOf("sleep", "1"))
        val result = tool.execute(request)

        assertTrue(result is ToolResult.Failure)
        val failure = result as ToolResult.Failure
        assertEquals("Process timed out after 100ms", failure.reason)
        assertFalse(failure.recoverable)
    }

    @Test
    fun `outputCompressor strips blanks and head-tail elides long output`() {
        val tool = ShellTool()
        val body = (1..100).joinToString("\n") { "line$it" }
        val raw = "\n$body\n\n"
        val compressed = tool.outputCompressor.compress(raw, ToolOutputContext(exitCode = 0))
        val lines = compressed.split("\n")
        assertEquals("line1", lines.first())
        assertEquals("line100", lines.last())
        assertEquals(81, lines.size) // 40 head + 1 marker + 40 tail
        assertTrue(compressed.contains("lines elided"))
    }

    @Test
    fun `outputCompressor passes raw through on non-zero exit`() {
        val tool = ShellTool()
        val raw = "a\n\nb\n\nc"
        val compressed = tool.outputCompressor.compress(raw, ToolOutputContext(exitCode = 1))
        assertEquals(raw, compressed)
    }
}
