// // 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") } @Test("planner: teamFirst with 2+ teams returns ScenarioEPlanner") func planner_teamFirst_returnsScenarioE() { let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: TestClock.now, endDate: TestClock.now.addingTimeInterval(86400 * 7), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 1, selectedTeamIds: ["team-1", "team-2"] ) let request = makeRequest(preferences: prefs) let planner = ScenarioPlannerFactory.planner(for: request) #expect(planner is ScenarioEPlanner) } @Test("classify: teamFirst with 2+ teams returns scenarioE") func classify_teamFirst_returnsScenarioE() { let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: TestClock.now, endDate: TestClock.now.addingTimeInterval(86400 * 7), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 1, selectedTeamIds: ["team-1", "team-2"] ) let request = makeRequest(preferences: prefs) let scenario = ScenarioPlannerFactory.classify(request) #expect(scenario == .scenarioE) } // 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 ) } }