import Foundation
import Testing
@testable import Workouts

/// Locks `RoutineOrdering.changedOrders` — the pure `.onMove` → renumbered-order math
/// shared by any list backed by a `SyncEngine` document write. Results are compared as
/// `"id:order"` strings since tuple arrays aren't `Equatable`.
struct RoutineOrderingTests {

    private func describe(_ changes: [(id: String, order: Int)]) -> [String] {
        changes.map { "\($0.id):\($0.order)" }
    }

    @Test func moveProducesMinimalChangedSet() {
        // A, B, C, D at orders 0...3; drag C (index 2) to the front. B and A shift by
        // one; D — already last — keeps its order and must NOT appear in the result.
        let changes = RoutineOrdering.changedOrders(
            ids: ["A", "B", "C", "D"],
            currentOrders: ["A": 0, "B": 1, "C": 2, "D": 3],
            from: IndexSet(integer: 2), to: 0
        )
        #expect(Set(describe(changes)) == Set(["C:0", "A:1", "B:2"]))
    }

    @Test func noOpMoveYieldsEmptyChangeSet() {
        // Dropping a row back where it started (or an app-level tap that fires the
        // same source/destination) must produce zero document churn.
        let changes = RoutineOrdering.changedOrders(
            ids: ["A", "B", "C", "D"],
            currentOrders: ["A": 0, "B": 1, "C": 2, "D": 3],
            from: IndexSet(integer: 0), to: 0
        )
        #expect(changes.isEmpty)
    }

    @Test func legacyDuplicateOrdersNormalizeOnFirstDrag() {
        // Legacy data where every row collided at order 0. Even a no-op array move
        // (A stays first) must renumber every row that isn't already at its correct
        // 0...n-1 position — the whole point of comparing against the stored value
        // rather than only the dragged row.
        let changes = RoutineOrdering.changedOrders(
            ids: ["A", "B", "C", "D"],
            currentOrders: ["A": 0, "B": 0, "C": 0, "D": 0],
            from: IndexSet(integer: 0), to: 0
        )
        #expect(Set(describe(changes)) == Set(["B:1", "C:2", "D:3"]))
    }

    @Test func idMissingFromCurrentOrdersIsAlwaysIncluded() {
        // A newly-added row with no prior stored order can't be assumed unchanged.
        let changes = RoutineOrdering.changedOrders(
            ids: ["A", "B"],
            currentOrders: ["A": 0],
            from: IndexSet(integer: 0), to: 0
        )
        #expect(Set(describe(changes)) == Set(["B:1"]))
    }

    @Test func moveToEndRenumbersOnlyDisplacedRows() {
        let changes = RoutineOrdering.changedOrders(
            ids: ["A", "B", "C", "D"],
            currentOrders: ["A": 0, "B": 1, "C": 2, "D": 3],
            from: IndexSet(integer: 0), to: 4
        )
        // A moves to the end; B, C, D each shift down one; nothing keeps its old order.
        #expect(Set(describe(changes)) == Set(["B:0", "C:1", "D:2", "A:3"]))
    }
}
