import Foundation
import Testing

@testable import NucleicCore

@Suite("Triage heuristics")
struct TriageTests {
    private func input(_ id: String, _ text: String) -> TriageInput {
        TriageInput(id: TodoID(rawValue: id), text: text)
    }

    @Test func extremeUrgencyPinsToTopRegardlessOfImpact() {
        let items = [
            input("a", "add a dark mode toggle to settings"),                       // low impact
            input("b", "refactor the auth architecture for scale and reliability"), // high impact
            input("c", "users hit a crash on launch, data loss reported"),          // critical
        ]
        let triaged = HeuristicTriage.triage(items)
        // The crash/data-loss idea pins to the very top even though (b) has the most
        // impact words — extreme urgency always wins.
        #expect(triaged.first?.id == TodoID(rawValue: "c"))
        #expect(triaged.first?.level == .critical)
        // Every idea gets exactly one verdict, ranked contiguously from the top.
        #expect(Set(triaged.map(\.id)) == Set(items.map(\.id)))
        #expect(triaged.map(\.rank) == [0, 1, 2])
    }

    @Test func impactOutranksMereRecency() {
        let triaged = HeuristicTriage.triage([
            input("plain", "rename a variable"),
            input("impactful", "improve onboarding conversion and retention for new users"),
        ])
        #expect(triaged.first?.id == TodoID(rawValue: "impactful"))
        #expect(triaged.first?.level == .high)
        #expect(triaged.first(where: { $0.id == TodoID(rawValue: "plain") })?.level == .low)
    }

    @Test func deterministicWithFifoTiebreak() {
        let items = [input("x", "tidy up"), input("y", "tidy up")]  // identical score
        let a = HeuristicTriage.triage(items)
        let b = HeuristicTriage.triage(items)
        #expect(a == b)  // deterministic
        // Equal scores keep input (FIFO) order.
        #expect(a.map(\.id) == [TodoID(rawValue: "x"), TodoID(rawValue: "y")])
    }

    @Test func emptyAndSingle() {
        #expect(HeuristicTriage.triage([]).isEmpty)
        let single = HeuristicTriage.triage([input("solo", "ship the release")])
        #expect(single.map(\.rank) == [0])
        #expect(single.first?.id == TodoID(rawValue: "solo"))
    }

    @Test func levelsSpanRelativelyEvenWithoutKeywords() {
        // None of these hit a keyword, so the old absolute thresholds collapsed them
        // all to Low. Levels are relative now, so the spectrum must not collapse.
        let items = (1...4).map { input("t\($0)", "todo number \($0)") }
        let triaged = HeuristicTriage.triage(items)
        #expect(Set(triaged.map(\.level)).count >= 2)  // a visible gradient, not all-Low
        // Top of the list sits strictly above the bottom.
        #expect(triaged.first!.level.severity > triaged.last!.level.severity)
        #expect(triaged.last!.level == .low)
    }

    @Test func modelRankingDrivesOrderAndLevels() {
        let items = (1...4).map { input("t\($0)", "idea \($0)") }
        // Model ranks them 3, 1, 4, 2 with varied levels.
        let ranked = [
            HeuristicTriage.ModelVerdict(number: 3, level: "CRITICAL", reason: "data loss"),
            HeuristicTriage.ModelVerdict(number: 1, level: "high", reason: "core flow"),
            HeuristicTriage.ModelVerdict(number: 4, level: "Medium", reason: "nice win"),
            HeuristicTriage.ModelVerdict(number: 2, level: "LOW", reason: "cosmetic"),
        ]
        let out = HeuristicTriage.fromModelRanking(ranked, items: items)
        // Order follows the model, not the input.
        #expect(out.map(\.id) == [TodoID(rawValue: "t3"), TodoID(rawValue: "t1"),
                                  TodoID(rawValue: "t4"), TodoID(rawValue: "t2")])
        #expect(out.map(\.rank) == [0, 1, 2, 3])
        #expect(out.first?.level == .critical)
        #expect(out.first?.reason == "data loss")
    }

    @Test func modelRankingAppendsDroppedAndIgnoresGarbage() {
        let items = (1...3).map { input("t\($0)", "idea \($0)") }
        // Model only ranked #2 (and emitted a bogus number + a duplicate to ignore).
        let ranked = [
            HeuristicTriage.ModelVerdict(number: 99, level: "HIGH", reason: "bogus"),
            HeuristicTriage.ModelVerdict(number: 2, level: "HIGH", reason: "real"),
            HeuristicTriage.ModelVerdict(number: 2, level: "LOW", reason: "dupe"),
        ]
        let out = HeuristicTriage.fromModelRanking(ranked, items: items)
        // Every idea still gets exactly one verdict; the ranked one leads.
        #expect(Set(out.map(\.id)) == Set(items.map(\.id)))
        #expect(out.first?.id == TodoID(rawValue: "t2"))
        #expect(out.map(\.rank) == [0, 1, 2])
    }

    @Test func modelRankingRespreadsWhenBunched() {
        let items = (1...4).map { input("t\($0)", "idea \($0)") }
        // Model ranked everything but flattened them all to MEDIUM.
        let ranked = (1...4).map {
            HeuristicTriage.ModelVerdict(number: $0, level: "MEDIUM", reason: "meh")
        }
        let out = HeuristicTriage.fromModelRanking(ranked, items: items)
        // Re-spread so the spectrum doesn't collapse, preserving the model's order.
        #expect(out.map(\.id) == items.map(\.id))
        #expect(Set(out.map(\.level)).count >= 2)
        #expect(out.first!.level.severity > out.last!.level.severity)
    }

    @Test func emptyModelRankingFallsBackToHeuristic() {
        let items = (1...3).map { input("t\($0)", "idea \($0)") }
        let out = HeuristicTriage.fromModelRanking([], items: items)
        #expect(out.map(\.id) == HeuristicTriage.triage(items).map(\.id))
    }

    @Test func levelTokenParsesLeniently() {
        #expect(TriageLevel(token: "CRITICAL") == .critical)
        #expect(TriageLevel(token: " high ") == .high)
        #expect(TriageLevel(token: "Med") == .medium)
        #expect(TriageLevel(token: "low") == .low)
        #expect(TriageLevel(token: "nonsense") == nil)
    }
}
