import Foundation
import NucleicProtocol
import Testing

@testable import NucleicCore

/// Contracts of the delegated intelligence provider (docs/COVALENCE_RUNNER.md §5, item 6):
/// per-kind request encoding, output parsing, the heuristics-as-guardrails discipline, and the
/// fail-closed host-exec audit — all against a scripted backend, so they hold identically for
/// the mesh and agent-CLI paths.
@Suite struct DelegatedIntelligenceTests {
    /// A backend scripted per test; records every request it's handed.
    private final class ScriptedBackend: IntelligenceGenerationBackend, @unchecked Sendable {
        private let lock = NSLock()
        private var recorded: [WireIntelligenceRequest] = []
        private let respond: @Sendable (WireIntelligenceRequest) -> WireIntelligenceResult?

        init(_ respond: @escaping @Sendable (WireIntelligenceRequest) -> WireIntelligenceResult?) {
            self.respond = respond
        }
        func run(_ request: WireIntelligenceRequest) async -> WireIntelligenceResult? {
            lock.withLock { recorded.append(request) }
            return respond(request)
        }
        var requests: [WireIntelligenceRequest] { lock.withLock { recorded } }
    }

    private func provider(
        _ respond: @escaping @Sendable (WireIntelligenceRequest) -> WireIntelligenceResult?
    ) -> (DelegatedIntelligenceProvider, ScriptedBackend) {
        let backend = ScriptedBackend(respond)
        return (DelegatedIntelligenceProvider(backend: backend), backend)
    }

    private func answer(_ text: String) -> @Sendable (WireIntelligenceRequest) -> WireIntelligenceResult? {
        { WireIntelligenceResult(id: $0.id, outputs: [text]) }
    }

    // MARK: Encoding

    @Test func encodesKindPriorityAndDeadline() async {
        let (provider, backend) = provider(answer("Ran the test suite"))
        _ = await provider.summarizeBashCommand("swift test")
        let request = backend.requests.first
        #expect(request?.kind == .summarizeBashCommand)
        #expect(request?.inputs == ["swift test"])
        #expect(request?.priority == .bashSummary)
        #expect(request?.deadlineSeconds == DelegatedIntelligenceProvider.deadline(for: .bashSummary))
    }

    @Test func auditRunsInteractiveAndNamingIsInteractive() async {
        let (provider, backend) = provider { _ in nil }
        _ = await provider.auditHostExecReason(command: "xcodebuild", reason: "needs Xcode")
        _ = await provider.sessionName(fromFirstMessage: "fix the login button")
        #expect(backend.requests.map(\.priority) == [.interactive, .interactive])
        #expect(backend.requests.map(\.kind) == [.auditHostExecReason, .sessionName])
    }

    // MARK: Parsing + fallbacks

    @Test func classifyTurnParsesAndFallsBack() async {
        let (awaiting, _) = provider(answer("AWAITING"))
        #expect(await awaiting.classifyTurn(lastReply: "Should I proceed with the merge?") == .awaitingInput)

        let (done, _) = provider(answer("DONE"))
        #expect(await done.classifyTurn(lastReply: "All finished.") == .completed)

        // Backend can't serve → the shape-based heuristic decides.
        let (offline, _) = provider { _ in nil }
        #expect(await offline.classifyTurn(lastReply: "Which option do you prefer?") == .awaitingInput)
    }

    @Test func bashSummaryRejectsEcho() async {
        // A parroted command dressed up as a summary is rejected for the raw command.
        let (echoing, _) = provider(answer("Ran swift test"))
        #expect(await echoing.summarizeBashCommand("swift test") == "swift test")

        let (good, _) = provider(answer("Ran the test suite"))
        #expect(await good.summarizeBashCommand("swift test") == "Ran the test suite")
    }

    @Test func mergeRejectsUngroundedLine() async {
        // The classic small-model failure: parroting an instruction example that shares no
        // content word with the gists → the deterministic merge instead.
        let gists = ["Listed the project files", "Searched the codebase"]
        let (parroting, _) = provider(answer("Built and tested the app"))
        #expect(await parroting.mergeBashSummaries(gists) == HeuristicSummary.joinBashSummaries(gists))

        let (grounded, _) = provider(answer("Looked through the project files and searched it"))
        #expect(await grounded.mergeBashSummaries(gists) == "Looked through the project files and searched it")
    }

    @Test func sessionNameSanitizesScaffoldingAndFallsBack() async {
        let (scaffolded, _) = provider(answer("Title: \"Fix Login Button Tap\"\nRequest: next"))
        #expect(await scaffolded.sessionName(fromFirstMessage: "fix the login button") == "Fix Login Button Tap")

        // A refusal never becomes a title — the message-derived heuristic name does.
        let (refusing, _) = provider(answer("I'm sorry, I cannot help with that"))
        let fallback = await refusing.sessionName(fromFirstMessage: "fix the login button please now")
        #expect(fallback == HeuristicTitle.fromMessage("fix the login button please now"))
    }

