import Testing
@testable import Kanban

struct RanksTests {

    // MARK: - Append

    @Test func appendOnEmptyLaneReturnsBoardConvention() {
        #expect(Ranks.append(toVisible: [Double]()) == 1024)
    }

    @Test func appendReturnsMaxPlusGap() {
        #expect(Ranks.append(toVisible: [1024, 2048, 512]) == 3072)
    }

    // MARK: - Insert at head

    @Test func insertAtHeadOnEmptyLaneReturnsBoardConvention() {
        #expect(Ranks.insertAtHead(ofVisible: [Double]()) == 1024)
    }

    @Test func insertAtHeadReturnsMinMinusGap() {
        #expect(Ranks.insertAtHead(ofVisible: [1024, 2048, 512]) == -512)
    }

    // MARK: - Midpoint

    @Test func midpointBetweenDistinctValuesIsStrictlyBetween() throws {
        let mid = try #require(Ranks.midpoint(between: 1024, and: 2048))
        #expect(mid == 1536)
        #expect(mid > 1024 && mid < 2048)
    }

    @Test func midpointIsOrderIndependent() {
        let forward = Ranks.midpoint(between: 1024, and: 2048)
        let reversed = Ranks.midpoint(between: 2048, and: 1024)
        #expect(forward == reversed)
    }

    @Test func midpointBetweenEqualValuesReturnsNil() {
        #expect(Ranks.midpoint(between: 42, and: 42) == nil)
    }

    @Test func midpointNeverEscapesTheOpenInterval() {
        // Values close enough that a naive (a+b)/2 could round to one of the
        // endpoints; the result (if any) must stay strictly inside.
        let a = 1.0
        let b = 1.0.nextUp.nextUp.nextUp
        if let mid = Ranks.midpoint(between: a, and: b) {
            #expect(mid > a && mid < b)
        }
    }

    // MARK: - Renumbered

    @Test func renumberedProducesWholeMultiplesOf1024() {
        #expect(Ranks.renumbered(count: 4) == [1024, 2048, 3072, 4096])
    }

    @Test func renumberedWithZeroCountIsEmpty() {
        #expect(Ranks.renumbered(count: 0) == [])
    }

    // MARK: - Display order / tie-break

    @Test func sortedForDisplayOrdersAscendingByRank() {
        let items: [(order: Double, name: String)] = [
            (order: 3072, name: "c-card"),
            (order: 1024, name: "a-card"),
            (order: 2048, name: "b-card"),
        ]
        let sorted = Ranks.sortedForDisplay(items, order: { $0.order }, name: { $0.name })
        #expect(sorted.map { $0.name } == ["a-card", "b-card", "c-card"])
    }

    @Test func sortedForDisplayBreaksTiesByFolderName() {
        let items: [(order: Double, name: String)] = [
            (order: 1024, name: "zzz-uuid"),
            (order: 1024, name: "aaa-uuid"),
            (order: 1024, name: "mmm-uuid"),
        ]
        let sorted = Ranks.sortedForDisplay(items, order: { $0.order }, name: { $0.name })
        #expect(sorted.map { $0.name } == ["aaa-uuid", "mmm-uuid", "zzz-uuid"])
    }

    @Test func isOrderedForDisplayMatchesSortedForDisplay() {
        let a: (order: Double, name: String) = (order: 1024, name: "aaa")
        let b: (order: Double, name: String) = (order: 1024, name: "bbb")
        #expect(Ranks.isOrderedForDisplay(a, before: b, order: { $0.order }, name: { $0.name }))
        #expect(!Ranks.isOrderedForDisplay(b, before: a, order: { $0.order }, name: { $0.name }))
    }

    // MARK: - Tombstone exclusion

    @Test func appendIgnoresTombstonedSiblings() {
        let items: [(order: Double, isDeleted: Bool)] = [
            (order: 1024, isDeleted: false),
            (order: 9999, isDeleted: true),
            (order: 2048, isDeleted: false),
        ]
        #expect(Ranks.append(toVisible: items) == 3072)
    }

    @Test func insertAtHeadIgnoresTombstonedSiblings() {
        let items: [(order: Double, isDeleted: Bool)] = [
            (order: 1024, isDeleted: false),
            (order: -9999, isDeleted: true),
            (order: 2048, isDeleted: false),
        ]
        #expect(Ranks.insertAtHead(ofVisible: items) == 0)
    }

    @Test func appendWithAllSiblingsTombstonedReturnsBoardConvention() {
        let items: [(order: Double, isDeleted: Bool)] = [
            (order: 1024, isDeleted: true),
            (order: 2048, isDeleted: true),
        ]
        #expect(Ranks.append(toVisible: items) == 1024)
    }

    @Test func insertAtHeadWithAllSiblingsTombstonedReturnsBoardConvention() {
        let items: [(order: Double, isDeleted: Bool)] = [
            (order: 1024, isDeleted: true),
            (order: 2048, isDeleted: true),
        ]
        #expect(Ranks.insertAtHead(ofVisible: items) == 1024)
    }

    // MARK: - Precision exhaustion → renumber, deterministically

    @Test func precisionExhaustionThenRenumberIsDeterministic() {
        func runScenario() -> (iterations: Int, renumbered: [Double]) {
            let lower = 1024.0
            var upper = 2048.0
            var iterations = 0
            let iterationCap = 4000
            while iterations < iterationCap, let mid = Ranks.midpoint(between: lower, and: upper) {
                upper = mid
                iterations += 1
            }
            return (iterations, Ranks.renumbered(count: 5))
        }

        let first = runScenario()
        let second = runScenario()

        // Precision must actually have been exhausted, not just hit the cap.
        #expect(first.iterations > 0)
        #expect(first.iterations < 4000)

        // Same scenario, run twice, must produce bit-identical results.
        #expect(first.iterations == second.iterations)
        #expect(first.renumbered == second.renumbered)

        // Renumbering yields clean whole multiples of 1024.
        #expect(first.renumbered == [1024, 2048, 3072, 4096, 5120])
    }
}
