import Foundation
import NucleicProtocol
import Testing

@testable import NucleicCore

/// Contracts of the purpose classifier's layers: the deterministic keyword layer's accuracy
/// floor on the labeled fixture set, the precision requirement on its HIGH-confidence fast
/// path (a HIGH verdict skips the model entirely, so it must never be wrong), the strict
/// `LABEL|CONFIDENCE|reason` parsing, and the delegated provider's layered acceptance rules —
/// all against a scripted backend so they hold identically for the mesh and agent-CLI paths.
@Suite struct PromptPurposeClassifierTests {
    // MARK: Layer 0 — deterministic heuristics

    /// The keyword layer alone must label at least 70% of the non-adversarial fixtures
    /// correctly — it's the offline/model-off floor, and the send-path fallback when a
    /// classification hasn't landed in time.
    @Test func heuristicAccuracyFloorOnFixtures() throws {
        let fixtures = try PurposeFixtures.load().filter { $0.purpose != "general" }
        var correct = 0
        var misses: [String] = []
        for fixture in fixtures {
            let verdict = HeuristicPurposeClassifier.classify(fixture.prompt)
            if verdict.purpose.rawValue == fixture.purpose {
                correct += 1
            } else {
                misses.append("\(fixture.prompt) → \(verdict.purpose) (want \(fixture.purpose))")
            }
        }
        let accuracy = Double(correct) / Double(fixtures.count)
        #expect(
            accuracy >= 0.70,
            "heuristic floor broken: \(correct)/\(fixtures.count)\n\(misses.joined(separator: "\n"))")
    }

    /// A HIGH-confidence heuristic verdict is final — no model gets to overrule it — so on
    /// the labeled set it must be *precise*: every HIGH verdict matches its label. (Recall
    /// is the floor test above; HIGH is allowed to be rare, never wrong.)
    @Test func highConfidenceHeuristicIsAlwaysRight() throws {
        for fixture in try PurposeFixtures.load() where fixture.purpose != "general" {
            let verdict = HeuristicPurposeClassifier.classify(fixture.prompt)
            if verdict.confidence == .high {
                #expect(
                    verdict.purpose.rawValue == fixture.purpose,
                    "HIGH but wrong: \(fixture.prompt) → \(verdict.purpose)")
            }
        }
    }

    @Test func emptyAndBlankPromptsAreGeneral() {
        #expect(HeuristicPurposeClassifier.classify("") == .general)
        #expect(HeuristicPurposeClassifier.classify("   \n  ") == .general)
    }

    @Test func pastedErrorShapeReadsAsDebugging() {
        let verdict = HeuristicPurposeClassifier.classify(
            "Thread 1: Fatal error: Unexpectedly found nil at AppDelegate.swift:88")
        #expect(verdict.purpose == .debugging)
    }

    // MARK: The strict model-output line

    @Test func modelLineParsing() {
        let good = PurposeVerdict.fromModelLine("BACKEND|HIGH|new API endpoint", source: .agent)
        #expect(good?.purpose == .backendImpl)
        #expect(good?.confidence == .high)
        #expect(good?.reason == "new API endpoint")
        #expect(good?.source == .agent)
        // Case/whitespace forgiveness, missing tail fields default LOW/"".
        #expect(PurposeVerdict.fromModelLine(" frontend ", source: .afm)?.purpose == .frontendImpl)
        #expect(PurposeVerdict.fromModelLine("REVIEW", source: .afm)?.confidence == .low)
        // Off-menu labels never parse — GENERAL is a fallback bucket, not a model answer.
        #expect(PurposeVerdict.fromModelLine("GENERAL|HIGH|shrug", source: .agent) == nil)
        #expect(PurposeVerdict.fromModelLine("I think this is backend work", source: .agent) == nil)
        #expect(PurposeVerdict.fromModelLine("", source: .agent) == nil)
    }

    @Test func classifierTokensRoundTrip() {
        for purpose in PromptPurpose.classifiable {
            #expect(PromptPurpose.fromClassifierToken(purpose.classifierToken) == purpose)
        }
        #expect(PromptPurpose.fromClassifierToken("GENERAL") == nil)
    }

    // MARK: Delegated provider layering

    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 } }
    }

    /// An unambiguous keyword verdict skips the model entirely — the fast path must not
    /// spend a generation on "refactor and dedupe"-shaped prompts.
    @Test func highConfidenceHeuristicSkipsTheModel() async {
        let backend = ScriptedBackend { _ in nil }
        let provider = DelegatedIntelligenceProvider(backend: backend)
        let verdict = await provider.classifyPromptPurpose(
            prompt: "Refactor and dedupe the validation code")
        #expect(verdict.purpose == .refactor)
        #expect(verdict.source == .heuristic)
        #expect(backend.requests.isEmpty)
    }

    @Test func confidentModelAnswerWins() async {
        let backend = ScriptedBackend {
            WireIntelligenceResult(id: $0.id, outputs: ["BACKEND|HIGH|server-side feature"])
        }
        let provider = DelegatedIntelligenceProvider(backend: backend)
        let verdict = await provider.classifyPromptPurpose(
            prompt: "wire up the thing so saved drafts survive a restart")
        #expect(verdict.purpose == .backendImpl)
        #expect(verdict.source == .agent)
        #expect(backend.requests.map(\.kind) == [.classifyPromptPurpose])
        #expect(backend.requests.first?.priority == .interactive)
    }

    /// A LOW model answer only corroborates: agreeing it's accepted, disagreeing it loses
    /// to the heuristic.
    @Test func lowConfidenceModelAnswerMustAgree() async {
        let disagree = ScriptedBackend {
            WireIntelligenceResult(id: $0.id, outputs: ["WRITING|LOW|not sure"])
        }
        let heuristic = await DelegatedIntelligenceProvider(backend: disagree)
            .classifyPromptPurpose(prompt: "sort out the flaky login test on the release branch")
        #expect(heuristic.source == .heuristic)

        let agree = ScriptedBackend {
            WireIntelligenceResult(id: $0.id, outputs: ["DEBUGGING|LOW|flaky test mention"])
        }
        let corroborated = await DelegatedIntelligenceProvider(backend: agree)
            .classifyPromptPurpose(prompt: "sort out the flaky login test on the release branch")
        #expect(corroborated.source == .agent)
        #expect(corroborated.purpose == .debugging)
    }

    @Test func garbledModelOutputFallsBackToHeuristic() async {
        let backend = ScriptedBackend {
            WireIntelligenceResult(id: $0.id, outputs: ["Sure! Let me take a look at that."])
        }
        let provider = DelegatedIntelligenceProvider(backend: backend)
        let verdict = await provider.classifyPromptPurpose(
            prompt: "look over the export flow when you get a chance")
        #expect(verdict.source == .heuristic)
    }

    /// The delegated executor template renders for the new wire kind (additive — an older
    /// executor answers "unsupported kind" and the host falls back to heuristics).
    @Test func delegateTemplateRendersForClassifyPromptPurpose() {
        let request = WireIntelligenceRequest(
            id: "x", kind: .classifyPromptPurpose, inputs: ["make the toolbar a bit nicer"],
            priority: .interactive)
        let job = IntelligenceDelegate.job(for: request)
        #expect(job != nil)
        #expect(job?.instructions.contains("text classifier") == true)
        #expect(job?.prompt.contains("make the toolbar a bit nicer") == true)
        // Every label token the parser accepts appears in the template, so the twins can't
        // drift silently.
        for purpose in PromptPurpose.classifiable {
            #expect(job?.instructions.contains(purpose.classifierToken) == true)
        }
    }

    // MARK: End-to-end eval (opt-in — needs a logged-in `claude` CLI on this machine)

    /// The full layered accuracy gate from the plan: ≥90% on the labeled set through the
    /// real one-shot small-SKU path. Run manually before shipping:
    /// `NUCLEIC_PURPOSE_EVAL=1 swift test --filter PromptPurposeClassifierTests`
    /// (The AFM layer is exercised in-app; this pins the escalation layer + prompt twins.)
    @Test(
        .enabled(if: ProcessInfo.processInfo.environment["NUCLEIC_PURPOSE_EVAL"] == "1"),
        .timeLimit(.minutes(30)))
    func layeredEvalOnFixtures() async throws {
        let fixtures = try PurposeFixtures.load().filter { $0.purpose != "general" }
        let provider = DelegatedIntelligenceProvider.agent()
        var correct = 0
        var misses: [String] = []
        for fixture in fixtures {
            let verdict = await provider.classifyPromptPurpose(prompt: fixture.prompt)
            if verdict.purpose.rawValue == fixture.purpose {
                correct += 1
            } else {
                misses.append("\(fixture.prompt) → \(verdict.purpose) (want \(fixture.purpose))")
            }
        }
        let accuracy = Double(correct) / Double(fixtures.count)
        #expect(
            accuracy >= 0.90,
            "eval gate: \(correct)/\(fixtures.count)\n\(misses.joined(separator: "\n"))")
    }
}