    @Test func triageParsesStrictLinesAndFallsBackOnGarbage() async {
        let items = [
            TriageInput(id: TodoID(rawValue: "a"), text: "rename a button"),
            TriageInput(id: TodoID(rawValue: "b"), text: "fix data-loss crash"),
        ]
        let (ranked, _) = provider(answer("2|CRITICAL|data loss\n1|LOW|cosmetic"))
        let triaged = await ranked.triageTodos(items, group: "p1")
        #expect(triaged.map(\.id) == [TodoID(rawValue: "b"), TodoID(rawValue: "a")])
        #expect(triaged.first?.level == .critical)
        #expect(triaged.first?.reason == "data loss")

        // Unparseable output → the deterministic heuristic still ranks everything.
        let (garbled, _) = provider(answer("no idea what you want"))
        #expect(await garbled.triageTodos(items, group: "p1").count == 2)
    }

    @Test func auditFailsClosed() async {
        // No backend answer → notChecked (never a spoofable clean bill).
        let (offline, _) = provider { _ in nil }
        let unchecked = await offline.auditHostExecReason(command: "rm -rf /", reason: "cleanup")
        #expect(unchecked.trust == .notChecked)

        // A device that answered with error set → still notChecked.
        let (erroring, _) = provider { WireIntelligenceResult(id: $0.id, error: "model off") }
        #expect(await erroring.auditHostExecReason(command: "ls", reason: "list").trust == .notChecked)

        // A parseable verdict maps through; a garbled token degrades to unclear, never trusted.
        let (contradictory, _) = provider(answer("CONTRADICTORY|claims host need for container work"))
        let verdict = await contradictory.auditHostExecReason(command: "npm test", reason: "needs Xcode")
        #expect(verdict.trust == .contradictory)
        #expect(verdict.rationale == "claims host need for container work")

        let (garbled, _) = provider(answer("TRUST ME|fine"))
        #expect(await garbled.auditHostExecReason(command: "ls", reason: "list").trust == .unclear)

        // Empty command/reason never even reaches the backend.
        let (untouched, backend) = provider(answer("CONSISTENT|ok"))
        #expect(await untouched.auditHostExecReason(command: "", reason: "x").trust == .notChecked)
        #expect(backend.requests.isEmpty)
    }

    @Test func trunkSummaryUsesFirstLineAndFallsBack() async {
        let (model, _) = provider(answer("Add login retry handling\nextra chatter"))
        let updated = await model.updateTrunkSummary(previous: "", with: "Goal: add retry")
        #expect(updated == "Add login retry handling")

        let (offline, _) = provider { _ in nil }
        let folded = await offline.updateTrunkSummary(previous: "", with: "Goal: add retry")
        #expect(folded == HeuristicSummary.foldTrunkSummary(previous: "", digest: "Goal: add retry"))
    }

    // MARK: Device-side executor

    @Test func executorAnswersErrorWithoutAModel() async {
        // HeuristicIntelligence has no generateText — the executor must answer with error set
        // (so the delegating host falls back immediately), never an empty success.
        let request = WireIntelligenceRequest(
            id: "r1", kind: .classifyTurn, inputs: ["done"], priority: .completion)
        let result = await IntelligenceDelegateExecutor.execute(
            request, provider: HeuristicIntelligence())
        #expect(result.id == "r1")
        #expect(result.error != nil)
        #expect(result.outputs.isEmpty)
    }

    @Test func executorGeneratesThroughProviderAtWirePriority() async {
        struct FakeModelProvider: IntelligenceProviding {
            let record: @Sendable (String, String, AFMRequestQueue.Priority) -> Void
            func summarize(session: Session, events: [AgentEvent]) async -> String { "" }
            func sessionName(fromFirstMessage message: String) async -> String? { nil }
            func generateText(
                instructions: String, prompt: String, priority: AFMRequestQueue.Priority
            ) async -> String? {
                record(instructions, prompt, priority)
                return "DONE"
            }
        }
        let recorded = LockedBox<(String, String, AFMRequestQueue.Priority)?>(nil)
        let request = WireIntelligenceRequest(
            id: "r2", kind: .classifyTurn, inputs: ["all merged"], priority: .completion)
        let result = await IntelligenceDelegateExecutor.execute(
            request,
            provider: FakeModelProvider(record: { recorded.set(($0, $1, $2)) }))
        #expect(result.outputs == ["DONE"])
        #expect(result.error == nil)
        let seen = recorded.get()
        #expect(seen?.2 == .completion)
        #expect(seen?.1 == "all merged")  // the shared template's prompt for classifyTurn
    }

    @Test func unknownKindAnswersError() async {
        let request = WireIntelligenceRequest(
            id: "r3", kind: IntelligenceRequestKind(rawValue: "someFutureKind"), inputs: [])
        let result = await IntelligenceDelegateExecutor.execute(
            request, provider: HeuristicIntelligence())
        #expect(result.error?.contains("someFutureKind") == true)
    }
}

