diff --git a/SportsTimeTests/ScenarioCPlannerTests.swift b/SportsTimeTests/ScenarioCPlannerTests.swift index 163283a..be798cc 100644 --- a/SportsTimeTests/ScenarioCPlannerTests.swift +++ b/SportsTimeTests/ScenarioCPlannerTests.swift @@ -2020,4 +2020,257 @@ struct ScenarioCPlannerTests { Issue.record("Expected success") } } + + // MARK: - Feature 1: Travel Corridor Game Inclusion Tests + + @Test("Direct route with games along path includes all corridor games") + func corridor_DirectRouteWithGamesAlongPath_IncludesAllGames() { + let planner = ScenarioCPlanner() + + // Create stadiums: LA → San Jose → SF (direct north on I-5/101) + let la = makeStadium(name: "Dodger Stadium", city: "Los Angeles", state: "CA", + latitude: 34.0739, longitude: -118.2400) + let sj = makeStadium(name: "SAP Center", city: "San Jose", state: "CA", + latitude: 37.3326, longitude: -121.9010) // Midpoint between LA and SF + let sf = sfStadium + + // Games along the path + let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) + let sjGame = makeGame(stadiumId: sj.id, date: date("2026-06-06 19:00")) + let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00")) + + let startLoc = LocationInput( + name: "Los Angeles", + coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude) + ) + let endLoc = LocationInput( + name: "San Francisco", + coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude) + ) + + let request = makeRequest( + games: [laGame, sjGame, sfGame], + stadiums: [la.id: la, sj.id: sj, sf.id: sf], + startLocation: startLoc, + endLocation: endLoc, + startDate: date("2026-06-01 00:00"), + endDate: date("2026-06-30 23:59") + ) + + let result = planner.plan(request: request) + + if case .success(let options) = result { + #expect(!options.isEmpty, "Should return at least one route") + + // Best option should include all 3 games (LA → SJ → SF) + let topOption = options.first! + let gameIds = topOption.stops.flatMap { $0.games } + + #expect(gameIds.contains(laGame.id), "Should include LA game") + #expect(gameIds.contains(sjGame.id), "Should include San Jose game (midpoint)") + #expect(gameIds.contains(sfGame.id), "Should include SF game") + } else { + Issue.record("Expected success with games along direct corridor") + } + } + + @Test("Game slightly off corridor within tolerance included") + func corridor_GameSlightlyOffCorridor_IncludedWithinTolerance() { + let planner = ScenarioCPlanner() + + // LA → SF is north on I-5 + // Sacramento is ~20 miles east of I-5, should be within corridor tolerance + let la = laStadium + let sacramento = makeStadium(name: "Golden 1 Center", city: "Sacramento", state: "CA", + latitude: 38.5802, longitude: -121.4996) // Slightly east of direct path + let sf = sfStadium + + let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) + let sacGame = makeGame(stadiumId: sacramento.id, date: date("2026-06-06 19:00")) + let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00")) + + let startLoc = LocationInput( + name: "Los Angeles", + coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude) + ) + let endLoc = LocationInput( + name: "San Francisco", + coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude) + ) + + let request = makeRequest( + games: [laGame, sacGame, sfGame], + stadiums: [la.id: la, sacramento.id: sacramento, sf.id: sf], + startLocation: startLoc, + endLocation: endLoc, + startDate: date("2026-06-01 00:00"), + endDate: date("2026-06-30 23:59") + ) + + let result = planner.plan(request: request) + + if case .success(let options) = result { + #expect(!options.isEmpty) + + // Sacramento should be included (within corridor tolerance) + let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } } + #expect(allGameIds.contains(sacGame.id), "Sacramento should be included (within ~50mi corridor tolerance)") + } else { + Issue.record("Expected success with slightly off-corridor game") + } + } + + @Test("Game far from corridor excluded") + func corridor_GameFarFromCorridor_Excluded() { + let planner = ScenarioCPlanner() + + // LA → SF is north-bound + // Phoenix is ~300 miles east, far from corridor + let la = laStadium + let phoenix = makeStadium(name: "Chase Field", city: "Phoenix", state: "AZ", + latitude: 33.4452, longitude: -112.0667) // Far east of LA→SF path + let sf = sfStadium + + let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) + let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-06-06 19:00")) + let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00")) + + let startLoc = LocationInput( + name: "Los Angeles", + coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude) + ) + let endLoc = LocationInput( + name: "San Francisco", + coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude) + ) + + let request = makeRequest( + games: [laGame, phoenixGame, sfGame], + stadiums: [la.id: la, phoenix.id: phoenix, sf.id: sf], + startLocation: startLoc, + endLocation: endLoc, + startDate: date("2026-06-01 00:00"), + endDate: date("2026-06-30 23:59") + ) + + let result = planner.plan(request: request) + + if case .success(let options) = result { + #expect(!options.isEmpty) + + // Phoenix should be excluded (too far from LA→SF corridor) + let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } } + #expect(!allGameIds.contains(phoenixGame.id), "Phoenix should be excluded (300mi east, far from corridor)") + } else { + Issue.record("Expected success but with Phoenix excluded") + } + } + + @Test("Multiple games some on corridor some off filters correctly") + func corridor_MultipleGamesMixed_FiltersCorrectly() { + let planner = ScenarioCPlanner() + + // LA → Portland route + let la = laStadium + let sd = sdStadium // South of LA - wrong direction + let sf = sfStadium // On path north + let seattle = makeStadium(name: "T-Mobile Park", city: "Seattle", state: "WA", + latitude: 47.5914, longitude: -122.3325) // Beyond Portland + let portland = makeStadium(name: "Providence Park", city: "Portland", state: "OR", + latitude: 45.5212, longitude: -122.6917) + + let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) + let sdGame = makeGame(stadiumId: sd.id, date: date("2026-06-06 19:00")) // Should exclude (south) + let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00")) // Should include (on path) + let seattleGame = makeGame(stadiumId: seattle.id, date: date("2026-06-08 19:00")) // Should exclude (beyond end) + + let startLoc = LocationInput( + name: "Los Angeles", + coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude) + ) + let endLoc = LocationInput( + name: "Portland", + coordinate: CLLocationCoordinate2D(latitude: portland.latitude, longitude: portland.longitude) + ) + + let request = makeRequest( + games: [laGame, sdGame, sfGame, seattleGame], + stadiums: [la.id: la, sd.id: sd, sf.id: sf, seattle.id: seattle, portland.id: portland], + startLocation: startLoc, + endLocation: endLoc, + startDate: date("2026-06-01 00:00"), + endDate: date("2026-06-30 23:59") + ) + + let result = planner.plan(request: request) + + if case .success(let options) = result { + #expect(!options.isEmpty) + + let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } } + + // Should include games on corridor + #expect(allGameIds.contains(laGame.id), "LA should be included (start)") + #expect(allGameIds.contains(sfGame.id), "SF should be included (on path north)") + + // Should exclude off-corridor games + #expect(!allGameIds.contains(sdGame.id), "San Diego should be excluded (south, wrong direction)") + #expect(!allGameIds.contains(seattleGame.id), "Seattle should be excluded (beyond end point)") + } else { + Issue.record("Expected success with selective corridor filtering") + } + } + + @Test("No games along corridor returns empty route or failure") + func corridor_NoGamesAlongCorridor_ReturnsEmptyOrFailure() { + let planner = ScenarioCPlanner() + + // LA → Seattle route (I-5 corridor) + let la = laStadium + let seattle = makeStadium(name: "T-Mobile Park", city: "Seattle", state: "WA", + latitude: 47.5914, longitude: -122.3325) + + // Games far from corridor + let phoenix = makeStadium(name: "Chase Field", city: "Phoenix", state: "AZ", + latitude: 33.4452, longitude: -112.0667) // East + let denver = makeStadium(name: "Coors Field", city: "Denver", state: "CO", + latitude: 39.7559, longitude: -104.9942) // Far east + + let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-06-05 19:00")) + let denverGame = makeGame(stadiumId: denver.id, date: date("2026-06-06 19:00")) + + let startLoc = LocationInput( + name: "Los Angeles", + coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude) + ) + let endLoc = LocationInput( + name: "Seattle", + coordinate: CLLocationCoordinate2D(latitude: seattle.latitude, longitude: seattle.longitude) + ) + + let request = makeRequest( + games: [phoenixGame, denverGame], + stadiums: [la.id: la, seattle.id: seattle, phoenix.id: phoenix, denver.id: denver], + startLocation: startLoc, + endLocation: endLoc, + startDate: date("2026-06-01 00:00"), + endDate: date("2026-06-30 23:59") + ) + + let result = planner.plan(request: request) + + // Should either fail (.noGamesInRange or .noValidRoutes) OR return route with no games + switch result { + case .failure(let failure): + // Expected: no games in corridor + #expect(failure.reason == .noValidRoutes || failure.reason == .noGamesInRange, + "Should fail with noValidRoutes or noGamesInRange") + case .success(let options): + // If success, route should have only start/end waypoints, no games + if let topOption = options.first { + let gameCount = topOption.stops.flatMap { $0.games }.count + #expect(gameCount == 0, "Route should have no games if all games are off-corridor") + } + } + } }