2683 lines
96 KiB
Swift
2683 lines
96 KiB
Swift
//
|
|
// ScenarioCPlannerTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Comprehensive tests for ScenarioCPlanner: Directional route planning.
|
|
// Tests start/end location validation, directional stadium filtering,
|
|
// monotonic progress, and date range generation.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite(.serialized)
|
|
struct ScenarioCPlannerTests {
|
|
|
|
// 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],
|
|
startLocation: LocationInput?,
|
|
endLocation: LocationInput?,
|
|
startDate: Date,
|
|
endDate: Date,
|
|
tripDuration: Int? = nil,
|
|
numberOfDrivers: Int = 1
|
|
) -> PlanningRequest {
|
|
let prefs = TripPreferences(
|
|
startLocation: startLocation,
|
|
endLocation: endLocation,
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
tripDuration: tripDuration,
|
|
numberOfDrivers: numberOfDrivers
|
|
)
|
|
return PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: games,
|
|
teams: [:],
|
|
stadiums: stadiums
|
|
)
|
|
}
|
|
|
|
// West Coast stadiums for testing directional routes
|
|
private var sdStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Petco Park", city: "San Diego", state: "CA", latitude: 32.7076, longitude: -117.1570)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Cross-country stadiums for directional testing
|
|
private var chicagoStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Wrigley Field", city: "Chicago", state: "IL", latitude: 41.9484, longitude: -87.6553)
|
|
}
|
|
|
|
private var detroitStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Comerica Park", city: "Detroit", state: "MI", latitude: 42.3390, longitude: -83.0485)
|
|
}
|
|
|
|
private var clevelandStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Progressive Field", city: "Cleveland", state: "OH", latitude: 41.4962, longitude: -81.6852)
|
|
}
|
|
|
|
private var pittsburghStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "PNC Park", city: "Pittsburgh", state: "PA", latitude: 40.4469, longitude: -80.0057)
|
|
}
|
|
|
|
private var nyStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Yankee Stadium", city: "New York", state: "NY", latitude: 40.8296, longitude: -73.9262)
|
|
}
|
|
|
|
private var minneapolisStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Target Field", city: "Minneapolis", state: "MN", latitude: 44.9817, longitude: -93.2776)
|
|
}
|
|
|
|
// MARK: - Basic Success Tests
|
|
|
|
@Test("Single game between start and end succeeds")
|
|
func plan_SingleGameBetweenStartAndEnd_Succeeds() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let ny = nyStadium
|
|
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, cleveland.id: cleveland, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Multiple games along route succeeds")
|
|
func plan_MultipleGamesAlongRoute_Succeeds() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let pittsburgh = pittsburghStadium
|
|
let ny = nyStadium
|
|
|
|
let game1 = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: pittsburgh.id, date: date("2026-06-12 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [
|
|
chicago.id: chicago,
|
|
cleveland.id: cleveland,
|
|
pittsburgh.id: pittsburgh,
|
|
ny.id: ny
|
|
],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Missing Location Tests
|
|
|
|
@Test("Missing start location fails")
|
|
func plan_MissingStartLocation_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let ny = nyStadium
|
|
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [ny.id: ny],
|
|
startLocation: nil,
|
|
endLocation: endLoc,
|
|
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 == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for missing start location")
|
|
}
|
|
}
|
|
|
|
@Test("Missing end location fails")
|
|
func plan_MissingEndLocation_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago],
|
|
startLocation: startLoc,
|
|
endLocation: nil,
|
|
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 == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for missing end location")
|
|
}
|
|
}
|
|
|
|
@Test("Both locations missing fails")
|
|
func plan_BothLocationsMissing_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [:],
|
|
startLocation: nil,
|
|
endLocation: nil,
|
|
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 == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for missing locations")
|
|
}
|
|
}
|
|
|
|
@Test("Start location without coordinates fails")
|
|
func plan_StartLocationNoCoordinates_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let ny = nyStadium
|
|
|
|
let startLoc = LocationInput(name: "Chicago", coordinate: nil)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for start location without coordinates")
|
|
}
|
|
}
|
|
|
|
@Test("End location without coordinates fails")
|
|
func plan_EndLocationNoCoordinates_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(name: "New York", coordinate: nil)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for end location without coordinates")
|
|
}
|
|
}
|
|
|
|
// MARK: - Stadium City Tests
|
|
|
|
@Test("No stadiums in start city fails")
|
|
func plan_NoStadiumsInStartCity_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let ny = nyStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Boston", // No Boston stadium in our list
|
|
coordinate: CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .noGamesInRange)
|
|
#expect(failure.violations.first?.description.contains("start city") == true)
|
|
} else {
|
|
Issue.record("Expected failure for no stadiums in start city")
|
|
}
|
|
}
|
|
|
|
@Test("No stadiums in end city fails")
|
|
func plan_NoStadiumsInEndCity_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Boston", // No Boston stadium
|
|
coordinate: CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .noGamesInRange)
|
|
#expect(failure.violations.first?.description.contains("end city") == true)
|
|
} else {
|
|
Issue.record("Expected failure for no stadiums in end city")
|
|
}
|
|
}
|
|
|
|
@Test("City name matching is case insensitive")
|
|
func plan_CityNameMatchingCaseInsensitive() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let cleveland = clevelandStadium
|
|
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
|
|
// Use different case for city names
|
|
let startLoc = LocationInput(
|
|
name: "CHICAGO", // uppercase
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "new york", // lowercase
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, cleveland.id: cleveland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should still work despite case differences
|
|
if case .failure = result {
|
|
Issue.record("Should handle case-insensitive city names")
|
|
}
|
|
}
|
|
|
|
// MARK: - Directional Filtering Tests
|
|
|
|
@Test("Backtracking stadium is filtered out")
|
|
func plan_BacktrackingStadium_FilteredOut() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let minneapolis = minneapolisStadium // West of Chicago - wrong direction
|
|
let ny = nyStadium
|
|
|
|
// Game in Minneapolis - going wrong way from Chicago to NY
|
|
let game = makeGame(stadiumId: minneapolis.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, minneapolis.id: minneapolis, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should fail because Minneapolis is not directional (west of Chicago when going east to NY)
|
|
if case .failure = result {
|
|
// Expected - no valid directional routes
|
|
} else if case .success(let options) = result {
|
|
// If it succeeded, Minneapolis should not appear in stops
|
|
for option in options {
|
|
for stop in option.stops {
|
|
#expect(stop.city != "Minneapolis", "Minneapolis should be filtered out")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Directional stadiums included in route")
|
|
func plan_DirectionalStadiums_Included() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let pittsburgh = pittsburghStadium
|
|
let ny = nyStadium
|
|
|
|
// Cleveland and Pittsburgh are directional (east of Chicago, toward NY)
|
|
let game1 = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: pittsburgh.id, date: date("2026-06-12 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [
|
|
chicago.id: chicago,
|
|
cleveland.id: cleveland,
|
|
pittsburgh.id: pittsburgh,
|
|
ny.id: ny
|
|
],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
// At least one option should include Cleveland and/or Pittsburgh
|
|
let allCities = options.flatMap { $0.stops.map { $0.city } }
|
|
let hasDirectionalCities = allCities.contains("Cleveland") || allCities.contains("Pittsburgh")
|
|
#expect(hasDirectionalCities)
|
|
} else {
|
|
Issue.record("Expected success with directional stadiums")
|
|
}
|
|
}
|
|
|
|
// MARK: - Output Structure Tests
|
|
|
|
@Test("Max 5 options returned")
|
|
func plan_Max5OptionsReturned() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Create many games to potentially generate many options
|
|
var games: [Game] = []
|
|
for day in 1...15 {
|
|
games.append(makeGame(
|
|
stadiumId: la.id,
|
|
date: date("2026-06-\(String(format: "%02d", day)) 19:00")
|
|
))
|
|
}
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: games,
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(options.count <= 5, "Should return at most 5 options")
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Options ranked from 1")
|
|
func plan_OptionsRankedFromOne() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(options.first?.rank == 1)
|
|
for (index, option) in options.enumerated() {
|
|
#expect(option.rank == index + 1)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Options have valid structure")
|
|
func plan_OptionsHaveValidStructure() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.rank > 0)
|
|
#expect(!option.stops.isEmpty)
|
|
#expect(!option.geographicRationale.isEmpty)
|
|
#expect(option.totalDrivingHours >= 0)
|
|
#expect(option.totalDistanceMiles >= 0)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segments count equals stops count minus 1")
|
|
func plan_TravelSegmentsCountCorrect() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.travelSegments.count == option.stops.count - 1,
|
|
"Travel segments should be stops - 1")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segments are drive mode")
|
|
func plan_TravelSegmentsAreDriveMode() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(segment.travelMode == .drive)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Geographic Rationale Tests
|
|
|
|
@Test("Geographic rationale shows start and end cities")
|
|
func plan_GeographicRationale_ShowsStartAndEnd() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let rationale = options.first?.geographicRationale {
|
|
#expect(rationale.contains("San Diego"))
|
|
#expect(rationale.contains("San Francisco"))
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Geographic rationale shows game count")
|
|
func plan_GeographicRationale_ShowsGameCount() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-11 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let rationale = options.first?.geographicRationale {
|
|
#expect(rationale.contains("game") || rationale.contains("2"))
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Stop Tests
|
|
|
|
@Test("Stops include start city")
|
|
func plan_StopsIncludeStartCity() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let firstStop = options.first?.stops.first {
|
|
#expect(firstStop.city == "San Diego")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Stops include end city")
|
|
func plan_StopsIncludeEndCity() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let lastStop = options.first?.stops.last {
|
|
#expect(lastStop.city == "San Francisco")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Start stop has no games")
|
|
func plan_StartStopHasNoGames() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let firstStop = options.first?.stops.first {
|
|
#expect(firstStop.games.isEmpty, "Start stop should have no games")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("End stop has no games")
|
|
func plan_EndStopHasNoGames() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let lastStop = options.first?.stops.last {
|
|
#expect(lastStop.games.isEmpty, "End stop should have no games")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Game stops have games")
|
|
func plan_GameStopsHaveGames() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Middle stops (not first or last) should have games
|
|
if let stops = options.first?.stops, stops.count > 2 {
|
|
let middleStops = stops.dropFirst().dropLast()
|
|
for stop in middleStops {
|
|
#expect(!stop.games.isEmpty, "Middle stops should have games")
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Stop has correct city from stadium")
|
|
func plan_StopHasCorrectCity() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
let allCities = options.first?.stops.map { $0.city } ?? []
|
|
#expect(allCities.contains("Los Angeles"))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Ranking Tests
|
|
|
|
@Test("More games ranked higher")
|
|
func plan_MoreGamesRankedHigher() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Create games that allow different route options
|
|
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-11 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if options.count >= 2 {
|
|
let firstGames = options[0].stops.flatMap { $0.games }.count
|
|
let secondGames = options[1].stops.flatMap { $0.games }.count
|
|
#expect(firstGames >= secondGames, "More games should be ranked first")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Less driving hours ranked higher for equal games")
|
|
func plan_LessDrivingRankedHigher() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if options.count >= 2 {
|
|
let firstGames = options[0].stops.flatMap { $0.games }.count
|
|
let secondGames = options[1].stops.flatMap { $0.games }.count
|
|
if firstGames == secondGames {
|
|
#expect(options[0].totalDrivingHours <= options[1].totalDrivingHours)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Date Range Tests
|
|
|
|
@Test("Explicit date range used when provided")
|
|
func plan_ExplicitDateRangeUsed() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Games outside and inside date range
|
|
let outsideGame = makeGame(stadiumId: la.id, date: date("2026-05-10 19:00"))
|
|
let insideGame = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [outsideGame, insideGame],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Only inside game should be in results
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(allGameIds.contains(insideGame.id))
|
|
#expect(!allGameIds.contains(outsideGame.id))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("No games in date range fails")
|
|
func plan_NoGamesInDateRange_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let cleveland = clevelandStadium
|
|
|
|
// Game outside date range
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-05-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, cleveland.id: cleveland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure = result {
|
|
// Expected - no games within date range
|
|
} else {
|
|
Issue.record("Expected failure for no games in date range")
|
|
}
|
|
}
|
|
|
|
// MARK: - Driving Constraints Tests
|
|
|
|
@Test("Two drivers allows longer routes")
|
|
func plan_TwoDrivers_AllowsLongerRoutes() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let cleveland = clevelandStadium
|
|
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
// With 2 drivers, longer routes should be feasible
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, cleveland.id: cleveland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59"),
|
|
numberOfDrivers: 2
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should succeed with 2 drivers
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test("Trip duration generates date ranges from games")
|
|
func plan_TripDurationGeneratesDateRanges() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Games at start (SD) and end (SF) cities to generate date ranges
|
|
let startCityGame = makeGame(stadiumId: sd.id, date: date("2026-06-05 19:00"))
|
|
let middleGame = makeGame(stadiumId: la.id, date: date("2026-06-07 19:00"))
|
|
let endCityGame = makeGame(stadiumId: sf.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
// Use trip duration instead of explicit dates
|
|
let request = makeRequest(
|
|
games: [startCityGame, middleGame, endCityGame],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59"),
|
|
tripDuration: 7
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should succeed with trip duration mode
|
|
switch result {
|
|
case .success, .failure:
|
|
// Just verify it handles trip duration without crashing
|
|
break
|
|
}
|
|
}
|
|
|
|
// MARK: - Failure Tests
|
|
|
|
@Test("Failure contains violation details")
|
|
func plan_Failure_ContainsViolationDetails() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [:],
|
|
startLocation: nil,
|
|
endLocation: nil,
|
|
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)
|
|
} else {
|
|
Issue.record("Expected failure")
|
|
}
|
|
}
|
|
|
|
@Test("No valid directional routes fails")
|
|
func plan_NoValidDirectionalRoutes_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let minneapolis = minneapolisStadium
|
|
|
|
// Only game is in wrong direction (Minneapolis is west of Chicago)
|
|
let game = makeGame(stadiumId: minneapolis.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, minneapolis.id: minneapolis],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should fail because Minneapolis is not directional
|
|
if case .failure = result {
|
|
// Expected
|
|
} else {
|
|
Issue.record("Expected failure for non-directional route")
|
|
}
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
@Test("Same city for start and end")
|
|
func plan_SameCityStartAndEnd() {
|
|
let planner = ScenarioCPlanner()
|
|
let la = laStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [la.id: la],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// May succeed or fail depending on implementation
|
|
// Just verify it doesn't crash
|
|
switch result {
|
|
case .success, .failure:
|
|
break
|
|
}
|
|
}
|
|
|
|
@Test("Very short date range with game")
|
|
func plan_ShortDateRangeWithGame() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
// Just one day
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-10 00:00"),
|
|
endDate: date("2026-06-10 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test("Multiple games at same stadium")
|
|
func plan_MultipleGamesAtSameStadium() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-11 19:00"))
|
|
let game3 = makeGame(stadiumId: la.id, date: date("2026-06-12 13:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2, game3],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Games at same stadium should be grouped
|
|
for option in options {
|
|
let laStops = option.stops.filter { $0.city == "Los Angeles" }
|
|
#expect(laStops.count <= 1, "Same stadium games should be grouped")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Empty games array")
|
|
func plan_EmptyGamesArray() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should fail - no games
|
|
if case .failure = result {
|
|
// Expected
|
|
} else {
|
|
Issue.record("Expected failure for empty games")
|
|
}
|
|
}
|
|
|
|
@Test("Handles long cross-country route")
|
|
func plan_LongCrossCountryRoute() {
|
|
let planner = ScenarioCPlanner()
|
|
let sf = sfStadium
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let ny = nyStadium
|
|
|
|
// SF to NY is a long route
|
|
let game1 = makeGame(stadiumId: chicago.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: cleveland.id, date: date("2026-06-15 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [sf.id: sf, chicago.id: chicago, cleveland.id: cleveland, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59"),
|
|
numberOfDrivers: 2 // More drivers for longer route
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should handle without crashing
|
|
switch result {
|
|
case .success, .failure:
|
|
break
|
|
}
|
|
}
|
|
|
|
@Test("Stop coordinates match stadium")
|
|
func plan_StopCoordinatesMatchStadium() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Find LA stop
|
|
if let laStop = options.first?.stops.first(where: { $0.city == "Los Angeles" }) {
|
|
if let coord = laStop.coordinate {
|
|
#expect(abs(coord.latitude - la.latitude) < 0.01)
|
|
#expect(abs(coord.longitude - la.longitude) < 0.01)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Total distance is positive")
|
|
func plan_TotalDistanceIsPositive() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.totalDistanceMiles > 0)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Total driving hours is positive")
|
|
func plan_TotalDrivingHoursIsPositive() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.totalDrivingHours > 0)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segment has valid origin and destination")
|
|
func plan_TravelSegmentHasValidOriginDestination() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(!segment.fromLocation.name.isEmpty)
|
|
#expect(!segment.toLocation.name.isEmpty)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segment has positive distance")
|
|
func plan_TravelSegmentHasPositiveDistance() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(segment.distanceMeters > 0)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segment has positive duration")
|
|
func plan_TravelSegmentHasPositiveDuration() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(segment.durationSeconds > 0)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// Note: "Departure time before arrival time" test removed
|
|
// Travel segments are now location-based, not time-based
|
|
// The user decides when to travel; segments only describe route info
|
|
|
|
@Test("Games filtered to date range")
|
|
func plan_GamesFilteredToDateRange() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Game outside range
|
|
let earlyGame = makeGame(stadiumId: la.id, date: date("2026-05-01 19:00"))
|
|
// Game inside range
|
|
let validGame = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
// Game outside range
|
|
let lateGame = makeGame(stadiumId: la.id, date: date("2026-07-01 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [earlyGame, validGame, lateGame],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(allGameIds.contains(validGame.id))
|
|
#expect(!allGameIds.contains(earlyGame.id))
|
|
#expect(!allGameIds.contains(lateGame.id))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Many games handles efficiently")
|
|
func plan_ManyGames_HandlesEfficiently() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Create many games
|
|
var games: [Game] = []
|
|
for day in 1...30 {
|
|
games.append(makeGame(
|
|
stadiumId: la.id,
|
|
date: date("2026-06-\(String(format: "%02d", day)) 19:00")
|
|
))
|
|
}
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: games,
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should complete without timeout
|
|
switch result {
|
|
case .success, .failure:
|
|
break
|
|
}
|
|
}
|
|
|
|
@Test("Route progresses toward end")
|
|
func plan_RouteProgressesTowardEnd() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let pittsburgh = pittsburghStadium
|
|
let ny = nyStadium
|
|
|
|
let game1 = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: pittsburgh.id, date: date("2026-06-12 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [
|
|
chicago.id: chicago,
|
|
cleveland.id: cleveland,
|
|
pittsburgh.id: pittsburgh,
|
|
ny.id: ny
|
|
],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Verify each stop gets closer to (or doesn't significantly move away from) NY
|
|
for option in options {
|
|
var lastDistance = Double.infinity
|
|
for stop in option.stops {
|
|
if let coord = stop.coordinate {
|
|
let distance = sqrt(pow(coord.latitude - ny.latitude, 2) +
|
|
pow(coord.longitude - ny.longitude, 2))
|
|
// Allow small tolerance for backtracking
|
|
#expect(distance <= lastDistance * 1.2,
|
|
"Route should generally progress toward end")
|
|
lastDistance = distance
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Stop location has name")
|
|
func plan_StopLocationHasName() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for stop in option.stops {
|
|
#expect(!stop.location.name.isEmpty)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Games at directional stadium included")
|
|
func plan_GamesAtDirectionalStadium_Included() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// LA is between SD and SF (directional)
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(allGameIds.contains(game.id))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Feature 1: Travel Corridor Game Inclusion Tests
|
|
|
|
@Test("Direct route with games along path includes all corridor games")
|
|
func corridor_DirectRouteWithGamesAlongPath_IncludesAllGames() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// Create stadiums: LA → San Jose → SF (direct north on I-5/101)
|
|
let la = makeStadium(name: "Dodger Stadium", city: "Los Angeles", state: "CA",
|
|
latitude: 34.0739, longitude: -118.2400)
|
|
let sj = makeStadium(name: "SAP Center", city: "San Jose", state: "CA",
|
|
latitude: 37.3326, longitude: -121.9010) // Midpoint between LA and SF
|
|
let sf = sfStadium
|
|
|
|
// Games along the path
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let sjGame = makeGame(stadiumId: sj.id, date: date("2026-06-06 19:00"))
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, sjGame, sfGame],
|
|
stadiums: [la.id: la, sj.id: sj, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty, "Should return at least one route")
|
|
|
|
// Best option should include all 3 games (LA → SJ → SF)
|
|
let topOption = options.first!
|
|
let gameIds = topOption.stops.flatMap { $0.games }
|
|
|
|
#expect(gameIds.contains(laGame.id), "Should include LA game")
|
|
#expect(gameIds.contains(sjGame.id), "Should include San Jose game (midpoint)")
|
|
#expect(gameIds.contains(sfGame.id), "Should include SF game")
|
|
} else {
|
|
Issue.record("Expected success with games along direct corridor")
|
|
}
|
|
}
|
|
|
|
@Test("Game slightly off corridor within tolerance included")
|
|
func corridor_GameSlightlyOffCorridor_IncludedWithinTolerance() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → SF is north on I-5
|
|
// Sacramento is ~20 miles east of I-5, should be within corridor tolerance
|
|
let la = laStadium
|
|
let sacramento = makeStadium(name: "Golden 1 Center", city: "Sacramento", state: "CA",
|
|
latitude: 38.5802, longitude: -121.4996) // Slightly east of direct path
|
|
let sf = sfStadium
|
|
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let sacGame = makeGame(stadiumId: sacramento.id, date: date("2026-06-06 19:00"))
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, sacGame, sfGame],
|
|
stadiums: [la.id: la, sacramento.id: sacramento, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// Sacramento should be included (within corridor tolerance)
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(allGameIds.contains(sacGame.id), "Sacramento should be included (within ~50mi corridor tolerance)")
|
|
} else {
|
|
Issue.record("Expected success with slightly off-corridor game")
|
|
}
|
|
}
|
|
|
|
@Test("Game far from corridor excluded")
|
|
func corridor_GameFarFromCorridor_Excluded() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → SF is north-bound
|
|
// Phoenix is ~300 miles east, far from corridor
|
|
let la = laStadium
|
|
let phoenix = makeStadium(name: "Chase Field", city: "Phoenix", state: "AZ",
|
|
latitude: 33.4452, longitude: -112.0667) // Far east of LA→SF path
|
|
let sf = sfStadium
|
|
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-06-06 19:00"))
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, phoenixGame, sfGame],
|
|
stadiums: [la.id: la, phoenix.id: phoenix, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// Phoenix should be excluded (too far from LA→SF corridor)
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(!allGameIds.contains(phoenixGame.id), "Phoenix should be excluded (300mi east, far from corridor)")
|
|
} else {
|
|
Issue.record("Expected success but with Phoenix excluded")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func corridor_MultipleGamesMixed_FiltersCorrectly() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → Portland route
|
|
let la = laStadium
|
|
let sd = sdStadium // South of LA - wrong direction
|
|
let sf = sfStadium // On path north
|
|
let seattle = makeStadium(name: "T-Mobile Park", city: "Seattle", state: "WA",
|
|
latitude: 47.5914, longitude: -122.3325) // Beyond Portland
|
|
let portland = makeStadium(name: "Providence Park", city: "Portland", state: "OR",
|
|
latitude: 45.5212, longitude: -122.6917)
|
|
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let sdGame = makeGame(stadiumId: sd.id, date: date("2026-06-06 19:00")) // Should exclude (south)
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00")) // Should include (on path)
|
|
let seattleGame = makeGame(stadiumId: seattle.id, date: date("2026-06-08 19:00")) // Should exclude (beyond end)
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Portland",
|
|
coordinate: CLLocationCoordinate2D(latitude: portland.latitude, longitude: portland.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, sdGame, sfGame, seattleGame],
|
|
stadiums: [la.id: la, sd.id: sd, sf.id: sf, seattle.id: seattle, portland.id: portland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
|
|
// Should include games on corridor
|
|
#expect(allGameIds.contains(laGame.id), "LA should be included (start)")
|
|
#expect(allGameIds.contains(sfGame.id), "SF should be included (on path north)")
|
|
|
|
// Should exclude off-corridor games
|
|
#expect(!allGameIds.contains(sdGame.id), "San Diego should be excluded (south, wrong direction)")
|
|
#expect(!allGameIds.contains(seattleGame.id), "Seattle should be excluded (beyond end point)")
|
|
} else {
|
|
Issue.record("Expected success with selective corridor filtering")
|
|
}
|
|
}
|
|
|
|
@Test("No games along corridor returns empty route or failure")
|
|
func corridor_NoGamesAlongCorridor_ReturnsEmptyOrFailure() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → Seattle route (I-5 corridor)
|
|
let la = laStadium
|
|
let seattle = makeStadium(name: "T-Mobile Park", city: "Seattle", state: "WA",
|
|
latitude: 47.5914, longitude: -122.3325)
|
|
|
|
// Games far from corridor
|
|
let phoenix = makeStadium(name: "Chase Field", city: "Phoenix", state: "AZ",
|
|
latitude: 33.4452, longitude: -112.0667) // East
|
|
let denver = makeStadium(name: "Coors Field", city: "Denver", state: "CO",
|
|
latitude: 39.7559, longitude: -104.9942) // Far east
|
|
|
|
let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-06-05 19:00"))
|
|
let denverGame = makeGame(stadiumId: denver.id, date: date("2026-06-06 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Seattle",
|
|
coordinate: CLLocationCoordinate2D(latitude: seattle.latitude, longitude: seattle.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [phoenixGame, denverGame],
|
|
stadiums: [la.id: la, seattle.id: seattle, phoenix.id: phoenix, denver.id: denver],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should either fail (.noGamesInRange or .noValidRoutes) OR return route with no games
|
|
switch result {
|
|
case .failure(let failure):
|
|
// Expected: no games in corridor
|
|
#expect(failure.reason == .noValidRoutes || failure.reason == .noGamesInRange,
|
|
"Should fail with noValidRoutes or noGamesInRange")
|
|
case .success(let options):
|
|
// If success, route should have only start/end waypoints, no games
|
|
if let topOption = options.first {
|
|
let gameCount = topOption.stops.flatMap { $0.games }.count
|
|
#expect(gameCount == 0, "Route should have no games if all games are off-corridor")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Feature 2: Geographic Efficiency Validation (Anti-Backtracking) Tests
|
|
|
|
@Test("Route must start at specified start city")
|
|
func antiBacktrack_RouteStartsAtSpecifiedCity() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// SF → LA route (south-bound)
|
|
let sf = sfStadium
|
|
let la = laStadium
|
|
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00"))
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) // Earlier date
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, sfGame],
|
|
stadiums: [la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// The route should visit SF before LA (not LA first just because it's earlier)
|
|
// First stop with games should be SF
|
|
if let topOption = options.first {
|
|
let stopsWithGames = topOption.stops.filter { !$0.games.isEmpty }
|
|
if let firstGameStop = stopsWithGames.first {
|
|
#expect(firstGameStop.city == "San Francisco", "Route should start at SF (specified start city)")
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success with route starting at SF")
|
|
}
|
|
}
|
|
|
|
@Test("Route must end at specified end city")
|
|
func antiBacktrack_RouteEndsAtSpecifiedCity() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → Seattle route
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
let seattle = makeStadium(name: "T-Mobile Park", city: "Seattle", state: "WA",
|
|
latitude: 47.5914, longitude: -122.3325)
|
|
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-06 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Seattle",
|
|
coordinate: CLLocationCoordinate2D(latitude: seattle.latitude, longitude: seattle.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, sfGame],
|
|
stadiums: [la.id: la, sf.id: sf, seattle.id: seattle],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// Route should end at Seattle waypoint (after all games)
|
|
if let topOption = options.first {
|
|
// The last stop should be Seattle (or close to it)
|
|
let lastStop = topOption.stops.last
|
|
#expect(lastStop != nil, "Route should have stops")
|
|
|
|
// The route should include Seattle as the end destination
|
|
// Either as a waypoint or the last stop should be very close to Seattle
|
|
if let lastStop = lastStop, let lastCoord = lastStop.coordinate {
|
|
let lastLocation = CLLocation(latitude: lastCoord.latitude, longitude: lastCoord.longitude)
|
|
let seattleLocation = CLLocation(latitude: seattle.latitude, longitude: seattle.longitude)
|
|
let distance = lastLocation.distance(from: seattleLocation)
|
|
|
|
// Should either be Seattle itself or within 80km (for waypoint tolerance)
|
|
#expect(distance < 80000, "Route should end at or near Seattle (end city)")
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success with route ending at Seattle")
|
|
}
|
|
}
|
|
|
|
@Test("Intermediate games in wrong order rejected or reordered")
|
|
func antiBacktrack_WrongOrderGamesHandled() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → Portland route
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
let portland = makeStadium(name: "Providence Park", city: "Portland", state: "OR",
|
|
latitude: 45.5212, longitude: -122.6917)
|
|
|
|
// Games in suboptimal order: SF first, then LA (backtrack south), then Portland
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-05 19:00"))
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-06 19:00")) // Would require backtrack
|
|
let portlandGame = makeGame(stadiumId: portland.id, date: date("2026-06-07 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Portland",
|
|
coordinate: CLLocationCoordinate2D(latitude: portland.latitude, longitude: portland.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [sfGame, laGame, portlandGame],
|
|
stadiums: [la.id: la, sf.id: sf, portland.id: portland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// The route should either:
|
|
// A) Exclude the LA game on June 6 (since it requires backtracking south after SF)
|
|
// B) Reorder to LA → SF → Portland (optimal order)
|
|
|
|
if let topOption = options.first {
|
|
let gameIds = topOption.stops.flatMap { $0.games }
|
|
|
|
// If it includes all 3 games, check the order is sensible (LA before SF)
|
|
if gameIds.count == 3 {
|
|
let stopsWithGames = topOption.stops.filter { !$0.games.isEmpty }
|
|
if stopsWithGames.count >= 2 {
|
|
let firstCity = stopsWithGames[0].city
|
|
let secondCity = stopsWithGames[1].city
|
|
|
|
// LA should come before SF
|
|
#expect(
|
|
(firstCity == "Los Angeles" && secondCity == "San Francisco"),
|
|
"If all games included, LA should come before SF (no backtracking)"
|
|
)
|
|
}
|
|
}
|
|
// Otherwise, it's acceptable to exclude the backtracking game
|
|
}
|
|
} else {
|
|
Issue.record("Expected success with sensible ordering or game exclusion")
|
|
}
|
|
}
|
|
|
|
@Test("Multiple route options - least backtracking preferred")
|
|
func antiBacktrack_LeastBacktrackingPreferred() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → SF route
|
|
let la = laStadium
|
|
let sd = sdStadium // South of LA - major backtrack
|
|
let sj = makeStadium(name: "SAP Center", city: "San Jose", state: "CA",
|
|
latitude: 37.3387, longitude: -121.8853)
|
|
let sf = sfStadium
|
|
|
|
// Two potential routes:
|
|
// Option A: LA → SD (south backtrack) → SF (requires going way south first)
|
|
// Option B: LA → SJ → SF (direct north)
|
|
|
|
let laGame1 = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let sdGame = makeGame(stadiumId: sd.id, date: date("2026-06-06 19:00"))
|
|
let laGame2 = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) // Duplicate for Option B
|
|
let sjGame = makeGame(stadiumId: sj.id, date: date("2026-06-06 19:00"))
|
|
let sfGame1 = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00"))
|
|
let sfGame2 = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame1, sdGame, sjGame, sfGame1],
|
|
stadiums: [la.id: la, sd.id: sd, sj.id: sj, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// Top option should prefer LA → SJ → SF (direct) over LA → SD → SF (backtrack)
|
|
if let topOption = options.first {
|
|
let gameIds = topOption.stops.flatMap { $0.games }
|
|
|
|
// Should prefer SJ over SD (less backtracking)
|
|
if gameIds.contains(sjGame.id) && gameIds.contains(sdGame.id) {
|
|
Issue.record("Should not include both SJ and SD - prefer less backtracking")
|
|
}
|
|
|
|
// Better: should include SJ and exclude SD
|
|
#expect(gameIds.contains(sjGame.id) || !gameIds.contains(sdGame.id),
|
|
"Should prefer San Jose (direct north) over San Diego (backtrack south)")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success with least-backtracking route preferred")
|
|
}
|
|
}
|
|
|
|
@Test("Minor backtracking within tolerance is acceptable")
|
|
func antiBacktrack_MinorBacktrackingAcceptable() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → SF route with minor backtrack to Anaheim
|
|
let la = laStadium
|
|
let anaheim = makeStadium(name: "Honda Center", city: "Anaheim", state: "CA",
|
|
latitude: 33.8078, longitude: -117.8764) // ~30mi south of LA
|
|
let sj = makeStadium(name: "SAP Center", city: "San Jose", state: "CA",
|
|
latitude: 37.3387, longitude: -121.8853)
|
|
let sf = sfStadium
|
|
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let anaheimGame = makeGame(stadiumId: anaheim.id, date: date("2026-06-06 19:00"))
|
|
let sjGame = makeGame(stadiumId: sj.id, date: date("2026-06-07 19:00"))
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-08 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, anaheimGame, sjGame, sfGame],
|
|
stadiums: [la.id: la, anaheim.id: anaheim, sj.id: sj, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// Anaheim is only ~30 miles south, which should be acceptable as a minor detour
|
|
// The route should include Anaheim if time permits
|
|
if let topOption = options.first {
|
|
let gameIds = topOption.stops.flatMap { $0.games }
|
|
|
|
// Anaheim could be included (acceptable minor backtrack)
|
|
// This test just verifies we don't fail completely
|
|
#expect(true, "Route planning should succeed with minor backtracking scenario")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success - minor backtracking should be acceptable")
|
|
}
|
|
}
|
|
|
|
@Test("Excessive backtracking beyond destination rejected")
|
|
func antiBacktrack_ExcessiveBacktrackingRejected() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// LA → Seattle route (north-bound)
|
|
let la = laStadium
|
|
let sd = sdStadium // 120 miles south - excessive backtrack
|
|
let sf = sfStadium
|
|
let seattle = makeStadium(name: "T-Mobile Park", city: "Seattle", state: "WA",
|
|
latitude: 47.5914, longitude: -122.3325)
|
|
|
|
let laGame = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00"))
|
|
let sdGame = makeGame(stadiumId: sd.id, date: date("2026-06-06 19:00")) // Excessive backtrack
|
|
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-06-07 19:00"))
|
|
let seattleGame = makeGame(stadiumId: seattle.id, date: date("2026-06-08 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Seattle",
|
|
coordinate: CLLocationCoordinate2D(latitude: seattle.latitude, longitude: seattle.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [laGame, sdGame, sfGame, seattleGame],
|
|
stadiums: [la.id: la, sd.id: sd, sf.id: sf, seattle.id: seattle],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Either exclude San Diego (success without it) or return failure
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// San Diego should be excluded (excessive backtrack going north)
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(!allGameIds.contains(sdGame.id),
|
|
"San Diego should be excluded (120mi south, excessive backtrack on north-bound route)")
|
|
}
|
|
// Alternatively, could fail with noValidRoutes if San Diego is required
|
|
}
|
|
|
|
@Test("Correct directional classification for north-to-south route")
|
|
func antiBacktrack_NorthToSouthDirectionalCorrect() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
// Boston → Miami route (north to south)
|
|
let boston = makeStadium(name: "TD Garden", city: "Boston", state: "MA",
|
|
latitude: 42.3662, longitude: -71.0621)
|
|
let nyc = makeStadium(name: "Madison Square Garden", city: "New York", state: "NY",
|
|
latitude: 40.7505, longitude: -73.9934)
|
|
let dc = makeStadium(name: "Capital One Arena", city: "Washington", state: "DC",
|
|
latitude: 38.8981, longitude: -77.0209)
|
|
let miami = makeStadium(name: "FTX Arena", city: "Miami", state: "FL",
|
|
latitude: 25.7814, longitude: -80.1870)
|
|
|
|
let bostonGame = makeGame(stadiumId: boston.id, date: date("2026-06-05 19:00"))
|
|
let nycGame = makeGame(stadiumId: nyc.id, date: date("2026-06-06 19:00"))
|
|
let dcGame = makeGame(stadiumId: dc.id, date: date("2026-06-07 19:00"))
|
|
let miamiGame = makeGame(stadiumId: miami.id, date: date("2026-06-08 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Boston",
|
|
coordinate: CLLocationCoordinate2D(latitude: boston.latitude, longitude: boston.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Miami",
|
|
coordinate: CLLocationCoordinate2D(latitude: miami.latitude, longitude: miami.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [bostonGame, nycGame, dcGame, miamiGame],
|
|
stadiums: [boston.id: boston, nyc.id: nyc, dc.id: dc, miami.id: miami],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
|
|
// Route should follow north→south progression
|
|
if let topOption = options.first {
|
|
let stopsWithGames = topOption.stops.filter { !$0.games.isEmpty }
|
|
|
|
// Verify stops are in geographic north→south order
|
|
if stopsWithGames.count >= 2 {
|
|
for i in 0..<(stopsWithGames.count - 1) {
|
|
guard let currentCoord = stopsWithGames[i].coordinate,
|
|
let nextCoord = stopsWithGames[i + 1].coordinate else {
|
|
continue
|
|
}
|
|
|
|
let currentLat = currentCoord.latitude
|
|
let nextLat = nextCoord.latitude
|
|
|
|
// Each subsequent stop should be equal or farther south (lower latitude)
|
|
#expect(nextLat <= currentLat + 1.0, // Allow 1° tolerance for slight variations
|
|
"Route should progress south (Boston→NYC→DC→Miami)")
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success with north→south directional route")
|
|
}
|
|
}
|
|
}
|