import Foundation
import Testing

@testable import Oppi

@Suite("TimelineReducer ask tool handling")
@MainActor
struct TimelineReducerAskTests {

    // MARK: - formatAskAnswers

    @Test("formatAskAnswers with full answers uses question text")
    func formatAskAnswersFullAnswers() {
        let details: JSONValue = .object([
            "questions": .array([
                .object(["id": .string("color"), "question": .string("Pick a color")]),
                .object(["id": .string("size"), "question": .string("Pick a size")]),
            ]),
            "answers": .object([
                "color": .string("blue"),
                "size": .string("large"),
            ]),
            "allIgnored": .bool(false),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details) == "**Q:** Pick a color\n**A:** blue\n\n**Q:** Pick a size\n**A:** large")
    }

    @Test("formatAskAnswers with skipped question")
    func formatAskAnswersSkipped() {
        let details: JSONValue = .object([
            "questions": .array([
                .object(["id": .string("color"), "question": .string("Pick a color")]),
                .object(["id": .string("size"), "question": .string("Pick a size")]),
            ]),
            "answers": .object(["color": .string("red")]),
            "allIgnored": .bool(false),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details) == "**Q:** Pick a color\n**A:** red\n\n**Q:** Pick a size\n**A:** (skipped)")
    }

    @Test("formatAskAnswers single question keeps full question and answer")
    func formatAskAnswersSingleQuestion() {
        let details: JSONValue = .object([
            "questions": .array([
                .object(["id": .string("threads"), "question": .string("Which threads?")]),
            ]),
            "answers": .object(["threads": .string("47618223")]),
            "allIgnored": .bool(false),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details) == "**Q:** Which threads?\n**A:** 47618223")
    }

    @Test("formatAskAnswers single question multi-select keeps full question and answer")
    func formatAskAnswersSingleQuestionMultiSelect() {
        let details: JSONValue = .object([
            "questions": .array([
                .object(["id": .string("threads"), "question": .string("Which threads?")]),
            ]),
            "answers": .object(["threads": .array([.string("123"), .string("456")])]),
            "allIgnored": .bool(false),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details) == "**Q:** Which threads?\n**A:** 123, 456")
    }

    @Test("formatAskAnswers falls back to id when question text missing")
    func formatAskAnswersFallbackToId() {
        let details: JSONValue = .object([
            "questions": .array([
                .object(["id": .string("color")]),
                .object(["id": .string("size")]),
            ]),
            "answers": .object([
                "color": .string("blue"),
                "size": .string("large"),
            ]),
            "allIgnored": .bool(false),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details) == "**Q:** color\n**A:** blue\n\n**Q:** size\n**A:** large")
    }

    @Test("formatAskAnswers with allIgnored returns empty")
    func formatAskAnswersAllIgnored() {
        let details: JSONValue = .object([
            "questions": .array([.object(["id": .string("q1")])]),
            "answers": .object([:]),
            "allIgnored": .bool(true),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details).isEmpty)
    }

    @Test("formatAskAnswers with multi-select array (multi-question)")
    func formatAskAnswersMultiSelect() {
        let details: JSONValue = .object([
            "questions": .array([
                .object(["id": .string("tools"), "question": .string("Which tools?")]),
                .object(["id": .string("scope"), "question": .string("Scope?")]),
            ]),
            "answers": .object([
                "tools": .array([.string("ruff"), .string("mypy")]),
                "scope": .string("full"),
            ]),
            "allIgnored": .bool(false),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details) == "**Q:** Which tools?\n**A:** ruff, mypy\n\n**Q:** Scope?\n**A:** full")
    }

    @Test("formatAskAnswers maps option values to labels")
    func formatAskAnswersMapsOptionValuesToLabels() {
        let details: JSONValue = .object([
            "questions": .array([
                .object([
                    "id": .string("approach"),
                    "question": .string("Which approach?"),
                    "options": .array([
                        .object(["value": .string("full_rewrite"), "label": .string("Full rewrite")]),
                        .object(["value": .string("small_patch"), "label": .string("Small patch")]),
                    ])
                ])
            ]),
            "answers": .object(["approach": .string("full_rewrite")]),
            "allIgnored": .bool(false),
        ])
        #expect(TimelineReducer.formatAskAnswers(details: details) == "**Q:** Which approach?\n- [x] Full rewrite\n- [ ] Small patch")
    }

