import Foundation
import Testing

@testable import NucleicCore

/// `AskUserQuestion` parsing + answer encoding: replying `allow` with the input
/// unchanged makes the CLI report "the user did not answer the questions", so the
/// selections must land in `updatedInput.answers` keyed by the exact question text
/// (string for single-select, array for multiSelect).
@Suite struct AskUserQuestionTests {
    let input: JSONValue = [
        "questions": [
            [
                "header": "Format",
                "question": "How should I format the output?",
                "multiSelect": false,
                "options": [
                    ["label": "Summary", "description": "Brief overview"],
                    ["label": "Detailed", "description": "Full explanation"],
                ],
            ],
            [
                "header": "Sections",
                "question": "Which sections should I include?",
                "multiSelect": true,
                "options": [
                    ["label": "Intro", "description": "Opening"],
                    ["label": "Conclusion", "description": "Closing"],
                ],
            ],
        ]
    ]

    @Test func parsesQuestionsOptionsAndMultiSelect() {
        let questions = AskUserQuestion.questions(from: input)
        #expect(questions?.count == 2)
        #expect(questions?[0].header == "Format")
        #expect(questions?[0].multiSelect == false)
        #expect(questions?[0].options.map(\.label) == ["Summary", "Detailed"])
        #expect(questions?[1].multiSelect == true)
    }

    @Test func nonQuestionInputReturnsNil() {
        #expect(AskUserQuestion.questions(from: ["command": "ls"]) == nil)
        #expect(AskUserQuestion.questions(from: ["questions": []]) == nil)
    }

    @Test func singleSelectAnswerIsAStringMultiSelectIsAnArray() {
        let updated = AskUserQuestion.updatedInput(
            original: input,
            selections: [
                "How should I format the output?": ["Summary"],
                "Which sections should I include?": ["Intro", "Conclusion"],
            ])

        let answers = updated["answers"]
        #expect(answers?["How should I format the output?"] == .string("Summary"))
        #expect(
            answers?["Which sections should I include?"]
                == .array([.string("Intro"), .string("Conclusion")]))
        // Original questions are preserved alongside the answers.
        #expect(updated["questions"]?.arrayValue?.count == 2)
    }

    @Test func unselectedSingleSelectGetsEmptyString() {
        let updated = AskUserQuestion.updatedInput(original: input, selections: [:])
        #expect(updated["answers"]?["How should I format the output?"] == .string(""))
        #expect(updated["answers"]?["Which sections should I include?"] == .array([]))
    }

    /// The selections we fold into `updatedInput` never reappear in the persisted `tool_use`
    /// input — they come back only in the result text the CLI emits. Recover them from there
    /// so a scrolled-back transcript can render what the user picked. The format is a flat
    /// `"<question>"="<answer>"` list; matching the exact question text tolerates the em-dashes
    /// and quotes the CLI echoes verbatim.
    @Test func parsesAnswersFromResultText() {
        let questions = AskUserQuestion.questions(from: input) ?? []
        // A multi-select answer comes back as labels joined by a bare comma, no space.
        let result =
            "Your questions have been answered: "
            + "\"How should I format the output?\"=\"Detailed\", "
            + "\"Which sections should I include?\"=\"Intro,Conclusion\". "
            + "You can now continue with these answers in mind."

        let answers = AskUserQuestion.answers(in: result, for: questions)
        #expect(answers["How should I format the output?"] == ["Detailed"])
        // Split back into the individual picked options rather than rendered "Intro,Conclusion".
        #expect(answers["Which sections should I include?"] == ["Intro", "Conclusion"])
    }

    @Test func keepsQuoteBearingCustomAnswerWhole() {
        let questions = AskUserQuestion.questions(from: input) ?? []
        // A free-text "Other" answer the CLI echoes verbatim, complete with inner double quotes.
        // Reading only up to the first inner quote would cut it off; the closing quote is the
        // one followed by the `, ` separator (mid-list) or the `.` terminator (last answer).
        let result =
            "Your questions have been answered: "
            + "\"How should I format the output?\"=\"Use the \"fast\" path\", "
            + "\"Which sections should I include?\"=\"Both \"intro\" sections\". "
            + "You can now continue with these answers in mind."

        let answers = AskUserQuestion.answers(in: result, for: questions)
        #expect(answers["How should I format the output?"] == ["Use the \"fast\" path"])
        #expect(answers["Which sections should I include?"] == ["Both \"intro\" sections"])
    }

    @Test func unmatchedOrPendingQuestionsAreOmitted() {
        let questions = AskUserQuestion.questions(from: input) ?? []
        // A result that only answered the first question, plus a pre-answer placeholder.
        let partial =
            "Your questions have been answered: \"How should I format the output?\"=\"Summary\"."
        let answers = AskUserQuestion.answers(in: partial, for: questions)
        #expect(answers["How should I format the output?"] == ["Summary"])
        #expect(answers["Which sections should I include?"] == nil)

        #expect(AskUserQuestion.answers(in: "", for: questions).isEmpty)
    }

    @Test func splitsMultiSelectAndKeepsCommaBearingLabelsAndOtherWhole() {
        // Bare-comma join (the real format) and the occasional comma-space both decompose.
        #expect(
            AskUserQuestion.selectedLabels(from: "Intro,Conclusion", options: ["Intro", "Conclusion"])
                == ["Intro", "Conclusion"])
        #expect(
            AskUserQuestion.selectedLabels(from: "Intro, Conclusion", options: ["Intro", "Conclusion"])
                == ["Intro", "Conclusion"])
        // A label that itself contains a comma is matched whole, not chopped at its comma.
        #expect(
            AskUserQuestion.selectedLabels(
                from: "Both (mediated, git poll),Intro",
                options: ["Both (mediated, git poll)", "Intro"])
                == ["Both (mediated, git poll)", "Intro"])
        // A single pick is one answer; free-text that matches no option stays verbatim.
        #expect(AskUserQuestion.selectedLabels(from: "Summary", options: ["Summary"]) == ["Summary"])
        #expect(
            AskUserQuestion.selectedLabels(from: "my, own thoughts", options: ["Intro"])
                == ["my, own thoughts"])
    }
}
