import XCTest @testable import SportsTime final class ItineraryConstraintsTests: XCTestCase { // MARK: - Custom Item Tests (No Constraints) func test_customItem_canGoOnAnyDay() { // Given: A 5-day trip with games on days 1 and 5 let constraints = makeConstraints(tripDays: 5, gameDays: [1, 5]) let customItem = makeCustomItem(day: 1, sortOrder: 50) // When/Then: Custom item can go on any day XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 1, sortOrder: 50)) XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 50)) XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 3, sortOrder: 50)) XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 4, sortOrder: 50)) XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 5, sortOrder: 50)) } func test_customItem_canGoBeforeOrAfterGames() { // Given: A day with a game at sortOrder 100 let constraints = makeConstraints(tripDays: 3, gameDays: [2]) let customItem = makeCustomItem(day: 2, sortOrder: 50) // When/Then: Custom item can go before or after game XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 50)) // Before XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 150)) // After } // MARK: - Travel Constraint Tests func test_travel_validDayRange_simpleCase() { // Given: Chicago game Day 1, Detroit game Day 3 let constraints = makeConstraints( tripDays: 5, games: [ makeGameItem(city: "Chicago", day: 1), makeGameItem(city: "Detroit", day: 3) ] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 200) // When let range = constraints.validDayRange(for: travel) // Then: Travel can be on days 1 (after game), 2, or 3 (before game) XCTAssertEqual(range, 1...3) } func test_travel_mustBeAfterDepartureGames() { // Given: Chicago game on Day 1 at sortOrder 100 let constraints = makeConstraints( tripDays: 3, games: [makeGameItem(city: "Chicago", day: 1, sortOrder: 100)] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50) // When/Then: Travel before game is invalid XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 50)) // Travel after game is valid XCTAssertTrue(constraints.isValidPosition(for: travel, day: 1, sortOrder: 150)) } func test_travel_mustBeBeforeArrivalGames() { // Given: Detroit game on Day 3 at sortOrder 100 let constraints = makeConstraints( tripDays: 3, games: [makeGameItem(city: "Detroit", day: 3, sortOrder: 100)] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 150) // When/Then: Travel after arrival game is invalid XCTAssertFalse(constraints.isValidPosition(for: travel, day: 3, sortOrder: 150)) // Travel before game is valid XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: 50)) } func test_travel_canBeAnywhereOnRestDays() { // Given: Chicago game Day 1, Detroit game Day 4, rest days 2-3 let constraints = makeConstraints( tripDays: 4, games: [ makeGameItem(city: "Chicago", day: 1), makeGameItem(city: "Detroit", day: 4) ] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 50) // When/Then: Any position on rest days is valid XCTAssertTrue(constraints.isValidPosition(for: travel, day: 2, sortOrder: 1)) XCTAssertTrue(constraints.isValidPosition(for: travel, day: 2, sortOrder: 100)) XCTAssertTrue(constraints.isValidPosition(for: travel, day: 2, sortOrder: 500)) XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: 50)) } func test_travel_mustBeAfterAllDepartureGamesOnSameDay() { // Given: Two games in Chicago on Day 1 (1pm and 7pm) let constraints = makeConstraints( tripDays: 3, games: [ makeGameItem(city: "Chicago", day: 1, sortOrder: 100), // 1pm makeGameItem(city: "Chicago", day: 1, sortOrder: 101) // 7pm ] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 100.5) // When/Then: Between games is invalid XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 100.5)) // After all games is valid XCTAssertTrue(constraints.isValidPosition(for: travel, day: 1, sortOrder: 150)) } func test_travel_mustBeBeforeAllArrivalGamesOnSameDay() { // Given: Two games in Detroit on Day 3 (1pm and 7pm) let constraints = makeConstraints( tripDays: 3, games: [ makeGameItem(city: "Detroit", day: 3, sortOrder: 100), // 1pm makeGameItem(city: "Detroit", day: 3, sortOrder: 101) // 7pm ] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 100.5) // When/Then: Between games is invalid XCTAssertFalse(constraints.isValidPosition(for: travel, day: 3, sortOrder: 100.5)) // Before all games is valid XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: 50)) } func test_travel_cannotGoOutsideValidDayRange() { // Given: Chicago game Day 2, Detroit game Day 4 let constraints = makeConstraints( tripDays: 5, games: [ makeGameItem(city: "Chicago", day: 2), makeGameItem(city: "Detroit", day: 4) ] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50) // When/Then: Day 1 (before departure game) is invalid XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 50)) // Day 5 (after arrival game) is invalid XCTAssertFalse(constraints.isValidPosition(for: travel, day: 5, sortOrder: 50)) } // MARK: - Game Immutability Tests func test_gameItem_cannotBeMoved() { // Given: A game item let gameItem = makeGameItem(city: "Chicago", day: 2) let constraints = makeConstraints(tripDays: 5, games: [gameItem]) // When/Then: Game items should never be valid for any position XCTAssertFalse(constraints.isValidPosition(for: gameItem, day: 2, sortOrder: 100)) XCTAssertFalse(constraints.isValidPosition(for: gameItem, day: 3, sortOrder: 100)) XCTAssertFalse(constraints.isValidPosition(for: gameItem, day: 1, sortOrder: 50)) } // MARK: - Edge Case Tests func test_travel_validDayRange_returnsNil_whenConstraintsImpossible() { // Given: Departure game on Day 3, Arrival game on Day 1 (reversed order) let constraints = makeConstraints( tripDays: 3, games: [ makeGameItem(city: "Chicago", day: 3), // Must depart AFTER day 3 makeGameItem(city: "Detroit", day: 1) // Must arrive BEFORE day 1 ] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 50) // When/Then: No valid range exists (impossible constraints) XCTAssertNil(constraints.validDayRange(for: travel)) } // MARK: - Barrier Games Tests func test_barrierGames_returnsDepartureAndArrivalGames() { // Given: Chicago games Days 1-2, Detroit games Days 4-5 let chicagoGame1 = makeGameItem(city: "Chicago", day: 1, sortOrder: 100) let chicagoGame2 = makeGameItem(city: "Chicago", day: 2, sortOrder: 100) let detroitGame1 = makeGameItem(city: "Detroit", day: 4, sortOrder: 100) let detroitGame2 = makeGameItem(city: "Detroit", day: 5, sortOrder: 100) let constraints = makeConstraints( tripDays: 5, games: [chicagoGame1, chicagoGame2, detroitGame1, detroitGame2] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 50) // When let barriers = constraints.barrierGames(for: travel) // Then: Returns the last Chicago game and first Detroit game XCTAssertEqual(barriers.count, 2) XCTAssertTrue(barriers.contains { $0.id == chicagoGame2.id }) XCTAssertTrue(barriers.contains { $0.id == detroitGame1.id }) } // MARK: - Helpers private let testTripId = UUID() private func makeConstraints(tripDays: Int, gameDays: [Int] = []) -> ItineraryConstraints { let games = gameDays.map { makeGameItem(city: "TestCity", day: $0) } return makeConstraints(tripDays: tripDays, games: games) } private func makeConstraints(tripDays: Int, games: [ItineraryItem]) -> ItineraryConstraints { return ItineraryConstraints(tripDayCount: tripDays, items: games) } private func makeGameItem(city: String, day: Int, sortOrder: Double = 100) -> ItineraryItem { // For tests, we use gameId to encode the city return ItineraryItem( tripId: testTripId, day: day, sortOrder: sortOrder, kind: .game(gameId: "game-\(city)-\(UUID().uuidString.prefix(4))") ) } private func makeTravelItem(from: String, to: String, day: Int, sortOrder: Double) -> ItineraryItem { return ItineraryItem( tripId: testTripId, day: day, sortOrder: sortOrder, kind: .travel(TravelInfo(fromCity: from, toCity: to)) ) } private func makeCustomItem(day: Int, sortOrder: Double) -> ItineraryItem { return ItineraryItem( tripId: testTripId, day: day, sortOrder: sortOrder, kind: .custom(CustomInfo(title: "Test Item", icon: "star")) ) } }