import Foundation
import Testing

import RunnerCore

struct RunnerCoreTests {

    // MARK: - AgeGroup

    @Test func ageGroupDescription() {
        #expect(AgeGroup.junior.description == "Junior")
        #expect(AgeGroup.adult.description == "Adult")
        #expect(AgeGroup.senior.description == "Senior")
    }

    @Test func ageGroupRawValueInit() {
        #expect(AgeGroup(rawValue: 1) == .junior)
        #expect(AgeGroup(rawValue: 3) == .senior)
        #expect(AgeGroup(rawValue: 0) == nil)
        #expect(AgeGroup(rawValue: 4) == nil)
    }

    // MARK: - SortKey.sort

    private func runner(_ name: String, _ group: AgeGroup, _ time: String, _ nr: Int) -> Runner {
        Runner(name: name, ageGroup: group, time: time, number: nr)
    }

    @Test func sortKeyByName() {
        let a = runner("Bob", .adult, "10:00", 2)
        let b = runner("Ann", .senior, "09:00", 1)
        #expect(SortKey.name.sort(b, a))
        #expect(!SortKey.name.sort(a, b))
    }

    @Test func sortKeyByAgeGroup() {
        let junior = runner("X", .junior, "10:00", 5)
        let senior = runner("A", .senior, "09:00", 1)
        #expect(SortKey.ageGroup.sort(junior, senior))
        #expect(!SortKey.ageGroup.sort(senior, junior))
    }

    @Test func sortKeyByTime() {
        let fast = runner("X", .adult, "09:00", 2)
        let slow = runner("A", .adult, "10:00", 1)
        #expect(SortKey.time.sort(fast, slow))
    }

    @Test func sortKeyByNumber() {
        let first = runner("Z", .adult, "10:00", 1)
        let second = runner("A", .adult, "09:00", 2)
        #expect(SortKey.number.sort(first, second))
    }

    // MARK: - readFile

    @Test func readFileReturnsContents() throws {
        let dir = FileManager.default.temporaryDirectory
        let url = dir.appendingPathComponent("runnercore-test-\(UUID().uuidString).txt")
        try "hello\nworld".write(to: url, atomically: true, encoding: .utf8)
        defer { try? FileManager.default.removeItem(at: url) }

        #expect(readFile(url.path) == "hello\nworld")
    }

    @Test func readFileMissingReturnsEmpty() {
        #expect(readFile("/no/such/path/\(UUID().uuidString)") == "")
    }

    // MARK: - parseResults

    @Test func parseResultsValidLines() {
        let content = "1\t09:30\n2\t10:15"
        let results = parseResults(content, separator: "\t")
        #expect(results == [1: "09:30", 2: "10:15"])
    }

    @Test func parseResultsSkipsMalformed() {
        let content = "1\t09:30\nnope\n\n3"  // "nope" non-int, empty, "3" has <2 cols
        let results = parseResults(content, separator: "\t")
        #expect(results == [1: "09:30"])
    }

    @Test func parseResultsDuplicateKeyKeepsLast() {
        let content = "1\tfirst\n1\tsecond"
        let results = parseResults(content, separator: "\t")
        #expect(results == [1: "second"])
    }

    // MARK: - parseStarters

    @Test func parseStartersValidLines() {
        let content = "1\t2\tAlice\n7\t3\tBob"
        let starters = parseStarters(content, separator: "\t")
        #expect(starters.count == 2)
        #expect(starters[0].nr == 1)
        #expect(starters[0].ageGroup == 2)
        #expect(starters[0].name == "Alice")
        #expect(starters[1].name == "Bob")
    }

    @Test func parseStartersSkipsMalformed() {
        let content = "1\t2\tAlice\nx\t2\tBad\n5\t2"  // non-int nr, too few cols
        let starters = parseStarters(content, separator: "\t")
        #expect(starters.count == 1)
        #expect(starters[0].name == "Alice")
    }

    // MARK: - assembleRunners

