import Foundation
import Testing

@testable import NucleicProtocol

/// Every `wrapped…` fixture below is a VERBATIM reply captured from the on-device Apple
/// Foundation Model on macOS 27 beta 4 (build 26A5388g), running Nucleic's own production
/// prompts. That seed answers a plain `respond(to:)` as if it were driving a tool-calling
/// harness, so these are the shapes `ModelOutput` exists to undo. Guided generation
/// (`@Generable`) still binds its schema on the same seed, so no fixture here covers it.
@Suite("AFM output unwrapping")
struct ModelOutputTests {

    // MARK: Captured beta 4 scaffolding

    @Test func unwrapsBareJSONObjectFromChatNamePrompt() {
        // Captured: chat-name #1.
        #expect(ModelOutput.unwrap(#"{"task": "Explore Repository Contents"}"#) == "Explore Repository Contents")
    }

    @Test func dropsPseudoToolCallThatSwallowedTheTitle() {
        // Captured: chat-name #2. The title rides inside a tool-call literal, not as prose —
        // the caller derives a name from the user's message instead.
        #expect(ModelOutput.usableText(#"Tool call: {tool: "Explore Repository Contents", args: {}}"#) == nil)
    }

    @Test func rejectsSnakeCaseSlotNamesAsTitles() {
        // Captured: chat-name #3 — every value is a machine slug, not prose. Better to yield
        // nothing than a title reading "Login_button_response".
        #expect(ModelOutput.usableText(
            #"{"request_file":"login_button_response","component":"login_button","task":"login_button_response"}"#) == nil)
    }

