import Testing
import Foundation
@testable import Oppi

@Suite("TimelineReducer — Basic")
@MainActor
struct TimelineReducerBasicTests {

    @Test func basicAgentTurn() {
        let reducer = TimelineReducer()

        reducer.process(.agentStart(sessionId: "s1"))
        reducer.process(.textDelta(sessionId: "s1", delta: "Hello "))
        reducer.process(.textDelta(sessionId: "s1", delta: "world!"))
        reducer.process(.agentEnd(sessionId: "s1"))

        #expect(reducer.items.count == 1)
        guard case .assistantMessage(_, let text, _) = reducer.items[0] else {
            Issue.record("Expected assistantMessage")
            return
        }
        #expect(text == "Hello world!")
    }

    @Test func thinkingThenText() {
        let reducer = TimelineReducer()

        reducer.process(.agentStart(sessionId: "s1"))
        reducer.process(.thinkingDelta(sessionId: "s1", delta: "I need to "))
        reducer.process(.thinkingDelta(sessionId: "s1", delta: "think..."))
        reducer.process(.textDelta(sessionId: "s1", delta: "The answer is 42."))
        reducer.process(.agentEnd(sessionId: "s1"))

        #expect(reducer.items.count == 2) // thinking + assistant
        guard case .thinking(_, let preview, _, _) = reducer.items[0] else {
            Issue.record("Expected thinking")
            return
        }
        #expect(preview.contains("I need to think"))
    }

    @Test func thinkingStreamingShowsPreviewBeforeFinalization() {
        let reducer = TimelineReducer()

        reducer.process(.agentStart(sessionId: "s1"))
        reducer.process(.thinkingDelta(sessionId: "s1", delta: "Let me "))
        reducer.process(.thinkingDelta(sessionId: "s1", delta: "analyze this"))

        // Mid-stream: thinking item exists with isDone == false and preview text
        #expect(reducer.items.count == 1)
        guard case .thinking(_, let preview, _, let isDone) = reducer.items[0] else {
            Issue.record("Expected thinking item during streaming")
            return
        }
        #expect(preview.contains("Let me analyze"))
        #expect(!isDone) // Still streaming

        // Finalize
        reducer.process(.textDelta(sessionId: "s1", delta: "Answer."))
        reducer.process(.agentEnd(sessionId: "s1"))

        // After finalization: thinking isDone, then assistant message
        #expect(reducer.items.count == 2)
        guard case .thinking(_, _, _, let finalDone) = reducer.items[0] else {
            Issue.record("Expected thinking item after finalization")
            return
        }
        #expect(finalDone)
    }

    @Test func multipleAgentTurns() {
        let reducer = TimelineReducer()

        // Turn 1
        reducer.process(.agentStart(sessionId: "s1"))
        reducer.process(.textDelta(sessionId: "s1", delta: "First"))
        reducer.process(.agentEnd(sessionId: "s1"))

        // Turn 2
        reducer.process(.agentStart(sessionId: "s1"))
        reducer.process(.textDelta(sessionId: "s1", delta: "Second"))
        reducer.process(.agentEnd(sessionId: "s1"))

        let assistants = reducer.items.filter {
            if case .assistantMessage = $0 { return true }
            return false
        }
        #expect(assistants.count == 2)

        guard case .assistantMessage(_, let t1, _) = assistants[0],
              case .assistantMessage(_, let t2, _) = assistants[1] else {
            Issue.record("Expected two assistant messages")
            return
        }
        #expect(t1 == "First")
        #expect(t2 == "Second")
    }

    @Test func agentEndWithoutContentProducesNoItems() {
        let reducer = TimelineReducer()

        reducer.process(.agentStart(sessionId: "s1"))
        reducer.process(.agentEnd(sessionId: "s1"))

        // No text deltas → no assistant message or thinking item
        #expect(reducer.items.isEmpty)
    }