    @Test func assembleRunnersMatchesResults() {
        let starters = [(nr: 1, ageGroup: 2, name: "Alice"), (nr: 2, ageGroup: 3, name: "Bob")]
        let results = [1: "09:30", 2: "10:00"]
        let runners = assembleRunners(starters: starters, results: results)
        #expect(runners.count == 2)
        #expect(runners[0] == Runner(name: "Alice", ageGroup: .adult, time: "09:30", number: 1))
    }

    @Test func assembleRunnersSkipsMissingResultAndBadGroup() {
        let starters = [
            (nr: 1, ageGroup: 2, name: "Alice"),
            (nr: 2, ageGroup: 9, name: "Bob"),
        ]
        let results = [2: "10:00"]
        let runners = assembleRunners(starters: starters, results: results)
        #expect(runners.isEmpty)
    }

    // MARK: - nonParticipants

    @Test func nonParticipantsGetDNS() {
        let starters = [(nr: 1, ageGroup: 2, name: "Alice"), (nr: 2, ageGroup: 3, name: "Bob")]
        let results = [2: "10:00"]  // only Bob finished
        let absent = nonParticipants(starters: starters, results: results)
        #expect(absent == [Runner(name: "Alice", ageGroup: .adult, time: "DNS", number: 1)])
    }

    @Test func nonParticipantsSkipsBadGroup() {
        let starters = [(nr: 1, ageGroup: 9, name: "Ghost")]
        let absent = nonParticipants(starters: starters, results: [:])
        #expect(absent.isEmpty)
    }

    // MARK: - visibleRunners

    private var sample: [Runner] {
        [
            runner("Alice", .junior, "09:00", 1),
            runner("Bob", .adult, "10:00", 2),
            runner("Charlie", .senior, "08:00", 3),
        ]
    }

    @Test func visibleRunnersFiltersByGroup() {
        let visible = visibleRunners(
            sample, query: "", includedGroups: [.adult], sortKey: .number)
        #expect(visible.map(\.name) == ["Bob"])
    }

    @Test func visibleRunnersQueryIsCaseInsensitive() {
        let visible = visibleRunners(
            sample, query: "aLiC", includedGroups: Set(AgeGroup.allCases), sortKey: .number)
        #expect(visible.map(\.name) == ["Alice"])
    }

    @Test func visibleRunnersEmptyQueryReturnsAllSorted() {
        let visible = visibleRunners(
            sample, query: "", includedGroups: Set(AgeGroup.allCases), sortKey: .time)
        #expect(visible.map(\.name) == ["Charlie", "Alice", "Bob"])
    }

    @Test func visibleRunnersEmptyWhenNoGroups() {
        let visible = visibleRunners(
            sample, query: "", includedGroups: [], sortKey: .number)
        #expect(visible.isEmpty)
    }

    // MARK: - toggling

    @Test func togglingAddsWhenAbsent() {
        #expect(toggling([.junior], .adult) == [.junior, .adult])
    }

    @Test func togglingRemovesWhenPresent() {
        #expect(toggling([.junior, .adult], .adult) == [.junior])
    }

    // MARK: - menuLabel

    @Test func menuLabelOn() {
        #expect(menuLabel("Name", on: true) == "[Name]")
    }

    @Test func menuLabelOff() {
        #expect(menuLabel("Name", on: false) == " Name ")
    }

    // MARK: - formatRow

    @Test func formatRowPadsColumns() {
        let row = formatRow("#1", "Alice", "Junior", "09:00")
        #expect(row == "#1    Alice                 Junior   09:00")
        #expect(row == "#1".padding(toLength: 6, withPad: " ", startingAt: 0)
            + "Alice".padding(toLength: 22, withPad: " ", startingAt: 0)
            + "Junior".padding(toLength: 9, withPad: " ", startingAt: 0)
            + "09:00")
    }

    @Test func formatRowTruncatesLongFields() {
        // padding(toLength:) truncates when the source is longer than the target.
        let longName = String(repeating: "x", count: 30)
        let row = formatRow("#1", longName, "Junior", "09:00")
        #expect(row.contains(String(repeating: "x", count: 22)))
        #expect(!row.contains(String(repeating: "x", count: 23)))
    }
}
