package com.correx.infrastructure.tools

import com.correx.core.approvals.Tier
import com.correx.core.events.EventDispatcher
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.LowQualityExtractionEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.SourceFetchedEvent
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.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.contract.ParamRole
import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolCapability
import com.correx.core.tools.contract.ToolExecutor
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.contract.ValidationResult
import com.correx.core.tools.registry.ToolRegistry
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.nio.file.Files

class SandboxedToolExecutorResearchSourceTest {

    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)
    }

    /** Stands in for a research fetch: returns the same metadata shape WebFetchTool emits. */
    private class FakeFetchTool(private val metadata: Map<String, String>) : Tool, ToolExecutor {
        override val name: String = "web_fetch"
        override val description: String = "fake"
        override val tier: Tier = Tier.T2
        override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.NETWORK_ACCESS)
        override val paramRoles: Map<String, ParamRole> = mapOf("url" to ParamRole.NETWORK_TARGET)
        override val parametersSchema: JsonObject = JsonObject(emptyMap())
        override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid
        override suspend fun execute(request: ToolRequest): ToolResult =
            ToolResult.Success(request.invocationId, output = "md", metadata = metadata)
    }

    private fun request() = ToolRequest(
        ToolInvocationId("inv"), SessionId("s"), StageId("st"), "web_fetch",
        mapOf("url" to "https://example.com/a"),
    )

    private fun runFetch(metadata: Map<String, String>): CapturingEventStore {
        val tool = FakeFetchTool(metadata)
        val events = CapturingEventStore()
        val exec = SandboxedToolExecutor(
            delegate = tool,
            registry = SingleToolRegistry(tool),
            eventDispatcher = EventDispatcher(events),
            workDir = Files.createTempDirectory("sbx-research"),
        )
        runBlocking { exec.execute(request()) }
        return events
    }

    private fun fetchMetadata(quality: String) = mapOf(
        "url" to "https://example.com/a",
        "content_type" to "text/html",
        "content_sha256" to "deadbeef",
        "fetched_bytes" to "4096",
        "extractor_version" to "html-md-1",
        "quality" to quality,
    )

    @Test
    fun `ok fetch emits SourceFetchedEvent and no low-quality event`() {
        val events = runFetch(fetchMetadata("OK"))

        val fetched = events.payloads.filterIsInstance<SourceFetchedEvent>().single()
        assertEquals("https://example.com/a", fetched.url)
        assertEquals("deadbeef", fetched.contentSha256)
        assertEquals("OK", fetched.quality)
        assertEquals(4096, fetched.byteCount)
        assertTrue(events.payloads.filterIsInstance<LowQualityExtractionEvent>().isEmpty())
    }

    @Test
    fun `low-quality fetch also emits LowQualityExtractionEvent`() {
        val events = runFetch(fetchMetadata("LOW_QUALITY"))

        assertEquals(1, events.payloads.filterIsInstance<SourceFetchedEvent>().size)
        val low = events.payloads.filterIsInstance<LowQualityExtractionEvent>().single()
        assertEquals("https://example.com/a", low.url)
        assertEquals("deadbeef", low.contentSha256)
        assertTrue(low.reason.isNotBlank())
    }

    @Test
    fun `non-fetch tool result emits no research source events`() {
        // No url/content_sha256/quality markers → the promotion path must stay silent.
        val events = runFetch(mapOf("some" to "metadata"))

        assertTrue(events.payloads.filterIsInstance<SourceFetchedEvent>().isEmpty())
        assertTrue(events.payloads.filterIsInstance<LowQualityExtractionEvent>().isEmpty())
    }
}
