package link.socket.ampere.renderer

import com.github.ajalt.mordant.terminal.Terminal
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import link.socket.ampere.agents.domain.event.CognitiveEvent
import link.socket.ampere.agents.domain.event.Event
import link.socket.ampere.agents.domain.event.CognitiveStateSnapshot
import link.socket.ampere.agents.domain.event.EventSource
import link.socket.ampere.agents.domain.event.ProviderCallCompletedEvent
import link.socket.ampere.agents.domain.event.SparkAppliedEvent
import link.socket.ampere.agents.domain.event.SparkRemovedEvent
import link.socket.ampere.agents.domain.Urgency
import link.socket.ampere.api.model.TokenUsage
import org.junit.jupiter.api.Test
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class EventRendererTest {

    /**
     * Helper to capture terminal output for testing.
     * Mordant writes to System.out, so we redirect it to capture output.
     */
    private fun captureTerminalOutput(block: (Terminal, EventRenderer) -> Unit): String {
        val outputStream = ByteArrayOutputStream()
        val printStream = PrintStream(outputStream)
        val originalOut = System.out

        return try {
            System.setOut(printStream)
            val terminal = Terminal()
            val renderer = EventRenderer(terminal, TimeZone.UTC)
            block(terminal, renderer)
            outputStream.toString()
        } finally {
            System.setOut(originalOut)
        }
    }

    private fun stripAnsi(text: String): String {
        return text.replace(Regex("\u001B\\[[0-9;]*[a-zA-Z]"), "")
    }

    private fun extractRenderedIcon(output: String): String {
        val line = stripAnsi(output).lineSequence().firstOrNull()?.trim() ?: ""
        val parts = line.split(Regex("\\s+"))
        return parts.getOrNull(1) ?: ""
    }

    /**
     * Helper to create a test TaskCreated event.
     */
    private fun taskEvent(
        eventId: String = "evt-task-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        urgency: Urgency = Urgency.HIGH,
        taskId: String = "TASK-123",
        description: String = "Implement authentication",
        assignedTo: String? = "agent-auth"
    ): Event.TaskCreated = Event.TaskCreated(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        taskId = taskId,
        description = description,
        assignedTo = assignedTo
    )

    /**
     * Helper to create a test QuestionRaised event.
     */
    private fun questionEvent(
        eventId: String = "evt-question-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        urgency: Urgency = Urgency.MEDIUM,
        questionText: String = "How should we handle this error?",
        context: String = "During database migration"
    ): Event.QuestionRaised = Event.QuestionRaised(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        questionText = questionText,
        context = context
    )

    /**
     * Helper to create a test CodeSubmitted event.
     */
    private fun codeEvent(
        eventId: String = "evt-code-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-dev"),
        urgency: Urgency = Urgency.LOW,
        filePath: String = "src/main/Auth.kt",
        changeDescription: String = "Add OAuth support",
        reviewRequired: Boolean = true,
        assignedTo: String? = "agent-reviewer"
    ): Event.CodeSubmitted = Event.CodeSubmitted(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        filePath = filePath,
        changeDescription = changeDescription,
        reviewRequired = reviewRequired,
        assignedTo = assignedTo
    )

    private fun sparkAppliedEvent(
        eventId: String = "evt-spark-applied-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        urgency: Urgency = Urgency.LOW,
        agentId: String = "agent-test",
        stackDepth: Int = 2,
        stackDescription: String = "[ANALYTICAL] → [Project:ampere]",
        sparkName: String = "Project:ampere",
        sparkType: String = "ProjectSpark"
    ): SparkAppliedEvent = SparkAppliedEvent(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        agentId = agentId,
        stackDepth = stackDepth,
        stackDescription = stackDescription,
        sparkName = sparkName,
        sparkType = sparkType
    )

    private fun sparkRemovedEvent(
        eventId: String = "evt-spark-removed-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        urgency: Urgency = Urgency.LOW,
        agentId: String = "agent-test",
        stackDepth: Int = 1,
        stackDescription: String = "[ANALYTICAL]",
        previousSparkName: String = "Project:ampere"
    ): SparkRemovedEvent = SparkRemovedEvent(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        agentId = agentId,
        stackDepth = stackDepth,
        stackDescription = stackDescription,
        previousSparkName = previousSparkName
    )

    private fun snapshotEvent(
        eventId: String = "evt-spark-snapshot-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        urgency: Urgency = Urgency.LOW,
        agentId: String = "agent-test",
        stackDepth: Int = 3,
        stackDescription: String = "[INTEGRATIVE] → [Project:ampere] → [Role:Planning]",
        affinity: String = "INTEGRATIVE",
        sparkNames: List<String> = listOf("Project:ampere", "Role:Planning"),
        effectivePromptLength: Int = 1200,
        availableToolCount: Int? = 7
    ): CognitiveStateSnapshot = CognitiveStateSnapshot(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        agentId = agentId,
        stackDepth = stackDepth,
        stackDescription = stackDescription,
        affinity = affinity,
        sparkNames = sparkNames,
        effectivePromptLength = effectivePromptLength,
        availableToolCount = availableToolCount
    )

    private fun providerCallCompletedEvent(
        eventId: String = "evt-llm-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        urgency: Urgency = Urgency.LOW,
        usage: TokenUsage = TokenUsage(
            inputTokens = 1247,
            outputTokens = 892,
            estimatedCost = 0.0031,
        ),
        latencyMs: Long = 340,
        success: Boolean = true,
    ): ProviderCallCompletedEvent = ProviderCallCompletedEvent(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        workflowId = "wf-render",
        agentId = "agent-test",
        providerId = "openai",
        modelId = "gpt-4.1",
        usage = usage,
        latencyMs = latencyMs,
        success = success,
    )

    private fun escalationFiredEvent(
        eventId: String = "evt-escalation-fired-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        urgency: Urgency = Urgency.HIGH,
    ): CognitiveEvent.EscalationFired = CognitiveEvent.EscalationFired(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        urgency = urgency,
        agentId = "agent-test",
        uncertaintyValue = 0.9,
        threshold = 0.7,
        prompt = "Need human decision",
        cognitivePhase = null,
    )

    private fun escalationConsideredEvent(
        eventId: String = "evt-escalation-considered-1",
        timestamp: Instant = Clock.System.now(),
        source: EventSource = EventSource.Agent("agent-test"),
        fired: Boolean = false,
    ): CognitiveEvent.EscalationConsidered = CognitiveEvent.EscalationConsidered(
        eventId = eventId,
        timestamp = timestamp,
        eventSource = source,
        agentId = "agent-test",
        uncertaintyValue = 0.42,
        threshold = 0.7,
        fired = fired,
        cognitivePhase = null,
    )

    @Test
    fun `render TaskCreated event shows task ID, description, and assignment`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                taskEvent(
                    taskId = "TASK-456",
                    description = "Fix login bug",
                    assignedTo = "agent-bugfixer"
                )
            )
        }

        assertContains(output, "TASK-456")
        assertContains(output, "Fix login bug")
        assertContains(output, "assigned to: agent-bugfixer")
        assertContains(output, "TaskCreated")
    }

    @Test
    fun `render QuestionRaised event shows question text and context`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                questionEvent(
                    questionText = "Should we use Redis or Memcached?",
                    context = "Caching layer design"
                )
            )
        }

        assertContains(output, "Should we use Redis or Memcached?")
        assertContains(output, "Caching layer design")
        assertContains(output, "QuestionRaised")
    }

    @Test
    fun `render CodeSubmitted event shows file path and change description`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                codeEvent(
                    filePath = "src/auth/OAuth.kt",
                    changeDescription = "Implement OAuth2 flow",
                    reviewRequired = true,
                    assignedTo = "agent-security"
                )
            )
        }

        assertContains(output, "src/auth/OAuth.kt")
        assertContains(output, "Implement OAuth2 flow")
        assertContains(output, "review required")
        assertContains(output, "agent-security")
        assertContains(output, "CodeSubmitted")
    }

    @Test
    fun `render formats timestamp as HH-MM-SS`() {
        // Create an event with a specific timestamp
        val timestamp = Instant.parse("2024-01-15T14:32:18Z")
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(taskEvent(timestamp = timestamp))
        }

        // Should show time in UTC (as configured in captureTerminalOutput)
        assertContains(output, "14:32:18")
    }

    @Test
    fun `render shows urgency level for all event types`() {
        val highUrgencyOutput = captureTerminalOutput { _, renderer ->
            renderer.render(taskEvent(urgency = Urgency.HIGH))
        }
        assertContains(highUrgencyOutput, "[HIGH]")

        val mediumUrgencyOutput = captureTerminalOutput { _, renderer ->
            renderer.render(questionEvent(urgency = Urgency.MEDIUM))
        }
        assertContains(mediumUrgencyOutput, "[MEDIUM]")

        val lowUrgencyOutput = captureTerminalOutput { _, renderer ->
            renderer.render(codeEvent(urgency = Urgency.LOW))
        }
        assertContains(lowUrgencyOutput, "[LOW]")
    }

    @Test
    fun `render completed provider call shows usage cost and latency`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(providerCallCompletedEvent())
        }

        val rendered = stripAnsi(output)
        assertContains(rendered, "1,247 in / 892 out")
        assertContains(rendered, "~\$0.0031")
        assertContains(rendered, "340ms")
        assertContains(rendered, "ProviderCallCompleted")
    }

    @Test
    fun `render completed provider call omits cost when unavailable`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                providerCallCompletedEvent(
                    usage = TokenUsage(
                        inputTokens = 1247,
                        outputTokens = 892,
                        estimatedCost = null,
                    ),
                ),
            )
        }

        val rendered = stripAnsi(output)
        assertContains(rendered, "1,247 in / 892 out")
        assertContains(rendered, "340ms")
        assertTrue(!rendered.contains("~$"))
    }

    @Test
    fun `render shows event source for agent events`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                taskEvent(source = EventSource.Agent("agent-orchestrator"))
            )
        }

        assertContains(output, "agent-orchestrator")
    }

    @Test
    fun `render shows event source for human events`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                taskEvent(source = EventSource.Human)
            )
        }

        assertContains(output, "human")
    }

    @Test
    fun `render TaskCreated without assignment doesn't show assigned to`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                taskEvent(assignedTo = null)
            )
        }

        assertTrue(!output.contains("assigned to:"))
    }

    @Test
    fun `render QuestionRaised truncates long context`() {
        val longContext = "This is a very long context string that should be truncated " +
            "because it exceeds the maximum length of 60 characters for display purposes"

        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                questionEvent(context = longContext)
            )
        }

        // Should be truncated with ellipsis
        assertContains(output, "...")
        // Should not contain the full string
        assertTrue(output.length < longContext.length + 100) // Some buffer for formatting
    }

    @Test
    fun `render CodeSubmitted without review shows no review indicator`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                codeEvent(reviewRequired = false, assignedTo = null)
            )
        }

        assertTrue(!output.contains("review required"))
    }

    @Test
    fun `render includes appropriate icons for event types`() {
        val taskOutput = captureTerminalOutput { _, renderer ->
            renderer.render(taskEvent())
        }
        assertContains(taskOutput, "📋")

        val questionOutput = captureTerminalOutput { _, renderer ->
            renderer.render(questionEvent())
        }
        assertContains(questionOutput, "❓")

        val codeOutput = captureTerminalOutput { _, renderer ->
            renderer.render(codeEvent())
        }
        assertContains(codeOutput, "💻")

        val escalationOutput = captureTerminalOutput { _, renderer ->
            renderer.render(escalationFiredEvent())
        }
        assertContains(escalationOutput, "❓")
        assertContains(escalationOutput, "EscalationFired")

        val consideredOutput = captureTerminalOutput { _, renderer ->
            renderer.render(escalationConsideredEvent())
        }
        assertContains(consideredOutput, "❔")
        assertContains(consideredOutput, "EscalationConsidered")
    }

    @Test
    fun `render SparkAppliedEvent uses applied icon in prefix`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(sparkAppliedEvent())
        }

        assertEquals(SparkColors.SparkIcons.APPLIED, extractRenderedIcon(output))
    }

    @Test
    fun `render SparkRemovedEvent uses removed icon in prefix`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(sparkRemovedEvent())
        }

        assertEquals(SparkColors.SparkIcons.REMOVED, extractRenderedIcon(output))
    }

    @Test
    fun `render CognitiveStateSnapshot uses snapshot icon in prefix`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(snapshotEvent())
        }

        assertEquals(SparkColors.SparkIcons.SNAPSHOT, extractRenderedIcon(output))
    }

    @Test
    fun `render handles different timezones correctly`() {
        val timestamp = Instant.parse("2024-01-15T14:32:18Z")

        // Create renderer with specific timezone
        val outputStream = ByteArrayOutputStream()
        val printStream = PrintStream(outputStream)
        val originalOut = System.out

        try {
            System.setOut(printStream)
            val terminal = Terminal()
            val renderer = EventRenderer(terminal, TimeZone.of("America/New_York"))
            renderer.render(taskEvent(timestamp = timestamp))

            val output = outputStream.toString()

            // In EST/EDT, this should show a different time than UTC
            // 14:32:18 UTC = 09:32:18 EST (UTC-5)
            val expectedTime = timestamp.toLocalDateTime(TimeZone.of("America/New_York"))
            val expectedTimeStr = buildString {
                append(expectedTime.hour.toString().padStart(2, '0'))
                append(":")
                append(expectedTime.minute.toString().padStart(2, '0'))
                append(":")
                append(expectedTime.second.toString().padStart(2, '0'))
            }

            assertContains(output, expectedTimeStr)
        } finally {
            System.setOut(originalOut)
        }
    }

    @Test
    fun `render SparkAppliedEvent shows StackApplied type name`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(sparkAppliedEvent())
        }

        assertContains(output, "StackApplied")
    }

    @Test
    fun `render SparkRemovedEvent shows StackRemoved type name`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(sparkRemovedEvent())
        }

        assertContains(output, "StackRemoved")
    }

    @Test
    fun `render CognitiveStateSnapshot shows StackSnapshot type name`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(snapshotEvent())
        }

        assertContains(output, "StackSnapshot")
    }

    @Test
    fun `render SparkAppliedEvent summary shows Stack push with name and depth`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                sparkAppliedEvent(
                    sparkName = "Role:Developer",
                    stackDepth = 3
                )
            )
        }

        assertContains(output, "Stack push: Role:Developer")
        assertContains(output, "(depth: 3)")
    }

    @Test
    fun `render SparkRemovedEvent summary shows Stack pop with name and depth`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                sparkRemovedEvent(
                    previousSparkName = "Task:Implement",
                    stackDepth = 2
                )
            )
        }

        assertContains(output, "Stack pop: Task:Implement")
        assertContains(output, "(depth: 2)")
    }

    @Test
    fun `render CognitiveStateSnapshot summary shows Stack snapshot with affinity and layers`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                snapshotEvent(
                    affinity = "ANALYTICAL",
                    sparkNames = listOf("Project:ampere", "Role:Code"),
                    effectivePromptLength = 1500
                )
            )
        }

        assertContains(output, "Stack snapshot: [ANALYTICAL]")
        assertContains(output, "+ 2 layer(s)")
        assertContains(output, "(prompt: 1500 chars)")
    }

    @Test
    fun `render CognitiveStateSnapshot without layers omits layer count`() {
        val output = captureTerminalOutput { _, renderer ->
            renderer.render(
                snapshotEvent(
                    affinity = "OPERATIONAL",
                    sparkNames = emptyList(),
                    effectivePromptLength = 800
                )
            )
        }

        assertContains(output, "Stack snapshot: [OPERATIONAL]")
        assertTrue(!output.contains("layer(s)"))
        assertContains(output, "(prompt: 800 chars)")
    }
}
