Stadium Progress & Achievements: - Add StadiumVisit and Achievement SwiftData models - Create Progress tab with interactive map view - Implement photo-based visit import with GPS/date matching - Add achievement badges (count-based, regional, journey) - Create shareable progress cards for social media - Add canonical data infrastructure (stadium identities, team aliases) - Implement score resolution from free APIs (MLB, NBA, NHL stats) UI Improvements: - Add ThemedSpinner and ThemedSpinnerCompact components - Replace all ProgressView() with themed spinners throughout app - Fix sport selection state not persisting when navigating away Bug Fixes: - Fix Coast to Coast trips showing only 1 city (validation issue) - Fix stadium progress showing 0/0 (filtering issue) - Remove "Stadium Quest" title from progress view 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1442 lines
48 KiB
Swift
1442 lines
48 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("ScenarioBPlanner Tests")
|
|
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")
|
|
}
|
|
}
|
|
}
|