// // TravelPlacementTests.swift // SportsTimeTests // // Regression tests for travel segment day placement. // Specifically tests that repeat cities (e.g., Follow Team mode) // don't cause travel segments to be placed on non-existent days. // import XCTest @testable import SportsTime final class TravelPlacementTests: XCTestCase { // MARK: - Helpers private let calendar = Calendar.current /// Create a date for May 2026 at a given day number. private func may(_ day: Int) -> Date { var components = DateComponents() components.year = 2026 components.month = 5 components.day = day return calendar.startOfDay(for: calendar.date(from: components)!) } /// Build an array of trip days from start to end (inclusive). private func tripDays(from start: Date, to end: Date) -> [Date] { var days: [Date] = [] var current = calendar.startOfDay(for: start) let endDay = calendar.startOfDay(for: end) while current <= endDay { days.append(current) current = calendar.date(byAdding: .day, value: 1, to: current)! } return days } private func makeStop(city: String, arrival: Date, departure: Date, games: [String] = []) -> TripStop { TripStop( stopNumber: 1, city: city, state: "XX", arrivalDate: arrival, departureDate: departure, games: games ) } private func makeSegment(from: String, to: String) -> TravelSegment { TravelSegment( fromLocation: LocationInput(name: from, coordinate: nil), toLocation: LocationInput(name: to, coordinate: nil), travelMode: .drive, distanceMeters: 500_000, durationSeconds: 18000 ) } // MARK: - dayNumber Tests func test_dayNumber_returnsCorrectDayForDateWithinTrip() { let days = tripDays(from: may(1), to: may(5)) XCTAssertEqual(TravelPlacement.dayNumber(for: may(1), in: days), 1) XCTAssertEqual(TravelPlacement.dayNumber(for: may(3), in: days), 3) XCTAssertEqual(TravelPlacement.dayNumber(for: may(5), in: days), 5) } func test_dayNumber_returnsZeroForDateBeforeTrip() { let days = tripDays(from: may(5), to: may(10)) XCTAssertEqual(TravelPlacement.dayNumber(for: may(3), in: days), 0) } func test_dayNumber_returnsCountPlusOneForDateAfterTrip() { let days = tripDays(from: may(1), to: may(5)) XCTAssertEqual(TravelPlacement.dayNumber(for: may(8), in: days), 6) // count + 1 } // MARK: - Simple Trip (no repeat cities) func test_simpleTwoStopTrip_travelPlacedCorrectly() { // Chicago (May 1-3) → Detroit (May 5-6) // Travel should be on Day 4 (May 4) let stops = [ makeStop(city: "Chicago", arrival: may(1), departure: may(3)), makeStop(city: "Detroit", arrival: may(5), departure: may(6)) ] let segments = [makeSegment(from: "Chicago", to: "Detroit")] let trip = Trip( name: "Test", preferences: TripPreferences(), stops: stops, travelSegments: segments, totalGames: 0, totalDistanceMeters: 0, totalDrivingSeconds: 0 ) let days = tripDays(from: may(1), to: may(6)) let result = TravelPlacement.computeTravelByDay(trip: trip, tripDays: days) XCTAssertEqual(result.count, 1, "Should have 1 travel segment placed") XCTAssertNotNil(result[4], "Travel should be placed on Day 4 (May 4)") } // MARK: - Repeat City Bug (Follow Team regression) func test_repeatCity_travelPlacedOnCorrectDay_notGlobalLastGameDay() { // Simulates the Astros May 2026 pattern: // Houston (May 4-6) → Cincinnati (May 8-10) → Houston (May 11-17) → Chicago (May 22-24) → Houston (May 29-31) // // BUG: The old code used findLastGameDay("Houston") which returned Day 31, // causing Houston→Cincinnati travel to be placed on Day 32 (doesn't exist). // FIX: Use stop indices so each segment uses its specific stop's dates. let stops = [ makeStop(city: "Houston", arrival: may(4), departure: may(6)), // Stop 0 makeStop(city: "Cincinnati", arrival: may(8), departure: may(10)), // Stop 1 makeStop(city: "Houston", arrival: may(11), departure: may(17)), // Stop 2 makeStop(city: "Chicago", arrival: may(22), departure: may(24)), // Stop 3 makeStop(city: "Houston", arrival: may(29), departure: may(31)) // Stop 4 ] let segments = [ makeSegment(from: "Houston", to: "Cincinnati"), // Seg 0: stops[0] → stops[1] makeSegment(from: "Cincinnati", to: "Houston"), // Seg 1: stops[1] → stops[2] makeSegment(from: "Houston", to: "Chicago"), // Seg 2: stops[2] → stops[3] makeSegment(from: "Chicago", to: "Houston") // Seg 3: stops[3] → stops[4] ] let trip = Trip( name: "Follow Astros", preferences: TripPreferences(), stops: stops, travelSegments: segments, totalGames: 0, totalDistanceMeters: 0, totalDrivingSeconds: 0 ) let days = tripDays(from: may(4), to: may(31)) let result = TravelPlacement.computeTravelByDay(trip: trip, tripDays: days) // All 4 segments should be placed within the trip's day range XCTAssertEqual(result.count, 4, "All 4 travel segments should be placed") // Houston→Cincinnati: departure May 6 (Day 3), arrival May 8 (Day 5) → travel on Day 4 (May 7) XCTAssertNotNil(result[4], "Houston→Cincinnati travel should be on Day 4 (May 7)") // Cincinnati→Houston: departure May 10 (Day 7), arrival May 11 (Day 8) → travel on Day 8 (May 11) XCTAssertNotNil(result[8], "Cincinnati→Houston travel should be on Day 8 (May 11)") // Houston→Chicago: departure May 17 (Day 14), arrival May 22 (Day 19) → travel on Day 15 (May 18) XCTAssertNotNil(result[15], "Houston→Chicago travel should be on Day 15 (May 18)") // Chicago→Houston: departure May 24 (Day 21), arrival May 29 (Day 26) → travel on Day 22 (May 25) XCTAssertNotNil(result[22], "Chicago→Houston travel should be on Day 22 (May 25)") // Critical regression check: NO travel should be placed beyond the trip's day range for (day, _) in result { XCTAssertGreaterThanOrEqual(day, 1, "Travel day should be >= 1") XCTAssertLessThanOrEqual(day, days.count, "Travel day should be <= \(days.count), but got \(day)") } } func test_repeatCity_threeVisits_allTravelSegmentsVisible() { // A→B→A→B pattern (city visited 2x each) // A (Day 1-2) → B (Day 4-5) → A (Day 7-8) → B (Day 10-11) let stops = [ makeStop(city: "CityA", arrival: may(1), departure: may(2)), makeStop(city: "CityB", arrival: may(4), departure: may(5)), makeStop(city: "CityA", arrival: may(7), departure: may(8)), makeStop(city: "CityB", arrival: may(10), departure: may(11)) ] let segments = [ makeSegment(from: "CityA", to: "CityB"), makeSegment(from: "CityB", to: "CityA"), makeSegment(from: "CityA", to: "CityB") ] let trip = Trip( name: "Test Repeat", preferences: TripPreferences(), stops: stops, travelSegments: segments, totalGames: 0, totalDistanceMeters: 0, totalDrivingSeconds: 0 ) let days = tripDays(from: may(1), to: may(11)) let result = TravelPlacement.computeTravelByDay(trip: trip, tripDays: days) XCTAssertEqual(result.count, 3, "All 3 travel segments should be placed") // All within bounds for (day, _) in result { XCTAssertGreaterThanOrEqual(day, 1) XCTAssertLessThanOrEqual(day, 11) } } // MARK: - Consecutive Stops (no gap) func test_consecutiveStops_travelStillPlaced() { // Houston (May 1) → Chicago (May 2) - back to back, no rest day let stops = [ makeStop(city: "Houston", arrival: may(1), departure: may(1)), makeStop(city: "Chicago", arrival: may(2), departure: may(2)) ] let segments = [makeSegment(from: "Houston", to: "Chicago")] let trip = Trip( name: "Quick Trip", preferences: TripPreferences(), stops: stops, travelSegments: segments, totalGames: 0, totalDistanceMeters: 0, totalDrivingSeconds: 0 ) let days = tripDays(from: may(1), to: may(2)) let result = TravelPlacement.computeTravelByDay(trip: trip, tripDays: days) XCTAssertEqual(result.count, 1, "Travel segment should be placed") XCTAssertNotNil(result[2], "Travel should be on Day 2 (arrival day)") } }