    @Test func appendUserMessage() {
        let reducer = TimelineReducer()
        reducer.appendUserMessage("Hello from user")

        #expect(reducer.items.count == 1)
        guard case .userMessage(_, let text, _, _) = reducer.items[0] else {
            Issue.record("Expected userMessage")
            return
        }
        #expect(text == "Hello from user")
    }

    @Test func appendSystemEvent() {
        let reducer = TimelineReducer()

        reducer.appendSystemEvent("Session force-stopped")

        #expect(reducer.items.count == 1)
        guard case .systemEvent(_, let msg) = reducer.items[0] else {
            Issue.record("Expected systemEvent")
            return
        }
        #expect(msg == "Session force-stopped")
    }

    @Test func loadSessionUsesInjectedEnvironmentForPrewarmAndLogging() {
        var cancelCount = 0
        var cacheClearCount = 0
        var prewarmedTexts: [[String]] = []
        var logMessages: [String] = []
        let environment = TimelineReducerEnvironment(
            markdownPrewarmer: TimelineMarkdownPrewarmer(
                cachePurgeItemThreshold: 0,
                cancel: { cancelCount += 1 },
                clearCache: { cacheClearCount += 1 },
                prewarm: { prewarmedTexts.append($0) }
            ),
            logLoadSession: { logMessages.append($0) }
        )
        let reducer = TimelineReducer(environment: environment)

        reducer.loadSession([
            TraceEvent(
                id: "a1",
                type: .assistant,
                timestamp: "2025-01-01T00:00:00.000Z",
                text: "hello"
            )
        ])
        reducer.reset()

        #expect(cancelCount == 2)
        #expect(cacheClearCount == 1)
        #expect(prewarmedTexts == [["hello"]])
        #expect(logMessages.count == 1)
        #expect(logMessages[0].contains("full rebuild"))
    }

    @Test func userMessageProjectionStripsAttachmentMetadataForComparison() {
        let marked = "[[oppi-attachments:b:photos=1;p:repoFile=README.md]]\nhello"
        #expect(UserMessageTextProjection.comparableText(marked) == "hello")

        let withFiles = "hello\n\nAttached files:\n- report.pdf: .pi/attachments/report.pdf"
        #expect(UserMessageTextProjection.comparableText(withFiles) == "hello")

        let withReferences = "hello\n\nReferenced workspace files:\n- Sources/App.swift"
        #expect(UserMessageTextProjection.comparableText(withReferences) == "hello")
    }

    @Test func userMessageProjectionPreservesReservedHeadersUsedAsProse() {
        let commitPrompt = """
        Extra focus:
        Referenced workspace files: - Sources/App.swift

        Git hygiene:
        - Do not commit unless explicitly asked.
        """
        #expect(UserMessageTextProjection.visibleText(from: commitPrompt) == commitPrompt)

        let attachmentProse = """
        Attached files: explanatory text

        Checklist:
        - report.pdf: keep this visible
        """
        #expect(UserMessageTextProjection.visibleText(from: attachmentProse) == attachmentProse)

        let leadingSpaceHeader = " Referenced workspace files:\n- Keep this instruction visible."
        #expect(
            UserMessageTextProjection.visibleText(from: leadingSpaceHeader)
                == "Referenced workspace files:\n- Keep this instruction visible."
        )
    }

    @Test func retryStartRendersAsError() {
        let reducer = TimelineReducer()
        reducer.process(.retryStart(sessionId: "s1", attempt: 1, maxAttempts: 3, delayMs: 2000, errorMessage: "rate limit"))

        #expect(reducer.items.count == 1)
        guard case .error(_, let msg) = reducer.items[0] else {
            Issue.record("Expected error for retry, got \(reducer.items[0])")
            return
        }
        #expect(msg.contains("Retrying"))
        #expect(msg.contains("1/3"))
    }

    @Test func realErrorRendersAsError() {
        let reducer = TimelineReducer()
        reducer.process(.error(sessionId: "s1", message: "Something went wrong"))

        guard case .error(_, let msg) = reducer.items[0] else {
            Issue.record("Expected error")
            return
        }
        #expect(msg == "Something went wrong")
    }
}
