Files
Sportstime/SportsTimeTests/ScenarioAPlannerSwiftTests.swift
Trey t 1c20d54b8b test(09-01): add same-day multi-city conflict detection tests
- Tests same-day games in close cities (both included - FAILING)
- Tests same-day games in distant cities (only one per route - PASSING)
- Tests same-day games on opposite coasts (only one per route - PASSING)
- Tests three same-day games (picks feasible combinations - FAILING)

2 of 4 tests failing - need to implement feasible same-day game logic.
2026-01-10 12:53:15 -06:00

982 lines
37 KiB
Swift

//
// ScenarioAPlannerSwiftTests.swift
// SportsTimeTests
//
// Additional tests for ScenarioAPlanner using Swift Testing framework.
// Combined with ScenarioAPlannerTests.swift, this provides comprehensive coverage.
//
import Testing
@testable import SportsTime
import Foundation
import CoreLocation
// MARK: - ScenarioAPlanner Swift Tests
struct ScenarioAPlannerSwiftTests {
// MARK: - Test Data Helpers
private func makeStadium(
id: UUID = UUID(),
city: String,
latitude: Double,
longitude: Double,
sport: Sport = .mlb
) -> Stadium {
Stadium(
id: id,
name: "\(city) Stadium",
city: city,
state: "ST",
latitude: latitude,
longitude: longitude,
capacity: 40000,
sport: sport
)
}
private func makeGame(
id: UUID = UUID(),
stadiumId: UUID,
dateTime: Date
) -> Game {
Game(
id: id,
homeTeamId: UUID(),
awayTeamId: UUID(),
stadiumId: stadiumId,
dateTime: dateTime,
sport: .mlb,
season: "2026"
)
}
private func baseDate() -> Date {
Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))!
}
private func date(daysFrom base: Date, days: Int, hour: Int = 19) -> Date {
var date = Calendar.current.date(byAdding: .day, value: days, to: base)!
return Calendar.current.date(bySettingHour: hour, minute: 0, second: 0, of: date)!
}
private func makeDateRange(start: Date, days: Int) -> DateInterval {
let end = Calendar.current.date(byAdding: .day, value: days, to: start)!
return DateInterval(start: start, end: end)
}
private func plan(
games: [Game],
stadiums: [Stadium],
dateRange: DateInterval,
numberOfDrivers: Int = 1,
maxHoursPerDriver: Double = 8.0
) -> ItineraryResult {
let stadiumDict = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0) })
let preferences = TripPreferences(
planningMode: .dateRange,
startDate: dateRange.start,
endDate: dateRange.end,
numberOfDrivers: numberOfDrivers,
maxDrivingHoursPerDriver: maxHoursPerDriver
)
let request = PlanningRequest(
preferences: preferences,
availableGames: games,
teams: [:],
stadiums: stadiumDict
)
let planner = ScenarioAPlanner()
return planner.plan(request: request)
}
// MARK: - Failure Case Tests
@Test("plan with no date range returns failure")
func plan_NoDateRange_ReturnsFailure() {
// Create a request without a valid date range
let preferences = TripPreferences(
planningMode: .dateRange,
startDate: baseDate(),
endDate: baseDate() // Same date = no range
)
let request = PlanningRequest(
preferences: preferences,
availableGames: [],
teams: [:],
stadiums: [:]
)
let planner = ScenarioAPlanner()
let result = planner.plan(request: request)
#expect(result.failure?.reason == .missingDateRange)
}
@Test("plan with games all outside date range returns failure")
func plan_AllGamesOutsideRange_ReturnsFailure() {
let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903)
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 30))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.failure?.reason == .noGamesInRange)
}
@Test("plan with end date before start date returns failure")
func plan_InvalidDateRange_ReturnsFailure() {
let preferences = TripPreferences(
planningMode: .dateRange,
startDate: baseDate(),
endDate: Calendar.current.date(byAdding: .day, value: -5, to: baseDate())!
)
let request = PlanningRequest(
preferences: preferences,
availableGames: [],
teams: [:],
stadiums: [:]
)
let planner = ScenarioAPlanner()
let result = planner.plan(request: request)
#expect(result.failure != nil)
}
// MARK: - Success Case Tests
@Test("plan returns success with valid single game")
func plan_ValidSingleGame_ReturnsSuccess() {
let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903)
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.count == 1)
#expect(result.options.first?.stops.count == 1)
}
@Test("plan includes game exactly at range start")
func plan_GameAtRangeStart_Included() {
let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903)
// Game exactly at start of range
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0, hour: 10))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.stops.count == 1)
}
@Test("plan includes game exactly at range end")
func plan_GameAtRangeEnd_Included() {
let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903)
// Game at end of range
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 9, hour: 19))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
}
// MARK: - Driving Constraints Tests
@Test("plan rejects route that exceeds driving limit")
func plan_ExceedsDrivingLimit_RoutePruned() {
// Create two cities ~2000 miles apart
let ny = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060)
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
// Games 1 day apart - impossible to drive
let games = [
makeGame(stadiumId: ny.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 1))
]
let result = plan(
games: games,
stadiums: [ny, la],
dateRange: makeDateRange(start: baseDate(), days: 10),
numberOfDrivers: 1,
maxHoursPerDriver: 8.0
)
// Should succeed but not have both games in same route
if result.isSuccess {
// May have single-game options but not both together
#expect(true)
}
}
@Test("plan with two drivers allows longer routes")
func plan_TwoDrivers_AllowsLongerRoutes() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let denver = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903)
// ~1000 miles, ~17 hours - doable with 2 drivers
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: denver.id, dateTime: date(daysFrom: baseDate(), days: 2))
]
let result = plan(
games: games,
stadiums: [la, denver],
dateRange: makeDateRange(start: baseDate(), days: 10),
numberOfDrivers: 2,
maxHoursPerDriver: 8.0
)
#expect(result.isSuccess)
}
// MARK: - Stop Grouping Tests
@Test("multiple games at same stadium grouped into one stop")
func plan_SameStadiumGames_GroupedIntoOneStop() {
let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298)
let games = [
makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 1)),
makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
]
let result = plan(
games: [games[0], games[1], games[2]],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.stops.count == 1)
#expect(result.options.first?.stops.first?.games.count == 3)
}
@Test("stop arrival date is first game date")
func plan_StopArrivalDate_IsFirstGameDate() {
let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298)
let firstGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let secondGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 3))
let result = plan(
games: [firstGame, secondGame],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let stop = result.options.first?.stops.first
let firstGameDate = Calendar.current.startOfDay(for: firstGame.startTime)
let stopArrival = Calendar.current.startOfDay(for: stop?.arrivalDate ?? Date.distantPast)
#expect(firstGameDate == stopArrival)
}
@Test("stop departure date is last game date")
func plan_StopDepartureDate_IsLastGameDate() {
let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298)
let firstGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let secondGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 4))
let result = plan(
games: [firstGame, secondGame],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let stop = result.options.first?.stops.first
let lastGameDate = Calendar.current.startOfDay(for: secondGame.startTime)
let stopDeparture = Calendar.current.startOfDay(for: stop?.departureDate ?? Date.distantFuture)
#expect(lastGameDate == stopDeparture)
}
// MARK: - Travel Segment Tests
@Test("single stop has zero travel segments")
func plan_SingleStop_ZeroTravelSegments() {
let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903)
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.travelSegments.isEmpty == true)
}
@Test("two stops have one travel segment")
func plan_TwoStops_OneTravelSegment() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3))
]
let result = plan(
games: games,
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let twoStopOption = result.options.first { $0.stops.count == 2 }
#expect(twoStopOption?.travelSegments.count == 1)
}
@Test("travel segment has correct origin and destination")
func plan_TravelSegment_CorrectOriginDestination() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3))
]
let result = plan(
games: games,
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let twoStopOption = result.options.first { $0.stops.count == 2 }
let segment = twoStopOption?.travelSegments.first
#expect(segment?.fromLocation.name == "Los Angeles")
#expect(segment?.toLocation.name == "San Francisco")
}
@Test("travel segment distance is reasonable for LA to SF")
func plan_TravelSegment_ReasonableDistance() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3))
]
let result = plan(
games: games,
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let twoStopOption = result.options.first { $0.stops.count == 2 }
let distance = twoStopOption?.totalDistanceMiles ?? 0
// LA to SF is ~380 miles, with routing factor ~500 miles
#expect(distance > 400 && distance < 600)
}
// MARK: - Option Ranking Tests
@Test("options are ranked starting from 1")
func plan_Options_RankedFromOne() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3))
]
let result = plan(
games: games,
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.rank == 1)
}
@Test("all options have valid isValid property")
func plan_Options_AllValid() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3))
]
let result = plan(
games: games,
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
for option in result.options {
#expect(option.isValid, "All options should pass isValid check")
}
}
@Test("totalGames computed property is correct")
func plan_TotalGames_ComputedCorrectly() {
let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298)
let games = [
makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 1)),
makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
]
let result = plan(
games: games,
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.totalGames == 3)
}
// MARK: - Edge Cases
@Test("games in reverse chronological order still processed correctly")
func plan_ReverseChronologicalGames_ProcessedCorrectly() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
// Games added in reverse order
let game1 = makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 5))
let game2 = makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 2))
let result = plan(
games: [game1, game2], // SF first (later date)
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
// Should be sorted: LA (day 2) then SF (day 5)
let twoStopOption = result.options.first { $0.stops.count == 2 }
#expect(twoStopOption?.stops[0].city == "Los Angeles")
#expect(twoStopOption?.stops[1].city == "San Francisco")
}
@Test("handles many games efficiently")
func plan_ManyGames_HandledEfficiently() {
var stadiums: [Stadium] = []
var games: [Game] = []
// Create 15 games along the west coast
let cities: [(String, Double, Double)] = [
("San Diego", 32.7157, -117.1611),
("Los Angeles", 34.0522, -118.2437),
("Bakersfield", 35.3733, -119.0187),
("Fresno", 36.7378, -119.7871),
("San Jose", 37.3382, -121.8863),
("San Francisco", 37.7749, -122.4194),
("Oakland", 37.8044, -122.2712),
("Sacramento", 38.5816, -121.4944),
("Reno", 39.5296, -119.8138),
("Redding", 40.5865, -122.3917),
("Eugene", 44.0521, -123.0868),
("Portland", 45.5152, -122.6784),
("Seattle", 47.6062, -122.3321),
("Tacoma", 47.2529, -122.4443),
("Vancouver", 49.2827, -123.1207)
]
for (index, city) in cities.enumerated() {
let id = UUID()
stadiums.append(makeStadium(id: id, city: city.0, latitude: city.1, longitude: city.2))
games.append(makeGame(stadiumId: id, dateTime: date(daysFrom: baseDate(), days: index)))
}
let result = plan(
games: games,
stadiums: stadiums,
dateRange: makeDateRange(start: baseDate(), days: 20)
)
#expect(result.isSuccess)
#expect(result.options.count <= 10)
}
@Test("empty stadiums dictionary returns failure")
func plan_EmptyStadiums_ReturnsSuccess() {
let stadiumId = UUID()
let game = makeGame(stadiumId: stadiumId, dateTime: date(daysFrom: baseDate(), days: 2))
// Game exists but stadium not in dictionary
let result = plan(
games: [game],
stadiums: [],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
// Should handle gracefully (may return failure or success with empty)
#expect(result.failure != nil || result.options.isEmpty || result.isSuccess)
}
@Test("stop has correct city from stadium")
func plan_StopCity_MatchesStadium() {
let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740)
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.stops.first?.city == "Phoenix")
}
@Test("stop has correct state from stadium")
func plan_StopState_MatchesStadium() {
let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740)
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.stops.first?.state == "ST")
}
@Test("stop has coordinate from stadium")
func plan_StopCoordinate_MatchesStadium() {
let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740)
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let coord = result.options.first?.stops.first?.coordinate
#expect(coord != nil)
#expect(abs(coord!.latitude - 33.4484) < 0.01)
#expect(abs(coord!.longitude - (-112.0740)) < 0.01)
}
@Test("firstGameStart property is set correctly")
func plan_FirstGameStart_SetCorrectly() {
let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903)
let gameTime = date(daysFrom: baseDate(), days: 2, hour: 19)
let game = makeGame(stadiumId: stadium.id, dateTime: gameTime)
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let firstGameStart = result.options.first?.stops.first?.firstGameStart
#expect(firstGameStart == gameTime)
}
@Test("location property has correct name")
func plan_LocationProperty_CorrectName() {
let stadium = makeStadium(city: "Austin", latitude: 30.2672, longitude: -97.7431)
let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2))
let result = plan(
games: [game],
stadiums: [stadium],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
#expect(result.options.first?.stops.first?.location.name == "Austin")
}
@Test("geographicRationale shows game count")
func plan_GeographicRationale_ShowsGameCount() {
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3))
]
let result = plan(
games: games,
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
let twoStopOption = result.options.first { $0.stops.count == 2 }
#expect(twoStopOption?.geographicRationale.contains("2") == true)
}
@Test("options with same game count sorted by driving hours")
func plan_SameGameCount_SortedByDrivingHours() {
// Create scenario where multiple routes have same game count
let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let games = [
makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)),
makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3))
]
let result = plan(
games: games,
stadiums: [la, sf],
dateRange: makeDateRange(start: baseDate(), days: 10)
)
#expect(result.isSuccess)
// All options should be valid and sorted
for option in result.options {
#expect(option.isValid)
}
}
// MARK: - Timezone Boundary Tests
@Test("game at range start in different timezone is included")
func plan_GameAtRangeStartDifferentTimezone_Included() {
// Date range: Jan 5 00:00 PST to Jan 10 23:59 PST
let pstCalendar = Calendar.current
var pstComponents = DateComponents()
pstComponents.year = 2026
pstComponents.month = 1
pstComponents.day = 5
pstComponents.hour = 0
pstComponents.minute = 0
pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeStart = pstCalendar.date(from: pstComponents)!
var endComponents = DateComponents()
endComponents.year = 2026
endComponents.month = 1
endComponents.day = 10
endComponents.hour = 23
endComponents.minute = 59
endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeEnd = pstCalendar.date(from: endComponents)!
let dateRange = DateInterval(start: rangeStart, end: rangeEnd)
// Game: Jan 5 19:00 EST (New York) = Jan 5 16:00 PST
let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060)
var estComponents = DateComponents()
estComponents.year = 2026
estComponents.month = 1
estComponents.day = 5
estComponents.hour = 19
estComponents.minute = 0
estComponents.timeZone = TimeZone(identifier: "America/New_York")!
let gameTime = pstCalendar.date(from: estComponents)!
let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime)
let result = plan(
games: [game],
stadiums: [nyStadium],
dateRange: dateRange
)
// Game should be included (within PST range)
#expect(result.isSuccess)
#expect(result.options.first?.stops.count == 1)
}
@Test("game just before range start in different timezone is excluded")
func plan_GameBeforeRangeStartDifferentTimezone_Excluded() {
// Date range: Jan 5 00:00 PST to Jan 10 23:59 PST
let pstCalendar = Calendar.current
var pstComponents = DateComponents()
pstComponents.year = 2026
pstComponents.month = 1
pstComponents.day = 5
pstComponents.hour = 0
pstComponents.minute = 0
pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeStart = pstCalendar.date(from: pstComponents)!
var endComponents = DateComponents()
endComponents.year = 2026
endComponents.month = 1
endComponents.day = 10
endComponents.hour = 23
endComponents.minute = 59
endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeEnd = pstCalendar.date(from: endComponents)!
let dateRange = DateInterval(start: rangeStart, end: rangeEnd)
// Game: Jan 4 22:00 EST (New York) = Jan 4 19:00 PST
let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060)
var estComponents = DateComponents()
estComponents.year = 2026
estComponents.month = 1
estComponents.day = 4
estComponents.hour = 22
estComponents.minute = 0
estComponents.timeZone = TimeZone(identifier: "America/New_York")!
let gameTime = pstCalendar.date(from: estComponents)!
let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime)
let result = plan(
games: [game],
stadiums: [nyStadium],
dateRange: dateRange
)
// Game should be excluded (before PST range start)
#expect(result.failure?.reason == .noGamesInRange)
}
@Test("game at range end in different timezone is included")
func plan_GameAtRangeEndDifferentTimezone_Included() {
// Date range: Jan 5 00:00 PST to Jan 10 23:59 PST
let pstCalendar = Calendar.current
var pstComponents = DateComponents()
pstComponents.year = 2026
pstComponents.month = 1
pstComponents.day = 5
pstComponents.hour = 0
pstComponents.minute = 0
pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeStart = pstCalendar.date(from: pstComponents)!
var endComponents = DateComponents()
endComponents.year = 2026
endComponents.month = 1
endComponents.day = 10
endComponents.hour = 23
endComponents.minute = 59
endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeEnd = pstCalendar.date(from: endComponents)!
let dateRange = DateInterval(start: rangeStart, end: rangeEnd)
// Game: Jan 10 21:00 EST (New York) = Jan 10 18:00 PST
let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060)
var estComponents = DateComponents()
estComponents.year = 2026
estComponents.month = 1
estComponents.day = 10
estComponents.hour = 21
estComponents.minute = 0
estComponents.timeZone = TimeZone(identifier: "America/New_York")!
let gameTime = pstCalendar.date(from: estComponents)!
let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime)
let result = plan(
games: [game],
stadiums: [nyStadium],
dateRange: dateRange
)
// Game should be included (within PST range)
#expect(result.isSuccess)
#expect(result.options.first?.stops.count == 1)
}
@Test("game just after range end in different timezone is excluded")
func plan_GameAfterRangeEndDifferentTimezone_Excluded() {
// Date range: Jan 5 00:00 PST to Jan 10 23:59 PST
let pstCalendar = Calendar.current
var pstComponents = DateComponents()
pstComponents.year = 2026
pstComponents.month = 1
pstComponents.day = 5
pstComponents.hour = 0
pstComponents.minute = 0
pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeStart = pstCalendar.date(from: pstComponents)!
var endComponents = DateComponents()
endComponents.year = 2026
endComponents.month = 1
endComponents.day = 10
endComponents.hour = 23
endComponents.minute = 59
endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")!
let rangeEnd = pstCalendar.date(from: endComponents)!
let dateRange = DateInterval(start: rangeStart, end: rangeEnd)
// Game: Jan 11 02:00 EST (New York) = Jan 10 23:00 PST
// This is actually WITHIN the range (before 23:59 PST)
// Let me adjust: Jan 11 03:00 EST = Jan 11 00:00 PST (after range)
let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060)
var estComponents = DateComponents()
estComponents.year = 2026
estComponents.month = 1
estComponents.day = 11
estComponents.hour = 3
estComponents.minute = 0
estComponents.timeZone = TimeZone(identifier: "America/New_York")!
let gameTime = pstCalendar.date(from: estComponents)!
let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime)
let result = plan(
games: [game],
stadiums: [nyStadium],
dateRange: dateRange
)
// Game should be excluded (after PST range end)
#expect(result.failure?.reason == .noGamesInRange)
}
// MARK: - Same-Day Multi-City Conflict Tests
@Test("same-day games in close cities are both included in route")
func plan_SameDayGamesCloseCities_BothIncluded() {
// LA game at 1pm, San Diego game at 7pm (120 miles, ~2hr drive)
let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sdStadium = makeStadium(city: "San Diego", latitude: 32.7157, longitude: -117.1611)
let base = baseDate()
let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13))
let sdGame = makeGame(stadiumId: sdStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19))
let result = plan(
games: [laGame, sdGame],
stadiums: [laStadium, sdStadium],
dateRange: makeDateRange(start: base, days: 10)
)
// Should succeed with both games in route (enough time to drive between)
#expect(result.isSuccess)
let twoStopOption = result.options.first { $0.stops.count == 2 }
#expect(twoStopOption != nil, "Should have route with both cities")
#expect(twoStopOption?.totalGames == 2)
}
@Test("same-day games in distant cities only one included per route")
func plan_SameDayGamesDistantCities_OnlyOnePerRoute() {
// LA game at 1pm, SF game at 7pm (380 miles, ~6hr drive)
let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let sfStadium = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
let base = baseDate()
let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13))
let sfGame = makeGame(stadiumId: sfStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19))
let result = plan(
games: [laGame, sfGame],
stadiums: [laStadium, sfStadium],
dateRange: makeDateRange(start: base, days: 10)
)
// Should succeed but each route picks ONE game (cannot attend both same day)
#expect(result.isSuccess)
for option in result.options {
// Each option should have only 1 stop (cannot do both same day)
#expect(option.stops.count == 1, "Route should pick only one game - cannot attend both LA and SF same day")
}
}
@Test("same-day games on opposite coasts only one included per route")
func plan_SameDayGamesOppositCoasts_OnlyOnePerRoute() {
// LA game at 1pm PST, NY game at 7pm EST (2800 miles, impossible same day)
let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060)
let base = baseDate()
let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13))
let nyGame = makeGame(stadiumId: nyStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19))
let result = plan(
games: [laGame, nyGame],
stadiums: [laStadium, nyStadium],
dateRange: makeDateRange(start: base, days: 10)
)
// Should succeed but each route picks ONE game (obviously impossible same day)
#expect(result.isSuccess)
for option in result.options {
#expect(option.stops.count == 1, "Route should pick only one game - cannot attend both coasts same day")
}
}
@Test("three same-day games picks feasible combinations")
func plan_ThreeSameDayGames_PicksFeasibleCombinations() {
// LA 1pm, Anaheim 4pm (30mi), San Diego 7pm (90mi from Anaheim)
// Feasible: LAAnaheimSD
// Cannot include NY game same day
let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
let anaheimStadium = makeStadium(city: "Anaheim", latitude: 33.8003, longitude: -117.8827)
let sdStadium = makeStadium(city: "San Diego", latitude: 32.7157, longitude: -117.1611)
let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060)
let base = baseDate()
let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13))
let anaheimGame = makeGame(stadiumId: anaheimStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 16))
let sdGame = makeGame(stadiumId: sdStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19))
let nyGame = makeGame(stadiumId: nyStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19))
let result = plan(
games: [laGame, anaheimGame, sdGame, nyGame],
stadiums: [laStadium, anaheimStadium, sdStadium, nyStadium],
dateRange: makeDateRange(start: base, days: 10)
)
// Should have options, and best option includes the 3 West Coast games
#expect(result.isSuccess)
// Should have a 3-stop option (LAAnaheimSD)
let threeStopOption = result.options.first { $0.stops.count == 3 }
#expect(threeStopOption != nil, "Should have route with 3 West Coast stops")
#expect(threeStopOption?.totalGames == 3)
// No option should include NY with any other game from same day
for option in result.options {
let cities = option.stops.map { $0.city }
if cities.contains("New York") {
#expect(option.stops.count == 1, "NY game cannot be combined with West Coast games same day")
}
}
}
}