297 lines
10 KiB
Swift
297 lines
10 KiB
Swift
//
|
|
// ScenarioPlannerFactoryTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification tests for ScenarioPlannerFactory.
|
|
//
|
|
|
|
import Testing
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("ScenarioPlannerFactory")
|
|
@MainActor
|
|
struct ScenarioPlannerFactoryTests {
|
|
|
|
// MARK: - Specification Tests: planner(for:)
|
|
|
|
@Test("planner: followTeamId set returns ScenarioDPlanner")
|
|
func planner_followTeamId_returnsScenarioD() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .followTeam,
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1,
|
|
followTeamId: "team-123"
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs)
|
|
let planner = ScenarioPlannerFactory.planner(for: request)
|
|
|
|
#expect(planner is ScenarioDPlanner)
|
|
}
|
|
|
|
@Test("planner: selectedGames not empty returns ScenarioBPlanner")
|
|
func planner_selectedGames_returnsScenarioB() {
|
|
let game = makeGame()
|
|
let prefs = TripPreferences(
|
|
planningMode: .gameFirst,
|
|
sports: [.mlb],
|
|
mustSeeGameIds: [game.id],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs, games: [game])
|
|
let planner = ScenarioPlannerFactory.planner(for: request)
|
|
|
|
#expect(planner is ScenarioBPlanner)
|
|
}
|
|
|
|
@Test("planner: start and end locations returns ScenarioCPlanner")
|
|
func planner_startEndLocations_returnsScenarioC() {
|
|
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .locations,
|
|
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
|
endLocation: LocationInput(name: "LA", coordinate: laCoord),
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs)
|
|
let planner = ScenarioPlannerFactory.planner(for: request)
|
|
|
|
#expect(planner is ScenarioCPlanner)
|
|
}
|
|
|
|
@Test("planner: date range only returns ScenarioAPlanner")
|
|
func planner_dateRangeOnly_returnsScenarioA() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .dateRange,
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs)
|
|
let planner = ScenarioPlannerFactory.planner(for: request)
|
|
|
|
#expect(planner is ScenarioAPlanner)
|
|
}
|
|
|
|
@Test("planner: priority is D > B > C > A")
|
|
func planner_priority_DoverBoverCoverA() {
|
|
// If all conditions are met, followTeamId wins
|
|
let game = makeGame()
|
|
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .followTeam,
|
|
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
|
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
|
|
sports: [.mlb],
|
|
mustSeeGameIds: [game.id], // B condition
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1,
|
|
followTeamId: "team-123" // D condition
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs, games: [game])
|
|
let planner = ScenarioPlannerFactory.planner(for: request)
|
|
|
|
#expect(planner is ScenarioDPlanner, "D should take priority")
|
|
}
|
|
|
|
@Test("planner: B takes priority over C")
|
|
func planner_priority_BoverC() {
|
|
// If B and C conditions met but not D, B wins
|
|
let game = makeGame()
|
|
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .gameFirst,
|
|
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
|
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
|
|
sports: [.mlb],
|
|
mustSeeGameIds: [game.id], // B condition
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
// followTeamId: nil by default - Not D
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs, games: [game])
|
|
let planner = ScenarioPlannerFactory.planner(for: request)
|
|
|
|
#expect(planner is ScenarioBPlanner, "B should take priority over C")
|
|
}
|
|
|
|
// MARK: - Specification Tests: classify()
|
|
|
|
@Test("classify: followTeamId returns scenarioD")
|
|
func classify_followTeamId_returnsScenarioD() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .followTeam,
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1,
|
|
followTeamId: "team-123"
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs)
|
|
let scenario = ScenarioPlannerFactory.classify(request)
|
|
|
|
#expect(scenario == .scenarioD)
|
|
}
|
|
|
|
@Test("classify: selectedGames returns scenarioB")
|
|
func classify_selectedGames_returnsScenarioB() {
|
|
let game = makeGame()
|
|
let prefs = TripPreferences(
|
|
planningMode: .gameFirst,
|
|
sports: [.mlb],
|
|
mustSeeGameIds: [game.id],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs, games: [game])
|
|
let scenario = ScenarioPlannerFactory.classify(request)
|
|
|
|
#expect(scenario == .scenarioB)
|
|
}
|
|
|
|
@Test("classify: startEndLocations returns scenarioC")
|
|
func classify_startEndLocations_returnsScenarioC() {
|
|
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .locations,
|
|
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
|
endLocation: LocationInput(name: "LA", coordinate: laCoord),
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs)
|
|
let scenario = ScenarioPlannerFactory.classify(request)
|
|
|
|
#expect(scenario == .scenarioC)
|
|
}
|
|
|
|
@Test("classify: dateRangeOnly returns scenarioA")
|
|
func classify_dateRangeOnly_returnsScenarioA() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .dateRange,
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
)
|
|
|
|
let request = makeRequest(preferences: prefs)
|
|
let scenario = ScenarioPlannerFactory.classify(request)
|
|
|
|
#expect(scenario == .scenarioA)
|
|
}
|
|
|
|
// MARK: - Property Tests
|
|
|
|
@Test("Property: planner and classify are consistent")
|
|
func property_plannerAndClassifyConsistent() {
|
|
// Scenario A
|
|
let prefsA = TripPreferences(
|
|
planningMode: .dateRange,
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1
|
|
)
|
|
let requestA = makeRequest(preferences: prefsA)
|
|
let plannerA = ScenarioPlannerFactory.planner(for: requestA)
|
|
let classifyA = ScenarioPlannerFactory.classify(requestA)
|
|
#expect(plannerA is ScenarioAPlanner && classifyA == .scenarioA)
|
|
|
|
// Scenario D
|
|
let prefsD = TripPreferences(
|
|
planningMode: .followTeam,
|
|
sports: [.mlb],
|
|
startDate: TestClock.now,
|
|
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
|
leisureLevel: .moderate,
|
|
lodgingType: .hotel,
|
|
numberOfDrivers: 1,
|
|
followTeamId: "team-123"
|
|
)
|
|
let requestD = makeRequest(preferences: prefsD)
|
|
let plannerD = ScenarioPlannerFactory.planner(for: requestD)
|
|
let classifyD = ScenarioPlannerFactory.classify(requestD)
|
|
#expect(plannerD is ScenarioDPlanner && classifyD == .scenarioD)
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func makeRequest(
|
|
preferences: TripPreferences,
|
|
games: [Game] = []
|
|
) -> PlanningRequest {
|
|
PlanningRequest(
|
|
preferences: preferences,
|
|
availableGames: games,
|
|
teams: [:],
|
|
stadiums: [:]
|
|
)
|
|
}
|
|
|
|
private func makeGame() -> Game {
|
|
Game(
|
|
id: "game-\(UUID().uuidString)",
|
|
homeTeamId: "team1",
|
|
awayTeamId: "team2",
|
|
stadiumId: "stadium1",
|
|
dateTime: TestClock.now,
|
|
sport: .mlb,
|
|
season: "2026",
|
|
isPlayoff: false
|
|
)
|
|
}
|
|
}
|