    @Test func unwrapsFencedJSONArrayWithEmptyToolCalls() {
        // Captured: todo-gist #1 and #2.
        let wrapped = """
            ```json
            [
              {
                "tool_calls": [],
                "summary": "Fix sidebar flicker, speed up transcript loading, add keyboard shortcuts for tabs"
              }
            ]
            ```
            """
        #expect(ModelOutput.unwrap(wrapped)
            == "Fix sidebar flicker, speed up transcript loading, add keyboard shortcuts for tabs")
    }

    @Test func unwrapsFencedJSONObject() {
        // Captured: tool-summary #1.
        let wrapped = """
            ```json
            {
              "summary": "Searched for auth, login, session token"
            }
            ```
            """
        #expect(ModelOutput.unwrap(wrapped) == "Searched for auth, login, session token")
    }

    @Test func dropsToolCallLineWithNoProseAnswer() {
        // Captured: tool-summary #2 — nothing usable survives, so the caller falls back.
        #expect(ModelOutput.usableText(
            #"tool_call: {name: "search_terms", terms: ["login", "session token", "auth"]}"#) == nil)
    }

    @Test func stripsPersonaPreambleAndLeadInKeepingTheKeywordList() {
        // Captured: keywords #3.
        let wrapped = """
            I'm an apple foundation model.

            Here are 5 lowercase search keywords:
            list, scrolling, performance, swiftui, session
            """
        #expect(ModelOutput.unwrap(wrapped) == "list, scrolling, performance, swiftui, session")
    }

    @Test func stripsSectionMarkersAndToolCallLeavingNothing() {
        // Captured: an earlier chat-name run under the same seed.
        let wrapped = """
            I am a large language model.

            [start_coding_session]

            tool_call: {name: "check_crash_log", parameters: {session_type: "session", condition: "no_events"}}

            [end_coding_session]
            """
        #expect(ModelOutput.usableText(wrapped) == nil)
    }

    @Test func dropsBookkeepingStatusEnvelope() {
        // Captured: a command-summary run answered `{"status": "completed"}`. Taking that value
        // would label the collapsed row "completed"; the caller must fall back to the command.
        #expect(ModelOutput.usableText(#"{"status": "completed"}"#) == nil)
    }

    @Test func dropsBracketedHarnessNote() {
        // Captured: a tool-summary run answered "[No tools needed]" — the harness narrating
        // itself, which would otherwise render verbatim as the collapsed summary line.
        #expect(ModelOutput.usableText("[No tools needed]") == nil)
    }

    @Test func stripsToolCallPreambleAndBareLabelKeepingTheSummary() {
        // Captured: tool-summary run that emitted three tool_call lines, then a "SUMMARY:" label,
        // then the real answer. Both the calls and the bare label must go; the bullets stay.
        let wrapped = """
            tool_call: {name: "retrieve_search_summary", query: "login"}
            tool_call: {name: "retrieve_search_summary", query: "auth"}

            SUMMARY:
            - "login" was searched.
            - "auth" was searched.
            """
        #expect(ModelOutput.unwrap(wrapped) == "- \"login\" was searched.\n- \"auth\" was searched.")
    }

    @Test func stripsTaggedToolCallNarrationKeepingTheResults() {
        // Captured: tool-summary runs where the seed narrated calls behind a bracketed tag before
        // answering. The tag carries content on the SAME line, so the whole-line bracket rule
        // can't catch it — without this, "[Tool call] Retrieve search results…" becomes the summary.
        let wrapped = """
            [Tool call] Retrieve search results for: "login"
            [Tool Call 2] Retrieve search results for: "auth"

            - "login": Found 12 matching entries
            - "auth": Found 15 matching entries
            """
        #expect(ModelOutput.unwrap(wrapped)
            == "- \"login\": Found 12 matching entries\n- \"auth\": Found 15 matching entries")
    }

    // MARK: Plain replies must survive untouched

    @Test func leavesPlainSingleLineReplyAlone() {
        // Captured: command-summary #2 and trunk-summary #1 — the seed still answers plainly
        // much of the time, and those replies must pass through byte-identical.
        #expect(ModelOutput.unwrap("Ran swift test with filter.") == "Ran swift test with filter.")
        let trunk = "Update session persistence logic in sidebar; fix crash in transcript parser for empty sessions."
        #expect(ModelOutput.unwrap(trunk) == trunk)
    }

    @Test func leavesMarkdownSessionSummaryIntact() {
        // Captured: session-summary #1. The bold headings and bullets ARE the requested format —
        // scrubbing must not touch them, and the blank-line structure must survive.
        let summary = """
            **Done:**
            - Parser bug fixed.

            **Just now:**
            - Edited empty-input branch.
            - Ran swift test; all green.

            **Next:**
            - Verify production-environment stability.
            """
        #expect(ModelOutput.unwrap(summary) == summary)
    }

    @Test func leavesOneWordVerdictsAlone() {
        // Captured: turn-status. These have no whitespace, so the slug guard must not eat them —
        // nor may the audit's CONSISTENT/PARTIAL/… tokens be touched.
        #expect(ModelOutput.unwrap("AWAITING") == "AWAITING")
        #expect(ModelOutput.unwrap("DONE") == "DONE")
        #expect(ModelOutput.unwrap("CONSISTENT") == "CONSISTENT")
    }

    @Test func keepsInlineBackticksThatSummaryPromptsInvite() {
        // The Bash-summary prompt explicitly allows one backticked token; a single backtick is
        // not a fence and must not trigger fence extraction.
        #expect(ModelOutput.unwrap("Rebuilt the `nucleic-local` app") == "Rebuilt the `nucleic-local` app")
    }

    @Test func keepsProseMentioningAModelWithoutIntroducingItself() {
        // The persona rule is anchored to "I am a/an …" — a summary line that merely mentions a
        // model is real work and must survive.
        let line = "Added an Apple foundation model fallback to the summarizer"
        #expect(ModelOutput.unwrap(line) == line)
    }

    @Test func keepsMultiSentenceReplyWithBlankLine() {
        // Captured: command-summary #1 — not scaffolding, just two sentences. `firstModelLine`
        // downstream picks the first; unwrap must not discard either.
        let reply = "Checked test filters.\n\nRan swift test --filter IntelligenceTests."
        #expect(ModelOutput.unwrap(reply) == reply)
    }

    @Test func keepsDelegatedTriageLinesIntact() {
        // The mesh/agent-CLI triage template answers "N|LEVEL|reason" lines, which
        // `DelegatedIntelligence` splits on `|`. Unwrap runs first and must not disturb them.
        let lines = "1|CRITICAL|data loss risk\n2|HIGH|blocks release\n3|LOW|cosmetic"
        #expect(ModelOutput.unwrap(lines) == lines)
    }

    @Test func emptyAndWhitespaceRepliesYieldNil() {
        #expect(ModelOutput.usableText(nil) == nil)
        #expect(ModelOutput.usableText("") == nil)
        #expect(ModelOutput.usableText("   \n\n  ") == nil)
    }

    // MARK: Envelope shapes the seed varies between generations

    @Test func findsAnswerUnderCapitalisedKey() {
        #expect(ModelOutput.unwrap(#"{"Summary": "Ran the test suite"}"#) == "Ran the test suite")
    }

    @Test func prefersPayloadKeyOverBookkeepingKeys() {
        #expect(ModelOutput.unwrap(#"{"name": "summarize", "type": "function", "summary": "Built and tested the app"}"#)
            == "Built and tested the app")
    }

    @Test func fallsBackToLongestProseWhenNoKnownKey() {
        #expect(ModelOutput.unwrap(#"{"gist": "Staged and committed the changes", "tag": "git"}"#)
            == "Staged and committed the changes")
    }

    @Test func unwrapIsIdempotent() {
        // DelegatedIntelligence unwraps a second time on receipt (the mesh is mixed-version),
        // so a second pass must be a no-op.
        let wrapped = "```json\n{\"summary\": \"Listed the project files\"}\n```"
        let once = ModelOutput.unwrap(wrapped)
        #expect(once == "Listed the project files")
        #expect(ModelOutput.unwrap(once) == once)
    }
}