    @Test("formatAskAnswers with nil details returns empty")
    func formatAskAnswersNilDetails() {
        #expect(TimelineReducer.formatAskAnswers(details: nil).isEmpty)
    }

    // MARK: - Ask tool handling in timeline

    @Test("ask toolStart creates tool row")
    func askToolStartCreatesRow() {
        let reducer = TimelineReducer()
        reducer.process(.toolStart(
            sessionId: "s1", toolEventId: "ask-evt-1", tool: "ask",
            args: ["questions": .array([.object(["id": .string("q1")])])],
            callSegments: [StyledSegment(text: "Pick a color", style: .muted)]
        ))
        #expect(reducer.items.contains(where: {
            if case .toolCall(let id, _, _, _, _, _, _) = $0 { return id == "ask-evt-1" }
            return false
        }))
    }

    @Test("ask toolEnd injects user message with full question and answer")
    func askToolEndInjectsUserMessage() {
        let reducer = TimelineReducer()
        reducer.process(.toolStart(
            sessionId: "s1", toolEventId: "ask-evt-1", tool: "ask", args: [:]
        ))
        reducer.process(.toolEnd(
            sessionId: "s1", toolEventId: "ask-evt-1",
            details: .object([
                "questions": .array([.object([
                    "id": .string("approach"),
                    "question": .string("Which approach?")
                ])]),
                "answers": .object(["approach": .string("full_rewrite")]),
                "allIgnored": .bool(false),
            ])
        ))
        var foundText: String?
        for item in reducer.items {
            if case .userMessage(let id, let text, _, _) = item, id == "ask-answer-ask-evt-1" {
                foundText = text
            }
        }
        #expect(foundText == "**Q:** Which approach?\n**A:** full_rewrite")
        #expect(reducer.toolDetailsStore.details(for: "ask-evt-1") != nil)
    }

    @Test("ask toolEnd clears any leaked output from tool row")
    func askToolEndClearsOutput() {
        let reducer = TimelineReducer()
        reducer.process(.toolStart(
            sessionId: "s1", toolEventId: "ask-evt-1", tool: "ask", args: [:]
        ))
        // Simulate output arriving (server normally suppresses this, but test defensive path)
        reducer.process(.toolOutput(.init(
            sessionId: "s1", toolEventId: "ask-evt-1",
            output: "approach: full_rewrite",
            isError: false, mode: .append, truncated: false, totalBytes: nil,
            details: nil
        )))
        // toolEnd should clear the output and mark done with empty preview
        reducer.process(.toolEnd(
            sessionId: "s1", toolEventId: "ask-evt-1",
            details: .object([
                "questions": .array([.object(["id": .string("approach")])]),
                "answers": .object(["approach": .string("full_rewrite")]),
                "allIgnored": .bool(false),
            ])
        ))
        // Tool row should be done with no output
        for item in reducer.items {
            if case .toolCall(let id, _, _, let preview, let byteCount, _, let isDone) = item, id == "ask-evt-1" {
                #expect(isDone)
                #expect(preview.isEmpty)
                #expect(byteCount == 0)
            }
        }
    }

    @Test("ask toolEnd marks tool row as done")
    func askToolEndMarksRowDone() {
        let reducer = TimelineReducer()
        reducer.process(.toolStart(
            sessionId: "s1", toolEventId: "ask-evt-1", tool: "ask", args: [:]
        ))
        reducer.process(.toolEnd(
            sessionId: "s1", toolEventId: "ask-evt-1",
            details: .object([
                "questions": .array([.object(["id": .string("q1")])]),
                "answers": .object(["q1": .string("yes")]),
                "allIgnored": .bool(false),
            ])
        ))
        let toolRow = reducer.items.first(where: {
            if case .toolCall(let id, _, _, _, _, _, _) = $0 { return id == "ask-evt-1" }
            return false
        })
        if case .toolCall(_, _, _, _, _, _, let isDone) = toolRow {
            #expect(isDone)
        }
    }

    @Test("non-ask tool not affected by ask handling")
    func nonAskToolUnaffected() {
        let reducer = TimelineReducer()
        reducer.process(.toolStart(
            sessionId: "s1", toolEventId: "bash-1", tool: "bash",
            args: ["command": .string("ls")]
        ))
        reducer.process(.toolEnd(
            sessionId: "s1", toolEventId: "bash-1"
        ))
        #expect(!reducer.items.contains(where: {
            if case .userMessage = $0 { return true }
            return false
        }))
    }
}
