Files
Sportstime/SportsTimeTests/Planning/ScenarioAPlannerTests.swift
Trey t 1703ca5b0f refactor: change domain model IDs from UUID to String canonical IDs
This refactor fixes the achievement system by using stable canonical string
IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures
stadium mappings for achievements are consistent across app launches and
CloudKit sync operations.

Changes:
- Stadium, Team, Game: id property changed from UUID to String
- Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums
- CKModels: removed UUID parsing, use canonical IDs directly
- AchievementEngine: now matches against canonical stadium IDs
- All test files updated to use String IDs instead of UUID()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:24:33 -06:00

697 lines
29 KiB
Swift

//
// ScenarioAPlannerTests.swift
// SportsTimeTests
//
// Phase 4: ScenarioAPlanner Tests
// Scenario A: User provides dates, planner finds games.
//
import Testing
import CoreLocation
@testable import SportsTime
@Suite("ScenarioAPlanner Tests")
struct ScenarioAPlannerTests {
// MARK: - Test Fixtures
private let calendar = Calendar.current
private let planner = ScenarioAPlanner()
/// Creates a date with specific year/month/day/hour
private func makeDate(year: Int = 2026, month: Int = 6, day: Int, hour: Int = 19) -> Date {
var components = DateComponents()
components.year = year
components.month = month
components.day = day
components.hour = hour
components.minute = 0
return calendar.date(from: components)!
}
/// Creates a stadium at a known location
private func makeStadium(
id: String = "stadium_test_\(UUID().uuidString)",
city: String,
lat: Double,
lon: Double,
sport: Sport = .mlb
) -> Stadium {
Stadium(
id: id,
name: "\(city) Stadium",
city: city,
state: "ST",
latitude: lat,
longitude: lon,
capacity: 40000,
sport: sport
)
}
/// Creates a game at a stadium
private func makeGame(
id: String = "game_test_\(UUID().uuidString)",
stadiumId: String,
homeTeamId: String = "team_test_\(UUID().uuidString)",
awayTeamId: String = "team_test_\(UUID().uuidString)",
dateTime: Date,
sport: Sport = .mlb
) -> Game {
Game(
id: id,
homeTeamId: homeTeamId,
awayTeamId: awayTeamId,
stadiumId: stadiumId,
dateTime: dateTime,
sport: sport,
season: "2026"
)
}
/// Creates a PlanningRequest with the given parameters
private func makePlanningRequest(
startDate: Date,
endDate: Date,
games: [Game],
stadiums: [String: Stadium],
teams: [String: Team] = [:],
allowRepeatCities: Bool = true,
numberOfDrivers: Int = 1,
maxDrivingHoursPerDriver: Double = 8.0,
mustStopLocations: [LocationInput] = []
) -> PlanningRequest {
let preferences = TripPreferences(
planningMode: .dateRange,
sports: [.mlb],
startDate: startDate,
endDate: endDate,
leisureLevel: .moderate,
mustStopLocations: mustStopLocations,
numberOfDrivers: numberOfDrivers,
maxDrivingHoursPerDriver: maxDrivingHoursPerDriver,
allowRepeatCities: allowRepeatCities
)
return PlanningRequest(
preferences: preferences,
availableGames: games,
teams: teams,
stadiums: stadiums
)
}
// MARK: - 4A: Valid Inputs
@Test("4.1 - Valid date range returns games in range")
func test_planByDates_ValidDateRange_ReturnsGamesInRange() {
// Setup: 3 games across nearby cities over 5 days
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
let detroitId = "stadium_detroit_\(UUID().uuidString)"
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit]
let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19))
let game3 = makeGame(stadiumId: detroitId, dateTime: makeDate(day: 9, hour: 19))
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [game1, game2, game3],
stadiums: stadiums
)
// Execute
let result = planner.plan(request: request)
// Verify
#expect(result.isSuccess, "Should succeed with valid date range and games")
#expect(!result.options.isEmpty, "Should return at least one itinerary option")
// All returned games should be within date range
for option in result.options {
#expect(option.stops.allSatisfy { !$0.games.isEmpty || $0.city.isEmpty == false },
"Each option should have valid stops")
}
}
@Test("4.2 - Single day range returns games on that day")
func test_planByDates_SingleDayRange_ReturnsGamesOnThatDay() {
// Setup: Multiple games on a single day at the same stadium
let stadiumId = "stadium_test_\(UUID().uuidString)"
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [stadiumId: stadium]
// Doubleheader on June 5
let game1 = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 13))
let game2 = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 19))
// Game outside the range
let gameOutside = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 6, hour: 19))
let request = makePlanningRequest(
startDate: makeDate(day: 5, hour: 0),
endDate: makeDate(day: 5, hour: 23),
games: [game1, game2, gameOutside],
stadiums: stadiums
)
// Execute
let result = planner.plan(request: request)
// Verify
#expect(result.isSuccess, "Should succeed for single day range")
#expect(!result.options.isEmpty, "Should return at least one option")
// Games in options should only be from June 5
if let firstOption = result.options.first {
let gameIds = Set(firstOption.stops.flatMap { $0.games })
#expect(gameIds.contains(game1.id) || gameIds.contains(game2.id),
"Should include games from the single day")
#expect(!gameIds.contains(gameOutside.id),
"Should not include games outside the date range")
}
}
@Test("4.3 - Multi-week range returns multiple games")
func test_planByDates_MultiWeekRange_ReturnsMultipleGames() {
// Setup: Games spread across 3 weeks in nearby cities
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
let detroitId = "stadium_detroit_\(UUID().uuidString)"
let clevelandId = "stadium_cleveland_\(UUID().uuidString)"
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944)
let stadiums = [
chicagoId: chicago,
milwaukeeId: milwaukee,
detroitId: detroit,
clevelandId: cleveland
]
// Games across 3 weeks
let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 1, hour: 19))
let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 5, hour: 19))
let game3 = makeGame(stadiumId: detroitId, dateTime: makeDate(day: 10, hour: 19))
let game4 = makeGame(stadiumId: clevelandId, dateTime: makeDate(day: 15, hour: 19))
let game5 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 20, hour: 19))
let request = makePlanningRequest(
startDate: makeDate(day: 1, hour: 0),
endDate: makeDate(day: 21, hour: 23),
games: [game1, game2, game3, game4, game5],
stadiums: stadiums
)
// Execute
let result = planner.plan(request: request)
// Verify
#expect(result.isSuccess, "Should succeed for multi-week range")
#expect(!result.options.isEmpty, "Should return itinerary options")
// Should have options with multiple games
let optionWithMultipleGames = result.options.first { $0.totalGames >= 2 }
#expect(optionWithMultipleGames != nil, "Should have options covering multiple games")
}
// MARK: - 4B: Edge Cases
@Test("4.4 - No games in range returns failure")
func test_planByDates_NoGamesInRange_ThrowsError() {
// Setup: Games outside the requested date range
let stadiumId = "stadium_test_\(UUID().uuidString)"
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [stadiumId: stadium]
// Games in July, but request is for June
let gameOutside1 = makeGame(stadiumId: stadiumId, dateTime: makeDate(month: 7, day: 10, hour: 19))
let gameOutside2 = makeGame(stadiumId: stadiumId, dateTime: makeDate(month: 7, day: 15, hour: 19))
let request = makePlanningRequest(
startDate: makeDate(month: 6, day: 1, hour: 0),
endDate: makeDate(month: 6, day: 30, hour: 23),
games: [gameOutside1, gameOutside2],
stadiums: stadiums
)
// Execute
let result = planner.plan(request: request)
// Verify: Should fail with noGamesInRange
#expect(!result.isSuccess, "Should fail when no games in range")
#expect(result.failure?.reason == .noGamesInRange,
"Should return noGamesInRange failure reason")
}
@Test("4.5 - End date before start date returns failure")
func test_planByDates_EndDateBeforeStartDate_ThrowsError() {
// Setup: Invalid date range where end < start
let stadiumId = "stadium_test_\(UUID().uuidString)"
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [stadiumId: stadium]
let game = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 19))
// End date before start date
let request = makePlanningRequest(
startDate: makeDate(day: 15, hour: 0), // June 15
endDate: makeDate(day: 5, hour: 23), // June 5 (before start)
games: [game],
stadiums: stadiums
)
// Execute
let result = planner.plan(request: request)
// Verify: Should fail with missingDateRange (invalid range)
#expect(!result.isSuccess, "Should fail when end date is before start date")
#expect(result.failure?.reason == .missingDateRange,
"Should return missingDateRange for invalid date range")
}
@Test("4.6 - Single game in range returns single game route")
func test_planByDates_SingleGameInRange_ReturnsSingleGameRoute() {
// Setup: Only one game in the date range
let stadiumId = "stadium_test_\(UUID().uuidString)"
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [stadiumId: stadium]
let game = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 10, hour: 19))
let request = makePlanningRequest(
startDate: makeDate(day: 5, hour: 0),
endDate: makeDate(day: 15, hour: 23),
games: [game],
stadiums: stadiums
)
// Execute
let result = planner.plan(request: request)
// Verify
#expect(result.isSuccess, "Should succeed with single game")
#expect(!result.options.isEmpty, "Should return at least one option")
if let firstOption = result.options.first {
#expect(firstOption.totalGames == 1, "Should have exactly 1 game")
#expect(firstOption.stops.count == 1, "Should have exactly 1 stop")
#expect(firstOption.stops.first?.games.contains(game.id) == true,
"Stop should contain the single game")
}
}
@Test("4.7 - Max games in range handles gracefully", .timeLimit(.minutes(5)))
func test_planByDates_MaxGamesInRange_HandlesGracefully() {
// Setup: Generate 10K games using fixture generator
let config = FixtureGenerator.Configuration(
seed: 42,
gameCount: 10000,
stadiumCount: 30,
teamCount: 60,
dateRange: makeDate(day: 1, hour: 0)...makeDate(month: 9, day: 30, hour: 23),
geographicSpread: .nationwide
)
let data = FixtureGenerator.generate(with: config)
let request = makePlanningRequest(
startDate: makeDate(day: 1, hour: 0),
endDate: makeDate(month: 9, day: 30, hour: 23),
games: data.games,
stadiums: data.stadiumsById
)
// Execute with timing
let startTime = Date()
let result = planner.plan(request: request)
let elapsed = Date().timeIntervalSince(startTime)
// Verify: Should complete without crash/hang
#expect(elapsed < TestConstants.performanceTimeout,
"Should complete within performance timeout")
// Should produce some result (success or failure is acceptable)
// The key is that it doesn't crash or hang
if result.isSuccess {
#expect(!result.options.isEmpty, "If success, should have options")
}
// Failure is also acceptable for extreme scale (e.g., no valid routes)
}
// MARK: - 4C: Integration with DAG
@Test("4.8 - Uses DAG router for routing")
func test_planByDates_UsesDAGRouterForRouting() {
// Setup: Games that require DAG routing logic
// Create games in multiple cities with feasible transitions
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
let detroitId = "stadium_detroit_\(UUID().uuidString)"
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit]
// Games that can form a sensible route: Chicago Milwaukee Detroit
let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19))
let game3 = makeGame(stadiumId: detroitId, dateTime: makeDate(day: 9, hour: 19))
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [game1, game2, game3],
stadiums: stadiums
)
// Execute
let result = planner.plan(request: request)
// Verify: DAG router should produce routes
#expect(result.isSuccess, "Should succeed with routable games")
#expect(!result.options.isEmpty, "Should produce routes")
// Verify routes are in chronological order (DAG property)
for option in result.options {
// Stops should be in order that respects game times
var previousGameDate: Date?
for stop in option.stops {
if let firstGameId = stop.games.first,
let game = [game1, game2, game3].first(where: { $0.id == firstGameId }) {
if let prev = previousGameDate {
#expect(game.startTime >= prev,
"Games should be in chronological order (DAG property)")
}
previousGameDate = game.startTime
}
}
}
}
@Test("4.9 - Respects driver constraints")
func test_planByDates_RespectsDriverConstraints() {
// Setup: Games that would require excessive daily driving if constraints are loose
let nycId = "stadium_nyc_\(UUID().uuidString)"
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
// NYC to Chicago is ~790 miles (~13 hours of driving)
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [nycId: nyc, chicagoId: chicago]
// Games on consecutive days - can't drive 790 miles in 8 hours (single driver)
let game1 = makeGame(stadiumId: nycId, dateTime: makeDate(day: 5, hour: 14))
let game2 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 6, hour: 19))
// Test with strict constraints (1 driver, 8 hours max)
let strictRequest = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 7, hour: 23),
games: [game1, game2],
stadiums: stadiums,
numberOfDrivers: 1,
maxDrivingHoursPerDriver: 8.0
)
let strictResult = planner.plan(request: strictRequest)
// With strict constraints, should NOT have a route with both games on consecutive days
if strictResult.isSuccess {
let hasConsecutiveDayRoute = strictResult.options.contains { option in
option.totalGames == 2 && option.stops.count == 2
}
// If there's a 2-game route, verify it has adequate travel time
if hasConsecutiveDayRoute, let twoGameOption = strictResult.options.first(where: { $0.totalGames == 2 }) {
// With only 1 day between games, ~13 hours of driving is too much for 8hr/day limit
// The route should either not exist or have adequate travel days
let totalHours = twoGameOption.totalDrivingHours
let daysAvailable = 1.0 // Only 1 day between games
let hoursPerDay = totalHours / daysAvailable
// This assertion is soft - the router may reject this route entirely
#expect(hoursPerDay <= 8.0 || !hasConsecutiveDayRoute,
"Route should respect daily driving limits")
}
}
// Test with relaxed constraints (2 drivers = 16 hours max per day)
let relaxedRequest = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 7, hour: 23),
games: [game1, game2],
stadiums: stadiums,
numberOfDrivers: 2,
maxDrivingHoursPerDriver: 8.0
)
let relaxedResult = planner.plan(request: relaxedRequest)
// With 2 drivers (16 hours/day), the trip becomes more feasible
// Note: 790 miles at 60mph is ~13 hours, which fits in 16 hours
if relaxedResult.isSuccess {
// Should have more routing options with relaxed constraints
#expect(relaxedResult.options.count >= 1,
"Should have options with relaxed driver constraints")
}
}
// MARK: - 4D: Must-Stop Filtering (Issues #4 & #8)
@Test("4.10 - Must-stop filters to games in that city")
func test_planByDates_MustStop_FiltersToGamesInCity() {
// Setup: Games in Chicago, Milwaukee, Detroit
// Must-stop = Chicago should only return Chicago games
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
let detroitId = "stadium_detroit_\(UUID().uuidString)"
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit]
let chicagoGame = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
let milwaukeeGame = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19))
let detroitGame = makeGame(stadiumId: detroitId, dateTime: makeDate(day: 9, hour: 19))
let mustStop = LocationInput(name: "Chicago")
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [chicagoGame, milwaukeeGame, detroitGame],
stadiums: stadiums,
mustStopLocations: [mustStop]
)
// Execute
let result = planner.plan(request: request)
// Verify: Should succeed and ONLY include Chicago games
#expect(result.isSuccess, "Should succeed with must-stop in Chicago")
#expect(!result.options.isEmpty, "Should return at least one option")
// All games in result should be in Chicago only
for option in result.options {
let allGameIds = Set(option.stops.flatMap { $0.games })
#expect(allGameIds.contains(chicagoGame.id), "Should include Chicago game")
#expect(!allGameIds.contains(milwaukeeGame.id), "Should NOT include Milwaukee game")
#expect(!allGameIds.contains(detroitGame.id), "Should NOT include Detroit game")
}
}
@Test("4.11 - Must-stop with no matching games returns failure")
func test_planByDates_MustStop_NoMatchingGames_ReturnsFailure() {
// Setup: Games only in Milwaukee and Detroit
// Must-stop = Chicago no games there, should fail
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
let detroitId = "stadium_detroit_\(UUID().uuidString)"
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
let stadiums = [milwaukeeId: milwaukee, detroitId: detroit]
let milwaukeeGame = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19))
let detroitGame = makeGame(stadiumId: detroitId, dateTime: makeDate(day: 9, hour: 19))
let mustStop = LocationInput(name: "Chicago")
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [milwaukeeGame, detroitGame],
stadiums: stadiums,
mustStopLocations: [mustStop]
)
// Execute
let result = planner.plan(request: request)
// Verify: Should fail with noGamesInRange (no home games in Chicago)
#expect(!result.isSuccess, "Should fail when no games in must-stop city")
#expect(result.failure?.reason == .noGamesInRange,
"Should return noGamesInRange failure")
}
@Test("4.12 - Must-stop only returns HOME games (Issue #8)")
func test_planByDates_MustStop_OnlyReturnsHomeGames() {
// Setup: Cubs home game in Chicago + Cubs away game in Milwaukee (playing at Milwaukee)
// Must-stop = Chicago should ONLY return the Chicago home game
// This tests Issue #8: "Must stop needs to be home team"
let chicagoStadiumId = "stadium_chicago_\(UUID().uuidString)"
let milwaukeeStadiumId = "stadium_milwaukee_\(UUID().uuidString)"
let cubsTeamId = "team_cubs_\(UUID().uuidString)"
let brewersTeamId = "team_brewers_\(UUID().uuidString)"
let wrigleyField = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.9484, lon: -87.6553)
let millerPark = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0280, lon: -87.9712)
let stadiums = [chicagoStadiumId: wrigleyField, milwaukeeStadiumId: millerPark]
// Cubs HOME game at Wrigley (Chicago)
let cubsHomeGame = makeGame(
stadiumId: chicagoStadiumId,
homeTeamId: cubsTeamId,
awayTeamId: brewersTeamId,
dateTime: makeDate(day: 5, hour: 19)
)
// Cubs AWAY game at Miller Park (Milwaukee) - Cubs are playing but NOT at home
let cubsAwayGame = makeGame(
stadiumId: milwaukeeStadiumId,
homeTeamId: brewersTeamId,
awayTeamId: cubsTeamId,
dateTime: makeDate(day: 7, hour: 19)
)
let mustStop = LocationInput(name: "Chicago")
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [cubsHomeGame, cubsAwayGame],
stadiums: stadiums,
mustStopLocations: [mustStop]
)
// Execute
let result = planner.plan(request: request)
// Verify: Should succeed with ONLY the Chicago home game
#expect(result.isSuccess, "Should succeed with Chicago home game")
#expect(!result.options.isEmpty, "Should return at least one option")
// The away game in Milwaukee should NOT be included even though Cubs are playing
for option in result.options {
let allGameIds = Set(option.stops.flatMap { $0.games })
#expect(allGameIds.contains(cubsHomeGame.id), "Should include Cubs HOME game in Chicago")
#expect(!allGameIds.contains(cubsAwayGame.id), "Should NOT include Cubs AWAY game in Milwaukee")
}
}
@Test("4.13 - Must-stop with partial city name match works")
func test_planByDates_MustStop_PartialCityMatch_Works() {
// Setup: User types "Chicago" but stadium city is "Chicago, IL"
// Should still match via contains
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [chicagoId: chicago]
let game = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
// User might type partial name
let mustStop = LocationInput(name: "Chicago, IL")
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [game],
stadiums: stadiums,
mustStopLocations: [mustStop]
)
// Execute
let result = planner.plan(request: request)
// Verify: Should still find Chicago games with partial match
#expect(result.isSuccess, "Should succeed with partial city name match")
#expect(!result.options.isEmpty, "Should return options")
}
@Test("4.14 - Must-stop case insensitive")
func test_planByDates_MustStop_CaseInsensitive() {
// Setup: Must-stop = "CHICAGO" (uppercase) should match "Chicago"
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [chicagoId: chicago]
let game = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
let mustStop = LocationInput(name: "CHICAGO")
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [game],
stadiums: stadiums,
mustStopLocations: [mustStop]
)
// Execute
let result = planner.plan(request: request)
// Verify: Case insensitive match
#expect(result.isSuccess, "Should succeed with case-insensitive match")
}
@Test("4.15 - Multiple games in must-stop city all included")
func test_planByDates_MustStop_MultipleGamesInCity_AllIncluded() {
// Setup: Multiple games in Chicago on different days
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let stadiums = [chicagoId: chicago]
let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
let game2 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 7, hour: 13))
let game3 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 9, hour: 19))
let mustStop = LocationInput(name: "Chicago")
let request = makePlanningRequest(
startDate: makeDate(day: 4, hour: 0),
endDate: makeDate(day: 10, hour: 23),
games: [game1, game2, game3],
stadiums: stadiums,
mustStopLocations: [mustStop]
)
// Execute
let result = planner.plan(request: request)
// Verify: All Chicago games should be included
#expect(result.isSuccess, "Should succeed with multiple games in must-stop city")
if let option = result.options.first {
let allGameIds = Set(option.stops.flatMap { $0.games })
#expect(allGameIds.contains(game1.id), "Should include first Chicago game")
#expect(allGameIds.contains(game2.id), "Should include second Chicago game")
#expect(allGameIds.contains(game3.id), "Should include third Chicago game")
}
}
}