package com.correx.infrastructure.tools

import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.EventDispatcher
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ArtifactId
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.contract.Tool
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.registry.ToolRegistry
import com.correx.infrastructure.tools.filesystem.FileWriteTool
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.nio.file.Files

class SandboxedToolExecutorFileMutationTest {

    /** Content-addressed fake: identical bytes → identical id (mirrors CAS dedup). */
    private class FakeArtifactStore : ArtifactStore {
        val blobs = linkedMapOf<String, ByteArray>()
        override suspend fun put(bytes: ByteArray): ArtifactId {
            val id = ArtifactId(bytes.toString(Charsets.UTF_8).hashCode().toString())
            blobs[id.value] = bytes
            return id
        }
        override suspend fun get(id: ArtifactId): ByteArray? = blobs[id.value]
        override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
    }

    private class CapturingEventStore : EventStore {
        val payloads = mutableListOf<EventPayload>()
        override suspend fun append(event: NewEvent): StoredEvent {
            payloads += event.payload
            val seq = payloads.size.toLong()
            return StoredEvent(event.metadata, seq, seq, event.payload)
        }
        override suspend fun appendAll(events: List<NewEvent>): List<StoredEvent> = events.map { append(it) }
        override fun read(sessionId: SessionId): List<StoredEvent> = emptyList()
        override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> = emptyList()
        override fun lastSequence(sessionId: SessionId): Long? = null
        override fun subscribe(sessionId: SessionId): Flow<StoredEvent> = emptyFlow()
        override fun subscribeAll(): Flow<StoredEvent> = emptyFlow()
        override suspend fun lastGlobalSequence(): Long = payloads.size.toLong()
        override fun allEvents(): Sequence<StoredEvent> = emptySequence()
        override fun allSessionIds(): Set<SessionId> = emptySet()
    }

    private class SingleToolRegistry(private val tool: Tool) : ToolRegistry {
        override fun resolve(name: String): Tool? = if (name == tool.name) tool else null
        override fun all(): List<Tool> = listOf(tool)
    }

    private fun writeRequest(path: String, content: String) = ToolRequest(
        ToolInvocationId("inv"), SessionId("s"), StageId("st"), "file_write",
        mapOf("path" to path, "content" to content),
    )

    private fun executor(tool: FileWriteTool, store: FakeArtifactStore, events: CapturingEventStore) =
        SandboxedToolExecutor(
            delegate = tool,
            registry = SingleToolRegistry(tool),
            eventDispatcher = EventDispatcher(events),
            workDir = Files.createTempDirectory("sbx-work"),
            artifactStore = store,
        )

    @Test
    fun `overwrite emits FileWrittenEvent with distinct pre and post images in CAS`(): Unit = runBlocking {
        val dir = Files.createTempDirectory("sbx").toRealPath()
        val target = dir.resolve("f.txt")
        Files.writeString(target, "OLD")
        val tool = FileWriteTool(allowedPaths = setOf(dir))
        val store = FakeArtifactStore()
        val events = CapturingEventStore()

        val result = executor(tool, store, events).execute(writeRequest(target.toString(), "NEW"))

        assertTrue(result is ToolResult.Success)
        val fw = events.payloads.filterIsInstance<FileWrittenEvent>().single()
        assertEquals(target.toString(), fw.path)
        assertTrue(fw.preExisted)
        assertNotNull(fw.preImageHash)
        assertNotNull(fw.postImageHash)
        assertTrue(fw.preImageHash != fw.postImageHash)
        assertEquals("OLD", store.get(ArtifactId(fw.preImageHash!!))!!.toString(Charsets.UTF_8))
        assertEquals("NEW", store.get(ArtifactId(fw.postImageHash!!))!!.toString(Charsets.UTF_8))
    }

    @Test
    fun `new file write records preExisted false and null pre-image`(): Unit = runBlocking {
        val dir = Files.createTempDirectory("sbx-new").toRealPath()
        val target = dir.resolve("new.txt")
        val tool = FileWriteTool(allowedPaths = setOf(dir))
        val store = FakeArtifactStore()
        val events = CapturingEventStore()

        executor(tool, store, events).execute(writeRequest(target.toString(), "HELLO"))

        val fw = events.payloads.filterIsInstance<FileWrittenEvent>().single()
        assertEquals(false, fw.preExisted)
        assertNull(fw.preImageHash)
        assertEquals("HELLO", store.get(ArtifactId(fw.postImageHash!!))!!.toString(Charsets.UTF_8))
    }

    @Test
    fun `writing identical content reframes the success message as a no-op`(): Unit = runBlocking {
        val dir = Files.createTempDirectory("sbx-noop").toRealPath()
        val target = dir.resolve("f.txt")
        Files.writeString(target, "SAME")
        val tool = FileWriteTool(allowedPaths = setOf(dir))
        val store = FakeArtifactStore()
        val events = CapturingEventStore()

        val result = executor(tool, store, events).execute(writeRequest(target.toString(), "SAME"))

        assertTrue(result is ToolResult.Success)
        val success = result as ToolResult.Success
        assertTrue(success.output.startsWith("No change:"), success.output)
        assertEquals("true", success.metadata["noop"])
    }

    @Test
    fun `changing content keeps the normal success message`(): Unit = runBlocking {
        val dir = Files.createTempDirectory("sbx-changed").toRealPath()
        val target = dir.resolve("f.txt")
        Files.writeString(target, "OLD")
        val tool = FileWriteTool(allowedPaths = setOf(dir))
        val store = FakeArtifactStore()
        val events = CapturingEventStore()

        val result = executor(tool, store, events).execute(writeRequest(target.toString(), "NEW"))

        val success = result as ToolResult.Success
        assertTrue(!success.output.startsWith("No change:"), success.output)
        assertNull(success.metadata["noop"])
    }

    @Test
    fun `no artifact store means no FileWrittenEvent (backward compatible)`(): Unit = runBlocking {
        val dir = Files.createTempDirectory("sbx-nostore").toRealPath()
        val target = dir.resolve("f.txt")
        val tool = FileWriteTool(allowedPaths = setOf(dir))
        val events = CapturingEventStore()
        val exec = SandboxedToolExecutor(
            delegate = tool,
            registry = SingleToolRegistry(tool),
            eventDispatcher = EventDispatcher(events),
            workDir = Files.createTempDirectory("sbx-nostore-work"),
        )

        exec.execute(writeRequest(target.toString(), "X"))

        assertTrue(events.payloads.filterIsInstance<FileWrittenEvent>().isEmpty())
    }
}
