// // ItineraryReorderingTests.swift // SportsTimeTests // // Tests for item reordering within and across days. // import XCTest @testable import SportsTime private typealias H = ItineraryTestHelpers @MainActor final class ItineraryReorderingTests: XCTestCase { private let testDate = H.testDate // MARK: - Same Day Reordering Tests func test_reorderItems_withinSameDay_preservesCorrectOrder() { // Given: 3 items on Day 1: A(1.0), B(2.0), C(3.0) // When: Move C between A and B // Then: New sortOrder for C should be 1.5 let itemA = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "A") let itemB = H.makeCustomItem(day: 1, sortOrder: 2.0, title: "B") let itemC = H.makeCustomItem(day: 1, sortOrder: 3.0, title: "C") let dayData = ItineraryDayData( id: 1, dayNumber: 1, date: testDate, games: [], items: [.customItem(itemA), .customItem(itemB), .customItem(itemC)], travelBefore: nil ) var capturedSortOrder: Double = 0 let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, _, sortOrder in capturedSortOrder = sortOrder } controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [itemA, itemB, itemC]) // Rows: 0=header, 1=A(1.0), 2=B(2.0), 3=C(3.0) // Move C (row 3) to row 2 (between A and B) controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 3, section: 0), to: IndexPath(row: 2, section: 0)) XCTAssertEqual(capturedSortOrder, 1.5, accuracy: 0.01, "Moving C between A(1.0) and B(2.0) should give sortOrder 1.5") } func test_reorderItems_moveFirstToLast() { // Given: Items A(1.0), B(2.0), C(3.0) // When: Move A after C // Then: New sortOrder for A should be 4.0 (last + 1.0) let itemA = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "A") let itemB = H.makeCustomItem(day: 1, sortOrder: 2.0, title: "B") let itemC = H.makeCustomItem(day: 1, sortOrder: 3.0, title: "C") let dayData = ItineraryDayData( id: 1, dayNumber: 1, date: testDate, games: [], items: [.customItem(itemA), .customItem(itemB), .customItem(itemC)], travelBefore: nil ) var capturedSortOrder: Double = 0 let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, _, sortOrder in capturedSortOrder = sortOrder } controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [itemA, itemB, itemC]) // Rows: 0=header, 1=A(1.0), 2=B(2.0), 3=C(3.0) // Move A (row 1) to row 3 (after C) // After removing A: 0=header, 1=B, 2=C // Insert at row 3: 0=header, 1=B, 2=C, 3=A controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 1, section: 0), to: IndexPath(row: 3, section: 0)) XCTAssertEqual(capturedSortOrder, 4.0, accuracy: 0.01, "Moving A after C(3.0) should give sortOrder 4.0") } func test_reorderItems_moveLastToFirst() { // Given: Items A(2.0), B(4.0), C(6.0) // When: Move C before A // Then: New sortOrder for C should be 1.0 (first / 2.0) let itemA = H.makeCustomItem(day: 1, sortOrder: 2.0, title: "A") let itemB = H.makeCustomItem(day: 1, sortOrder: 4.0, title: "B") let itemC = H.makeCustomItem(day: 1, sortOrder: 6.0, title: "C") let dayData = ItineraryDayData( id: 1, dayNumber: 1, date: testDate, games: [], items: [.customItem(itemA), .customItem(itemB), .customItem(itemC)], travelBefore: nil ) var capturedSortOrder: Double = 0 let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, _, sortOrder in capturedSortOrder = sortOrder } controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [itemA, itemB, itemC]) // Rows: 0=header, 1=A(2.0), 2=B(4.0), 3=C(6.0) // Move C (row 3) to row 1 (before A, after header) controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 3, section: 0), to: IndexPath(row: 1, section: 0)) XCTAssertEqual(capturedSortOrder, 1.0, accuracy: 0.01, "Moving C before A(2.0) should give sortOrder 1.0 (first/2)") } // MARK: - Non-Reorderable Item Tests func test_games_cannotBeMoved() { // Games should always return false for canMoveRowAt let games = [H.makeRichGame(city: "Detroit", hour: 19)] let dayData = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: games, items: [], travelBefore: nil) let controller = ItineraryTableViewController(style: .plain) controller.reloadData(days: [dayData], travelValidRanges: [:]) // Row 0 = header, Row 1 = games XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 1, section: 0)), "Games should not be movable") } func test_header_cannotBeMoved() { let dayData = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil) let controller = ItineraryTableViewController(style: .plain) controller.reloadData(days: [dayData], travelValidRanges: [:]) XCTAssertFalse(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 0, section: 0)), "Header should not be movable") } // MARK: - Callback Tests func test_moveHeader_doesNotCallCallback() { // Headers can't be moved, but verify no callback fires let dayData = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil) var callbackCalled = false let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, _, _ in callbackCalled = true } controller.reloadData(days: [dayData], travelValidRanges: [:]) // Try to move header (shouldn't be possible since canMoveRowAt returns false) // But if someone calls moveRowAt directly: controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 0, section: 0), to: IndexPath(row: 0, section: 0)) XCTAssertFalse(callbackCalled, "Moving a header should not call any callback") } func test_moveGames_doesNotCallCallback() { let games = [H.makeRichGame(city: "Detroit", hour: 19)] let dayData = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: games, items: [], travelBefore: nil) var callbackCalled = false let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, _, _ in callbackCalled = true } controller.onTravelMoved = { _, _, _ in callbackCalled = true } controller.reloadData(days: [dayData], travelValidRanges: [:]) // Rows: 0=header, 1=games // Try to move games directly (shouldn't be possible) controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 1, section: 0), to: IndexPath(row: 0, section: 0)) XCTAssertFalse(callbackCalled, "Moving games should not call any callback") } }