// // ItineraryCustomItemTests.swift // SportsTimeTests // // Tests for custom item movement and constraints. // import XCTest @testable import SportsTime private typealias H = ItineraryTestHelpers final class ItineraryCustomItemTests: XCTestCase { private let testTripId = H.testTripId private let testDate = H.testDate // MARK: - Custom Item Movement Tests func test_customItem_canMoveToAnyDay() { // Given: A 5-day trip let constraints = ItineraryConstraints(tripDayCount: 5, items: []) let itineraryItem = ItineraryItem(tripId: testTripId, day: 1, sortOrder: 1.0, kind: .custom(CustomInfo(title: "Dinner", icon: "🍽️"))) // Custom items can go on any day for day in 1...5 { XCTAssertTrue(constraints.isValidPosition(for: itineraryItem, day: day, sortOrder: 50), "Custom item should be valid on Day \(day)") } } func test_customItem_canMoveBeforeOrAfterGames() { // Given: A day with a game at sortOrder 100 let game = H.makeGameItem(city: "Detroit", day: 2, sortOrder: 100) let constraints = ItineraryConstraints(tripDayCount: 3, items: [game]) let customItem = ItineraryItem(tripId: testTripId, day: 2, sortOrder: 50, kind: .custom(CustomInfo(title: "Breakfast", icon: "🍳"))) // Before game (sortOrder 50) - VALID XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 50), "Custom item before game should be valid") // After game (sortOrder 150) - VALID XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 150), "Custom item after game should be valid") } func test_customItem_cannotBeMovedOutsideTripRange() { // Given: A 3-day trip let constraints = ItineraryConstraints(tripDayCount: 3, items: []) let customItem = ItineraryItem(tripId: testTripId, day: 1, sortOrder: 1.0, kind: .custom(CustomInfo(title: "Test", icon: "⭐"))) // Day 0 (before trip) - INVALID XCTAssertFalse(constraints.isValidPosition(for: customItem, day: 0, sortOrder: 50), "Day 0 should be invalid") // Day 4 (after trip) - INVALID XCTAssertFalse(constraints.isValidPosition(for: customItem, day: 4, sortOrder: 50), "Day 4 should be invalid (trip is 3 days)") } // MARK: - Move Validation Tests func test_moveValidation_customItem_blockedFromRow0() { // Row 0 should always be blocked for drops let customItem = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "Moving") let dayData = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [.customItem(customItem)], travelBefore: nil) let controller = ItineraryTableViewController(style: .plain) controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [customItem]) // Attempt to move item to row 0 let source = IndexPath(row: 1, section: 0) let proposed = IndexPath(row: 0, section: 0) let result = controller.tableView(controller.tableView, targetIndexPathForMoveFromRowAt: source, toProposedIndexPath: proposed) // Should NOT allow row 0 XCTAssertNotEqual(result.row, 0, "Row 0 should be blocked for drops") } // MARK: - Cross-Day Movement Tests func test_moveItemBetweenDays_updatesDay() { // Given: Item on Day 1, move to Day 2 let item = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "Moving") let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [.customItem(item)], travelBefore: nil) let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [], travelBefore: nil) var capturedDay: Int = 0 let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, day, _ in capturedDay = day } controller.reloadData(days: [day1, day2], travelValidRanges: [:], itineraryItems: [item]) // Rows: 0=Day1 header, 1=item, 2=Day2 header // Move item (row 1) to after Day2 header (row 2 becomes row 2 after move) controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 1, section: 0), to: IndexPath(row: 2, section: 0)) XCTAssertEqual(capturedDay, 2, "Item should now belong to Day 2") } func test_moveItem_fromLastDayToFirstDay() { // Given: 3-day trip with item on Day 3 // When: Moving to Day 1 // Then: Day should be 1 let item = H.makeCustomItem(day: 3, sortOrder: 1.0, title: "Moving") let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil) let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [], travelBefore: nil) let day3 = ItineraryDayData(id: 3, dayNumber: 3, date: H.dayAfter(H.dayAfter(testDate)), games: [], items: [.customItem(item)], travelBefore: nil) var capturedDay: Int = 0 var capturedSortOrder: Double = 0 let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, day, sortOrder in capturedDay = day capturedSortOrder = sortOrder } controller.reloadData(days: [day1, day2, day3], travelValidRanges: [:], itineraryItems: [item]) // Rows: 0=Day1 header, 1=Day2 header, 2=Day3 header, 3=item // Move item to after Day1 header controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 3, section: 0), to: IndexPath(row: 1, section: 0)) XCTAssertEqual(capturedDay, 1, "Item should now be on Day 1") XCTAssertEqual(capturedSortOrder, 1.0, accuracy: 0.01, "First item on empty day should get sortOrder 1.0") } func test_moveItem_acrossMultipleDays_withGames() { // Given: Item on Day 3, games on Day 1 // When: Moving to Day 1 (after games) let game1 = H.makeRichGame(city: "Detroit", hour: 14) let item = H.makeCustomItem(day: 3, sortOrder: 1.0, title: "Moving") let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [game1], items: [], travelBefore: nil) let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [], travelBefore: nil) let day3 = ItineraryDayData(id: 3, dayNumber: 3, date: H.dayAfter(H.dayAfter(testDate)), games: [], items: [.customItem(item)], travelBefore: nil) var capturedDay: Int = 0 let controller = ItineraryTableViewController(style: .plain) controller.onCustomItemMoved = { _, day, _ in capturedDay = day } controller.reloadData(days: [day1, day2, day3], travelValidRanges: [:], itineraryItems: [item]) // Rows: 0=Day1 header, 1=games, 2=Day2 header, 3=Day3 header, 4=item // Move item to row 2 (after Day1 games, before Day2 header) controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 4, section: 0), to: IndexPath(row: 2, section: 0)) XCTAssertEqual(capturedDay, 1, "Item moved after Day 1 games should be on Day 1") } }