package com.correx.infrastructure.tools.filesystem

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.nio.file.Files
import java.util.*

class FileReadToolTest {

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

    private fun createRequest(path: String): ToolRequest {
        return ToolRequest(
            invocationId = invocationId,
            sessionId = sessionId,
            stageId = stageId,
            toolName = "file_read",
            parameters = mapOf("path" to path),
        )
    }

    @Test
    fun `validateRequest returns Valid for allowed path`(): Unit = runBlocking {
        val tempFile = Files.createTempFile("test_allowed", ".txt")
        val tool = FileReadTool(allowedPaths = setOf(tempFile.parent))
        val request = createRequest(tempFile.toString())
        assertEquals(ValidationResult.Valid, tool.validateRequest(request))
    }

    @Test
    fun `validateRequest returns Invalid for disallowed path`(): Unit = runBlocking {
        val tempFile = Files.createTempFile("test_disallowed", ".txt")
        val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("subdir")))
        val request = createRequest(tempFile.toString())
        val result = tool.validateRequest(request)
        assertTrue(result is ValidationResult.Invalid)
        assertTrue((result as ValidationResult.Invalid).reason.contains("not in the allowed list"))
    }

    @Test
    fun `validateRequest admits an out-of-jail path carried on request grantedPaths`(): Unit = runBlocking {
        // A file the static jail does NOT cover (an unrelated temp dir), mirroring an out-of-workspace
        // reference the operator approved. Without the grant it's rejected; with it, admitted.
        val jailDir = Files.createTempDirectory("jail")
        val outside = Files.createTempFile("outside_ref", ".md")
        val tool = FileReadTool(allowedPaths = setOf(jailDir))

        val ungranted = createRequest(outside.toString())
        assertTrue(tool.validateRequest(ungranted) is ValidationResult.Invalid)

        val granted = createRequest(outside.toString()).copy(grantedPaths = setOf(outside.toString()))
        assertEquals(ValidationResult.Valid, tool.validateRequest(granted))
    }

    @Test
    fun `validateRequest returns Invalid for missing path parameter`(): Unit = runBlocking {
        val tool = FileReadTool()
        val request = ToolRequest(
            invocationId = invocationId,
            sessionId = sessionId,
            stageId = stageId,
            toolName = "file_read",
            parameters = emptyMap(),
        )
        val result = tool.validateRequest(request)
        assertTrue(result is ValidationResult.Invalid)
        assertEquals(
            "Missing or invalid 'path' parameter. Expected String.",
            (result as ValidationResult.Invalid).reason,
        )
    }

    @Test
    fun `execute returns Success for valid file`(): Unit = runBlocking {
        val content = "Hello, world!"
        val tempFile = Files.createTempFile("test_content", ".txt")
        Files.writeString(tempFile, content)

        val tool = FileReadTool(allowedPaths = setOf(tempFile.parent))
        val request = createRequest(tempFile.toString())
        val result = tool.execute(request)

        assertTrue(result is ToolResult.Success)
        val success = result as ToolResult.Success
        assertEquals(content, success.output)
        assertEquals(invocationId, success.invocationId)
    }

    @Test
    fun `execute returns Failure for non-existent file`(): Unit = runBlocking {
        val tempDir = Files.createTempDirectory("test_dir")
        val nonExistentFile = tempDir.resolve("missing.txt")

        val tool = FileReadTool(allowedPaths = setOf(nonExistentFile))
        val request = createRequest(nonExistentFile.toString())
        val result = tool.execute(request)

        assertTrue(result is ToolResult.Failure)
        val failure = result as ToolResult.Failure
        assertTrue(failure.reason.contains("File not found"))
        // A missing file is recoverable (F-009): the model often probes for files that may not
        // exist; the orchestrator feeds the miss back so it can adapt rather than failing the stage.
        assertTrue(failure.recoverable)
    }

    @Test
    fun `execute returns Failure for disallowed path`(): Unit = runBlocking {
        val tempFile = Files.createTempFile("test_disallowed_exec", ".txt")
        val tool = FileReadTool(allowedPaths = setOf(tempFile.parent.resolve("other")))
        val request = createRequest(tempFile.toString())
        val result = tool.execute(request)

        // It should fail at validation stage
        assertTrue(result is ToolResult.Failure)
        val failure = result as ToolResult.Failure
        assertTrue(failure.reason.contains("is not in the allowed list"))
    }

    @Test
    fun `execute lists directory entries sorted with sub-directory marker`(): Unit = runBlocking {
        val tempDir = Files.createTempDirectory("test_listdir")
        Files.writeString(tempDir.resolve("README.md"), "x")
        Files.writeString(tempDir.resolve("build.gradle"), "x")
        Files.createDirectory(tempDir.resolve("src"))

        val tool = FileReadTool(allowedPaths = setOf(tempDir))
        val result = tool.execute(createRequest(tempDir.toString()))

        assertTrue(result is ToolResult.Success)
        // Sorted by name; directories suffixed with '/' so the model can navigate.
        assertEquals("README.md\nbuild.gradle\nsrc/", (result as ToolResult.Success).output)
    }

    @Test
    fun `execute truncates a large read-to-EOF and points at start_line paging`(): Unit = runBlocking {
        val tempFile = Files.createTempFile("test_big", ".txt")
        Files.writeString(tempFile, (1..1000).joinToString("\n") { "line $it" })

        val tool = FileReadTool(allowedPaths = setOf(tempFile.parent))
        val result = tool.execute(createRequest(tempFile.toString()))

        assertTrue(result is ToolResult.Success)
        val success = result as ToolResult.Success
        assertTrue(success.output.startsWith("line 1\n"), "kept the head of the file")
        assertTrue(success.output.contains("truncated at 400 lines (file has 1000)"), "carries a paging note")
        assertTrue(success.output.contains("start_line/end_line"), "tells the model how to page")
        // A truncated view establishes no stale-write baseline.
        assertTrue(success.metadata["contentHash"] == null, "no whole-file hash on a truncated read")
    }

    @Test
    fun `execute honours an explicit end_line without truncating`(): Unit = runBlocking {
        val tempFile = Files.createTempFile("test_range", ".txt")
        Files.writeString(tempFile, (1..1000).joinToString("\n") { "line $it" })

        val tool = FileReadTool(allowedPaths = setOf(tempFile.parent))
        val request = ToolRequest(
            invocationId = invocationId,
            sessionId = sessionId,
            stageId = stageId,
            toolName = "file_read",
            parameters = mapOf("path" to tempFile.toString(), "start_line" to 10, "end_line" to 600),
        )
        val result = tool.execute(request)

        assertTrue(result is ToolResult.Success)
        val output = (result as ToolResult.Success).output
        assertFalse(output.contains("truncated"), "explicit range is deliberate paging, honoured in full")
        assertTrue(output.startsWith("line 10\n") && output.endsWith("line 600"))
    }

    @Test
    fun `execute records a content hash for a small whole-file read`(): Unit = runBlocking {
        val tempFile = Files.createTempFile("test_hash", ".txt")
        Files.writeString(tempFile, "short file\nsecond line")

        val tool = FileReadTool(allowedPaths = setOf(tempFile.parent))
        val result = tool.execute(createRequest(tempFile.toString()))

        assertTrue(result is ToolResult.Success)
        assertTrue((result as ToolResult.Success).metadata["contentHash"] != null)
    }

    @Test
    fun `outputCompressor strips blank lines and leading whitespace`() {
        val tool = FileReadTool()
        val raw = "  fun main() {\n\n      println()\n  }\n"
        val compressed = tool.outputCompressor.compress(raw, ToolOutputContext(exitCode = 0))
        assertEquals("fun main() {\nprintln()\n}", compressed)
    }
}
