Files
Sportstime/SportsTimeTests/ScenarioBPlannerTests.swift
2026-01-10 16:20:13 -06:00

1834 lines
63 KiB
Swift

//
// ScenarioBPlannerTests.swift
// SportsTimeTests
//
// Comprehensive tests for ScenarioBPlanner: Selected games scenario.
// Tests sliding window logic, anchor game requirements, and route generation.
//
import Testing
import Foundation
import CoreLocation
@testable import SportsTime
@Suite(.serialized)
struct ScenarioBPlannerTests {
// MARK: - Test Fixtures
private func makeStadium(
id: UUID = UUID(),
name: String,
city: String,
state: String,
latitude: Double,
longitude: Double,
sport: Sport = .mlb
) -> Stadium {
Stadium(
id: id,
name: name,
city: city,
state: state,
latitude: latitude,
longitude: longitude,
capacity: 40000,
sport: sport
)
}
private func makeGame(
id: UUID = UUID(),
stadiumId: UUID,
date: Date
) -> Game {
Game(
id: id,
homeTeamId: UUID(),
awayTeamId: UUID(),
stadiumId: stadiumId,
dateTime: date,
sport: .mlb,
season: "2026"
)
}
private func date(_ string: String) -> Date {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
formatter.timeZone = TimeZone(identifier: "America/Los_Angeles")
return formatter.date(from: string)!
}
private func makeRequest(
games: [Game],
stadiums: [UUID: Stadium],
mustSeeGameIds: Set<UUID>,
startDate: Date,
endDate: Date,
tripDuration: Int? = nil,
numberOfDrivers: Int = 1
) -> PlanningRequest {
var prefs = TripPreferences(
startDate: startDate,
endDate: endDate,
tripDuration: tripDuration,
numberOfDrivers: numberOfDrivers
)
prefs.mustSeeGameIds = mustSeeGameIds
return PlanningRequest(
preferences: prefs,
availableGames: games,
teams: [:],
stadiums: stadiums
)
}
// Standard test stadiums - close together for feasible routes
private var laStadium: Stadium {
makeStadium(id: UUID(), name: "Dodger Stadium", city: "Los Angeles", state: "CA", latitude: 34.0739, longitude: -118.2400)
}
private var sfStadium: Stadium {
makeStadium(id: UUID(), name: "Oracle Park", city: "San Francisco", state: "CA", latitude: 37.7786, longitude: -122.3893)
}
private var sdStadium: Stadium {
makeStadium(id: UUID(), name: "Petco Park", city: "San Diego", state: "CA", latitude: 32.7076, longitude: -117.1570)
}
private var phoenixStadium: Stadium {
makeStadium(id: UUID(), name: "Chase Field", city: "Phoenix", state: "AZ", latitude: 33.4455, longitude: -112.0667)
}
private var denverStadium: Stadium {
makeStadium(id: UUID(), name: "Coors Field", city: "Denver", state: "CO", latitude: 39.7559, longitude: -104.9942)
}
// MARK: - Basic Validation Tests
@Test("Empty selected games returns failure")
func plan_EmptySelectedGames_ReturnsFailure() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [], // Empty!
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59")
)
let result = planner.plan(request: request)
if case .failure(let failure) = result {
#expect(failure.reason == .noValidRoutes)
#expect(failure.violations.contains { $0.type == .selectedGames })
} else {
Issue.record("Expected failure for empty selected games")
}
}
@Test("Single selected game returns success")
func plan_SingleSelectedGame_ReturnsSuccess() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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)
#expect(options.first?.stops.count == 1)
} else {
Issue.record("Expected success for single selected game")
}
}
@Test("No date range or duration returns failure")
func plan_NoDateRangeOrDuration_ReturnsFailure() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
// Use inverted date range to trigger nil dateRange
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
startDate: date("2026-06-30 00:00"),
endDate: date("2026-06-01 00:00"), // Before start!
tripDuration: 0 // Zero duration
)
let result = planner.plan(request: request)
if case .failure(let failure) = result {
#expect(failure.reason == .missingDateRange)
} else {
// This test may pass if the system handles it differently
}
}
// MARK: - Selected Games Anchor Tests
@Test("Selected game appears in all options")
func plan_SelectedGame_AppearsInAllOptions() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let otherGame = makeGame(stadiumId: sf.id, date: date("2026-06-16 19:00"))
let request = makeRequest(
games: [selectedGame, otherGame],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [selectedGame.id],
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 {
for option in options {
let allGameIds = option.stops.flatMap { $0.games }
#expect(allGameIds.contains(selectedGame.id), "Selected game must be in every option")
}
} else {
Issue.record("Expected success")
}
}
@Test("Multiple selected games all appear in options")
func plan_MultipleSelectedGames_AllAppearInOptions() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
for option in options {
let allGameIds = option.stops.flatMap { $0.games }
#expect(allGameIds.contains(game1.id), "First selected game must be present")
#expect(allGameIds.contains(game2.id), "Second selected game must be present")
}
} else {
Issue.record("Expected success")
}
}
@Test("Selected games outside date range fail")
func plan_SelectedGamesOutsideRange_Fails() {
let planner = ScenarioBPlanner()
let la = laStadium
// Game is in July, date range is June
let game = makeGame(stadiumId: la.id, date: date("2026-07-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59")
)
let result = planner.plan(request: request)
if case .failure = result {
// Expected
} else {
Issue.record("Expected failure when selected games outside date range")
}
}
// MARK: - Sliding Window Tests
@Test("Trip duration generates sliding windows")
func plan_TripDuration_GeneratesSlidingWindows() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Selected games on day 5 and day 8
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-08 19:00"))
// Bonus games on various days
let bonus1 = makeGame(stadiumId: la.id, date: date("2026-06-03 19:00"))
let bonus2 = makeGame(stadiumId: sf.id, date: date("2026-06-12 19:00"))
let request = makeRequest(
games: [game1, game2, bonus1, bonus2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
startDate: date("2026-05-01 00:00"),
endDate: date("2026-05-01 00:00"), // Trigger sliding window mode
tripDuration: 10
)
let result = planner.plan(request: request)
// Should succeed - sliding windows cover both selected games
if case .success(let options) = result {
#expect(!options.isEmpty)
} else {
// May fail due to date range issues - that's ok
}
}
@Test("Short duration fits selected games")
func plan_ShortDuration_FitsSelectedGames() {
let planner = ScenarioBPlanner()
let la = laStadium
// Games 2 days apart
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-07 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la],
mustSeeGameIds: [game1.id, game2.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59"),
tripDuration: 3 // Just enough to fit
)
let result = planner.plan(request: request)
if case .success(let options) = result {
#expect(!options.isEmpty)
} else {
Issue.record("Expected success when duration fits games")
}
}
@Test("Duration equal to game span works")
func plan_DurationEqualsGameSpan_Works() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Games 5 days apart
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-01 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-06 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-10 23:59")
)
let result = planner.plan(request: request)
if case .success = result {
// Expected
} else {
Issue.record("Expected success when duration equals game span")
}
}
// MARK: - Bonus Games Tests
@Test("Bonus games added to route")
func plan_BonusGames_AddedToRoute() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium
let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let bonusGame1 = makeGame(stadiumId: sf.id, date: date("2026-06-16 19:00"))
let bonusGame2 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00"))
let request = makeRequest(
games: [selectedGame, bonusGame1, bonusGame2],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [selectedGame.id],
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 {
// Should have options with bonus games
let maxGames = options.map { $0.stops.flatMap { $0.games }.count }.max() ?? 0
#expect(maxGames > 1, "Should include bonus games in some options")
} else {
Issue.record("Expected success")
}
}
@Test("Geographic rationale shows selected and bonus counts")
func plan_GeographicRationale_ShowsSelectedAndBonusCounts() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let bonusGame = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [selectedGame, bonusGame],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [selectedGame.id],
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 {
// Find option with both games
if let optionWithBoth = options.first(where: {
$0.stops.flatMap { $0.games }.count > 1
}) {
#expect(optionWithBoth.geographicRationale.contains("selected"))
#expect(optionWithBoth.geographicRationale.contains("bonus"))
}
} else {
Issue.record("Expected success")
}
}
// MARK: - Travel Segments Tests
@Test("Two stops have one travel segment")
func plan_TwoStops_OneTravelSegment() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
for option in options {
if option.stops.count == 2 {
#expect(option.travelSegments.count == 1)
}
}
} else {
Issue.record("Expected success")
}
}
@Test("Single stop has no travel segments")
func plan_SingleStop_NoTravelSegments() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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 {
for option in options {
if option.stops.count == 1 {
#expect(option.travelSegments.isEmpty)
}
}
} else {
Issue.record("Expected success")
}
}
@Test("Travel segment has valid origin and destination")
func plan_TravelSegment_ValidOriginDestination() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
for option in options {
for segment in option.travelSegments {
#expect(!segment.fromLocation.name.isEmpty || segment.fromLocation.coordinate != nil)
#expect(!segment.toLocation.name.isEmpty || segment.toLocation.coordinate != nil)
}
}
} else {
Issue.record("Expected success")
}
}
// MARK: - Stop Building Tests
@Test("Games at same stadium grouped into one stop")
func plan_SameStadiumGames_GroupedIntoOneStop() {
let planner = ScenarioBPlanner()
let la = laStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-16 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la],
mustSeeGameIds: [game1.id, game2.id],
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 {
if let option = options.first {
#expect(option.stops.count == 1, "Two games at same stadium = one stop")
#expect(option.stops.first?.games.count == 2, "Stop should have both games")
}
} else {
Issue.record("Expected success")
}
}
@Test("Stop city matches stadium city")
func plan_StopCity_MatchesStadiumCity() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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 {
if let stop = options.first?.stops.first {
#expect(stop.city == "Los Angeles")
#expect(stop.state == "CA")
}
} else {
Issue.record("Expected success")
}
}
@Test("Stop coordinate matches stadium")
func plan_StopCoordinate_MatchesStadium() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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 {
if let stop = options.first?.stops.first {
#expect(stop.coordinate != nil)
#expect(abs(stop.coordinate!.latitude - 34.0739) < 0.01)
}
} else {
Issue.record("Expected success")
}
}
@Test("Stop arrival date is first game date")
func plan_StopArrivalDate_IsFirstGameDate() {
let planner = ScenarioBPlanner()
let la = laStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la],
mustSeeGameIds: [game1.id, game2.id],
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 {
if let stop = options.first?.stops.first {
let calendar = Calendar.current
let arrivalDay = calendar.component(.day, from: stop.arrivalDate)
#expect(arrivalDay == 15)
}
} else {
Issue.record("Expected success")
}
}
@Test("Stop departure date is last game date")
func plan_StopDepartureDate_IsLastGameDate() {
let planner = ScenarioBPlanner()
let la = laStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la],
mustSeeGameIds: [game1.id, game2.id],
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 {
if let stop = options.first?.stops.first {
let calendar = Calendar.current
let departureDay = calendar.component(.day, from: stop.departureDate)
#expect(departureDay == 17)
}
} else {
Issue.record("Expected success")
}
}
// MARK: - Ranking Tests
@Test("Options ranked from 1")
func plan_OptionsRankedFromOne() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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.first?.rank == 1)
for (index, option) in options.enumerated() {
#expect(option.rank == index + 1)
}
} else {
Issue.record("Expected success")
}
}
@Test("More games ranked higher")
func plan_MoreGames_RankedHigher() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-16 19:00"))
let game3 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00"))
let request = makeRequest(
games: [game1, game2, game3],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [game1.id],
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 {
// Options should be sorted by game count (most first)
var prevCount = Int.max
for option in options {
let count = option.stops.flatMap { $0.games }.count
#expect(count <= prevCount, "Should be sorted by game count descending")
prevCount = count
}
} else {
Issue.record("Expected success")
}
}
@Test("Max 10 options returned")
func plan_Max10OptionsReturned() {
let planner = ScenarioBPlanner()
let la = laStadium
// Create many games in same city to generate many sliding window options
var games: [Game] = []
for day in 1...20 {
games.append(makeGame(
stadiumId: la.id,
date: date("2026-06-\(String(format: "%02d", day)) 19:00")
))
}
// Use tripDuration to enable sliding windows (many possible 5-day windows)
// With 20 games and 5-day duration, there will be many valid windows
let request = makeRequest(
games: games,
stadiums: [la.id: la],
mustSeeGameIds: [games[10].id], // Select game in middle
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59"),
tripDuration: 5
)
let result = planner.plan(request: request)
if case .success(let options) = result {
#expect(options.count <= 10, "Should return at most 10 options")
#expect(options.count > 0, "Should return at least one option")
} else {
Issue.record("Expected success")
}
}
// MARK: - Driving Constraints Tests
@Test("Two drivers allows longer routes")
func plan_TwoDrivers_AllowsLongerRoutes() {
let planner = ScenarioBPlanner()
let la = laStadium
let phoenix = phoenixStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: phoenix.id, date: date("2026-06-16 19:00"))
let request1 = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, phoenix.id: phoenix],
mustSeeGameIds: [game1.id, game2.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59"),
numberOfDrivers: 1
)
let request2 = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, phoenix.id: phoenix],
mustSeeGameIds: [game1.id, game2.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59"),
numberOfDrivers: 2
)
let result1 = planner.plan(request: request1)
let result2 = planner.plan(request: request2)
// Both should succeed for short LA-Phoenix trip
// But 2 drivers gives more flexibility
if case .success(let options1) = result1, case .success(let options2) = result2 {
#expect(!options1.isEmpty && !options2.isEmpty)
}
}
@Test("Exceeding daily driving limit fails route")
func plan_ExceedingDailyLimit_FailsRoute() {
let planner = ScenarioBPlanner()
// Create far-apart stadiums
let nyStadium = makeStadium(
name: "Yankee Stadium",
city: "New York",
state: "NY",
latitude: 40.8296,
longitude: -73.9262
)
let sfStadium = makeStadium(
name: "Oracle Park",
city: "San Francisco",
state: "CA",
latitude: 37.7786,
longitude: -122.3893
)
// Games only 1 day apart - impossible to drive
let game1 = makeGame(stadiumId: nyStadium.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sfStadium.id, date: date("2026-06-16 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [nyStadium.id: nyStadium, sfStadium.id: sfStadium],
mustSeeGameIds: [game1.id, game2.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59"),
numberOfDrivers: 1
)
let result = planner.plan(request: request)
// Should fail - can't drive NY to SF in one day
if case .failure = result {
// Expected
} else {
// Also acceptable if it routes but validates later
}
}
// MARK: - Edge Cases
@Test("All games in future works")
func plan_AllGamesInFuture_Works() {
let planner = ScenarioBPlanner()
let la = laStadium
let futureDate = Calendar.current.date(byAdding: .year, value: 1, to: Date())!
let game = makeGame(stadiumId: la.id, date: futureDate)
let rangeStart = Calendar.current.date(byAdding: .day, value: -30, to: futureDate)!
let rangeEnd = Calendar.current.date(byAdding: .day, value: 30, to: futureDate)!
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
startDate: rangeStart,
endDate: rangeEnd
)
let result = planner.plan(request: request)
if case .success(let options) = result {
#expect(!options.isEmpty)
} else {
Issue.record("Expected success for future games")
}
}
@Test("Games in chronological order processed correctly")
func plan_ChronologicalGames_ProcessedCorrectly() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium
let game1 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game3 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2, game3],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [game1.id, game2.id, game3.id],
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)
// First stop should be SD (earliest game)
if let firstStop = options.first?.stops.first {
#expect(firstStop.city == "San Diego")
}
} else {
Issue.record("Expected success")
}
}
@Test("Reverse chronological games reordered")
func plan_ReverseChronologicalGames_Reordered() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Input games in reverse order
let game1 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game1, game2], // SF first, LA second
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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)
// LA should come before SF in route
if let option = options.first, option.stops.count == 2 {
#expect(option.stops[0].city == "Los Angeles")
#expect(option.stops[1].city == "San Francisco")
}
} else {
Issue.record("Expected success")
}
}
@Test("First game start time set correctly")
func plan_FirstGameStartTime_SetCorrectly() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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 {
if let stop = options.first?.stops.first {
#expect(stop.firstGameStart != nil)
// Verify it's the same as the game's dateTime
#expect(stop.firstGameStart == game.dateTime)
}
} else {
Issue.record("Expected success")
}
}
@Test("Location property has correct name")
func plan_LocationProperty_CorrectName() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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 {
if let stop = options.first?.stops.first {
#expect(stop.location.name == "Los Angeles")
}
} else {
Issue.record("Expected success")
}
}
@Test("Empty stadiums dictionary still works")
func plan_EmptyStadiums_StillWorks() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [:], // Empty!
mustSeeGameIds: [game.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59")
)
let result = planner.plan(request: request)
// Should still produce some result (with "Unknown" city)
if case .success(let options) = result {
#expect(!options.isEmpty)
#expect(options.first?.stops.first?.city == "Unknown")
}
}
@Test("Total driving hours computed correctly")
func plan_TotalDrivingHours_ComputedCorrectly() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
for option in options {
if option.stops.count > 1 {
#expect(option.totalDrivingHours > 0)
// LA to SF is ~380 miles, ~6 hours
#expect(option.totalDrivingHours < 20, "LA-SF shouldn't take 20 hours")
}
}
} else {
Issue.record("Expected success")
}
}
@Test("Total distance miles computed correctly")
func plan_TotalDistanceMiles_ComputedCorrectly() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
for option in options {
if option.stops.count > 1 {
#expect(option.totalDistanceMiles > 0)
// LA to SF is ~380 miles (with 1.3x factor ~500 miles)
#expect(option.totalDistanceMiles > 300)
#expect(option.totalDistanceMiles < 1000)
}
}
} else {
Issue.record("Expected success")
}
}
@Test("Geographic rationale shows cities")
func plan_GeographicRationale_ShowsCities() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
if let option = options.first {
#expect(option.geographicRationale.contains("Los Angeles") ||
option.geographicRationale.contains("San Francisco"))
}
} else {
Issue.record("Expected success")
}
}
// MARK: - Complex Scenario Tests
@Test("Three city route validates chronologically")
func plan_ThreeCityRoute_ValidatesChronologically() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium
let game1 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-16 19:00"))
let game3 = makeGame(stadiumId: sf.id, date: date("2026-06-18 19:00"))
let request = makeRequest(
games: [game1, game2, game3],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [game1.id, game2.id, game3.id],
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)
// Route should be SD LA SF (chronological)
if let option = options.first, option.stops.count == 3 {
#expect(option.stops[0].city == "San Diego")
#expect(option.stops[1].city == "Los Angeles")
#expect(option.stops[2].city == "San Francisco")
}
} else {
Issue.record("Expected success")
}
}
@Test("Partial selection with bonus games")
func plan_PartialSelection_WithBonusGames() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium
let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let bonusGame1 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let bonusGame2 = makeGame(stadiumId: sd.id, date: date("2026-06-13 19:00"))
let request = makeRequest(
games: [selectedGame, bonusGame1, bonusGame2],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [selectedGame.id], // Only one selected
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 {
// All options must have the selected game
for option in options {
let allGames = option.stops.flatMap { $0.games }
#expect(allGames.contains(selectedGame.id))
}
// Some options may have bonus games
let maxGames = options.map { $0.stops.flatMap { $0.games }.count }.max() ?? 0
#expect(maxGames >= 1)
} else {
Issue.record("Expected success")
}
}
@Test("Same day games at different stadiums")
func plan_SameDayDifferentStadiums_CreatesMultipleStops() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Both games on same day but different stadiums - will fail as impossible to attend both
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 14:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-15 20:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59")
)
let result = planner.plan(request: request)
// May fail due to impossibility or generate separate options
// Either is acceptable
switch result {
case .success, .failure:
break // Either is acceptable
}
}
@Test("Wide date range with few games")
func plan_WideDateRangeFewGames_Succeeds() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
// Very wide date range (3 months)
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
startDate: date("2026-04-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)
} else {
Issue.record("Expected success")
}
}
@Test("Narrow date range matches exactly")
func plan_NarrowDateRange_MatchesExactly() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
// Narrow date range (1 day)
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
startDate: date("2026-06-15 00:00"),
endDate: date("2026-06-15 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
#expect(!options.isEmpty)
} else {
Issue.record("Expected success for exact date match")
}
}
@Test("All options have valid structure")
func plan_AllOptions_HaveValidStructure() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
for option in options {
// Rank is positive
#expect(option.rank > 0)
// Stops non-empty
#expect(!option.stops.isEmpty)
// Travel segments = stops - 1 (or 0 for single stop)
if option.stops.count > 1 {
#expect(option.travelSegments.count == option.stops.count - 1)
}
// Driving hours is reasonable
#expect(option.totalDrivingHours >= 0)
// Distance is reasonable
#expect(option.totalDistanceMiles >= 0)
// Has rationale
#expect(!option.geographicRationale.isEmpty)
}
} else {
Issue.record("Expected success")
}
}
@Test("Travel segments are drive mode")
func plan_TravelSegments_AreDriveMode() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [game1.id, game2.id],
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 {
for option in options {
for segment in option.travelSegments {
#expect(segment.travelMode == .drive)
}
}
} else {
Issue.record("Expected success")
}
}
@Test("Stops have games array populated")
func plan_StopsHaveGamesArray_Populated() {
let planner = ScenarioBPlanner()
let la = laStadium
let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game],
stadiums: [la.id: la],
mustSeeGameIds: [game.id],
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 {
for option in options {
for stop in option.stops {
#expect(!stop.games.isEmpty, "Each stop must have at least one game")
}
}
} else {
Issue.record("Expected success")
}
}
@Test("Multiple games at single stop sorted by time")
func plan_MultipleGamesAtStop_SortedByTime() {
let planner = ScenarioBPlanner()
let la = laStadium
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-16 19:00"))
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00"))
let request = makeRequest(
games: [game1, game2],
stadiums: [la.id: la],
mustSeeGameIds: [game1.id, game2.id],
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 {
if let stop = options.first?.stops.first {
// game2 (June 15) should be first, game1 (June 16) second
#expect(stop.games.count == 2)
#expect(stop.games[0] == game2.id) // Earlier game first
#expect(stop.games[1] == game1.id) // Later game second
}
} else {
Issue.record("Expected success")
}
}
@Test("Failure contains constraint violation details")
func plan_Failure_ContainsViolationDetails() {
let planner = ScenarioBPlanner()
// Empty selected games should cause failure with details
let request = makeRequest(
games: [],
stadiums: [:],
mustSeeGameIds: [],
startDate: date("2026-06-01 00:00"),
endDate: date("2026-06-30 23:59")
)
let result = planner.plan(request: request)
if case .failure(let failure) = result {
#expect(!failure.violations.isEmpty)
#expect(failure.violations.first?.severity == .error)
#expect(!failure.violations.first!.description.isEmpty)
} else {
Issue.record("Expected failure")
}
}
@Test("Handles many simultaneous selected games")
func plan_ManySelectedGames_HandlesEfficiently() {
let planner = ScenarioBPlanner()
let la = laStadium
// Create 10 selected games at same stadium
var games: [Game] = []
var gameIds: Set<UUID> = []
for day in 1...10 {
let game = makeGame(
stadiumId: la.id,
date: date("2026-06-\(String(format: "%02d", day)) 19:00")
)
games.append(game)
gameIds.insert(game.id)
}
let request = makeRequest(
games: games,
stadiums: [la.id: la],
mustSeeGameIds: gameIds,
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 be one stop with 10 games
if let option = options.first {
#expect(option.stops.count == 1)
#expect(option.stops.first?.games.count == 10)
}
} else {
Issue.record("Expected success")
}
}
// MARK: - Filler Game Conflict Tests (Phase 09-02)
@Test("filler game between must-see games is included when feasible")
func plan_FillerBetweenAnchors_IncludedWhenFeasible() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Create San Jose stadium (between LA and SF)
let sjStadium = makeStadium(
name: "San Jose Stadium",
city: "San Jose",
state: "CA",
latitude: 37.3382,
longitude: -121.8863
)
// Must-see: LA Jan 5 1pm, SF Jan 7 7pm
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00"))
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-07 19:00"))
// Filler: San Jose Jan 6 7pm (between LA and SF, feasible timing)
let sjGame = makeGame(stadiumId: sjStadium.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, sfGame, sjGame],
stadiums: [la.id: la, sf.id: sf, sjStadium.id: sjStadium],
mustSeeGameIds: [laGame.id, sfGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Should have a 3-stop route: LA SJ SF
let threeStopOption = options.first { $0.stops.count == 3 }
#expect(threeStopOption != nil, "Should have route with filler game included")
if let option = threeStopOption {
let cities = option.stops.map { $0.city }
#expect(cities.contains("San Jose"), "Filler city should be included")
}
} else {
Issue.record("Expected success")
}
}
@Test
func plan_FillerSameDayAsAnchor_Excluded() {
let planner = ScenarioBPlanner()
let la = laStadium
// Create Anaheim stadium (30 miles from LA)
let anaheimStadium = makeStadium(
name: "Angel Stadium",
city: "Anaheim",
state: "CA",
latitude: 33.8003,
longitude: -117.8827
)
// Must-see: LA Jan 5 7pm
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
// Filler: Anaheim Jan 5 7pm (same time, different city - impossible)
let anaheimGame = makeGame(stadiumId: anaheimStadium.id, date: date("2026-01-05 19:00"))
let request = makeRequest(
games: [laGame, anaheimGame],
stadiums: [la.id: la, anaheimStadium.id: anaheimStadium],
mustSeeGameIds: [laGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// All options must include LA game (anchor)
for option in options {
let allGameIds = option.stops.flatMap { $0.games }
#expect(allGameIds.contains(laGame.id), "Anchor game must be present")
// If both games in same option, they cannot be at same time
if allGameIds.contains(anaheimGame.id) {
Issue.record("Filler game at same time as anchor should be excluded")
}
}
} else {
Issue.record("Expected success")
}
}
@Test("filler requiring backtracking is excluded or route reordered")
func plan_FillerRequiringBacktrack_ExcludedOrReordered() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium // San Diego (south of LA)
// Must-see: LA Jan 5 7pm, SF Jan 7 7pm (northbound route)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-07 19:00"))
// Filler: San Diego Jan 6 7pm (south of LA, requires backtrack from northbound route)
let sdGame = makeGame(stadiumId: sd.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, sfGame, sdGame],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [laGame.id, sfGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Check if SD is included in any route
for option in options {
let cities = option.stops.map { $0.city }
if cities.contains("San Diego") {
// If SD included, route should be reordered: SD LA SF
// OR it should be a separate option
if cities.count == 3 {
// Check it's not inefficient backtracking: LA SF SD or LA SD SF (backtrack)
let cityOrder = cities.joined(separator: "")
#expect(
cityOrder == "San Diego→Los Angeles→San Francisco",
"If filler included, route should avoid backtracking"
)
}
}
}
} else {
Issue.record("Expected success")
}
}
@Test("multiple filler options only feasible one included")
func plan_MultipleFillers_OnlyFeasibleIncluded() {
let planner = ScenarioBPlanner()
let la = laStadium
let phoenix = phoenixStadium
// Create Tucson (between LA and Phoenix)
let tucsonStadium = makeStadium(
name: "Tucson Stadium",
city: "Tucson",
state: "AZ",
latitude: 32.2226,
longitude: -110.9747
)
// Must-see: LA Jan 5 1pm, Phoenix Jan 7 7pm (eastbound)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00"))
let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-01-07 19:00"))
// Filler A: SF Jan 6 7pm (300mi north, wrong direction from LAPhoenix)
let sfGame = makeGame(stadiumId: sfStadium.id, date: date("2026-01-06 19:00"))
// Filler B: Tucson Jan 6 7pm (100mi from Phoenix, on the way)
let tucsonGame = makeGame(stadiumId: tucsonStadium.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, phoenixGame, sfGame, tucsonGame],
stadiums: [la.id: la, phoenix.id: phoenix, sfStadium.id: sfStadium, tucsonStadium.id: tucsonStadium],
mustSeeGameIds: [laGame.id, phoenixGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Look for routes with 3 stops
let threeStopOptions = options.filter { $0.stops.count == 3 }
for option in threeStopOptions {
let cities = option.stops.map { $0.city }
// If Tucson included, SF should not be (wrong direction)
if cities.contains("Tucson") {
#expect(!cities.contains("San Francisco"),
"Should not include SF when Tucson is better option")
}
// If SF included, Tucson should not be (SF is wrong direction)
if cities.contains("San Francisco") {
#expect(!cities.contains("Tucson"),
"Should not include Tucson when SF chosen (though Tucson is better)")
}
}
} else {
Issue.record("Expected success")
}
}
// MARK: - Impossible Geographic Combination Tests (Phase 09-02)
@Test
func plan_MustSeeGamesTooFarApart_Fails() {
let planner = ScenarioBPlanner()
// Create NY and LA stadiums (2800 miles, 42 hour drive)
let nyStadium = makeStadium(
name: "Yankee Stadium",
city: "New York",
state: "NY",
latitude: 40.8296,
longitude: -73.9262
)
let laStadium = self.laStadium
// Must-see: LA Jan 5 7pm, NY Jan 6 7pm (24 hours available)
let laGame = makeGame(stadiumId: laStadium.id, date: date("2026-01-05 19:00"))
let nyGame = makeGame(stadiumId: nyStadium.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, nyGame],
stadiums: [laStadium.id: laStadium, nyStadium.id: nyStadium],
mustSeeGameIds: [laGame.id, nyGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .failure(let failure) = result {
// Expected - cannot drive 2800mi in 24hr
#expect(failure.reason == .constraintsUnsatisfiable ||
failure.reason == .drivingExceedsLimit)
} else {
Issue.record("Expected failure - impossible to drive LA to NY in 24 hours")
}
}
@Test("must-see games in reverse order geographically fail")
func plan_MustSeeGamesReverseOrder_Fails() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Must-see: SF Jan 5, LA Jan 4
// Problem: SF game is chronologically after LA, but geographically north
// This requires backtracking or violates date ordering
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-05 19:00"))
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-04 19:00"))
let request = makeRequest(
games: [sfGame, laGame],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [sfGame.id, laGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
// This may succeed OR fail depending on routing logic
// If it succeeds, verify route respects chronology (LA SF)
if case .success(let options) = result {
for option in options {
if option.stops.count == 2 {
// Route should be chronological: LA (Jan 4) SF (Jan 5)
#expect(option.stops[0].city == "Los Angeles")
#expect(option.stops[1].city == "San Francisco")
}
}
}
// Failure is also acceptable
}
@Test("three must-see games forming triangle routes or fails")
func plan_ThreeMustSeeTriangle_RoutesOrFails() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium
// Triangle: LA Jan 5, SF Jan 6, SD Jan 7
// Inefficient: LASFSD requires backtracking south
// Better: SDLASF (but violates chronology)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-06 19:00"))
let sdGame = makeGame(stadiumId: sd.id, date: date("2026-01-07 19:00"))
let request = makeRequest(
games: [laGame, sfGame, sdGame],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [laGame.id, sfGame.id, sdGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Should attempt route in chronological order: LASFSD
let threeStopOption = options.first { $0.stops.count == 3 }
if let option = threeStopOption {
// Verify chronological order is respected
#expect(option.stops[0].city == "Los Angeles")
#expect(option.stops[1].city == "San Francisco")
#expect(option.stops[2].city == "San Diego")
}
}
// Failure is also acceptable if deemed too inefficient
}
@Test("must-see games exceeding driving constraints fail")
func plan_MustSeeExceedingDrivingLimit_Fails() {
let planner = ScenarioBPlanner()
let la = laStadium
let phoenix = phoenixStadium
// Must-see: LA Jan 5 1pm, Phoenix Jan 5 7pm (380 miles, 6hr drive)
// Constraints: 1 driver, 4hr/day max
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00"))
let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-01-05 19:00"))
// Create custom request with reduced driving limit
var prefs = TripPreferences(
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59"),
tripDuration: nil,
numberOfDrivers: 1
)
prefs.mustSeeGameIds = [laGame.id, phoenixGame.id]
prefs.maxDrivingHoursPerDriver = 4.0 // Limit to 4 hours
let request = PlanningRequest(
preferences: prefs,
availableGames: [laGame, phoenixGame],
teams: [:],
stadiums: [la.id: la, phoenix.id: phoenix]
)
let result = planner.plan(request: request)
if case .failure(let failure) = result {
// Expected - 6hr drive exceeds 4hr limit
#expect(failure.reason == .drivingExceedsLimit ||
failure.reason == .constraintsUnsatisfiable)
} else {
Issue.record("Expected failure - 6hr drive exceeds 4hr daily limit")
}
}
@Test("feasible must-see combination succeeds (sanity)")
func plan_FeasibleMustSeeCombination_Succeeds() {
let planner = ScenarioBPlanner()
let la = laStadium
// Create Anaheim (30 miles from LA, easy drive)
let anaheimStadium = makeStadium(
name: "Angel Stadium",
city: "Anaheim",
state: "CA",
latitude: 33.8003,
longitude: -117.8827
)
// Must-see: LA Jan 5 7pm, Anaheim Jan 7 7pm (plenty of time)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
let anaheimGame = makeGame(stadiumId: anaheimStadium.id, date: date("2026-01-07 19:00"))
let request = makeRequest(
games: [laGame, anaheimGame],
stadiums: [la.id: la, anaheimStadium.id: anaheimStadium],
mustSeeGameIds: [laGame.id, anaheimGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
#expect(!options.isEmpty, "Feasible must-see combination should succeed")
// Verify both games appear in route
for option in options {
let allGameIds = option.stops.flatMap { $0.games }
#expect(allGameIds.contains(laGame.id))
#expect(allGameIds.contains(anaheimGame.id))
}
} else {
Issue.record("Expected success for feasible must-see combination")
}
}
}