Files
Sportstime/SportsTime/Planning/Engine/TripPlanningEngine.swift
Trey t 8162b4a029 refactor(tests): TDD rewrite of all unit tests with spec documentation
Complete rewrite of unit test suite using TDD methodology:

Planning Engine Tests:
- GameDAGRouterTests: Beam search, anchor games, transitions
- ItineraryBuilderTests: Stop connection, validators, EV enrichment
- RouteFiltersTests: Region, time window, scoring filters
- ScenarioA/B/C/D PlannerTests: All planning scenarios
- TravelEstimatorTests: Distance, duration, travel days
- TripPlanningEngineTests: Orchestration, caching, preferences

Domain Model Tests:
- AchievementDefinitionsTests, AnySportTests, DivisionTests
- GameTests, ProgressTests, RegionTests, StadiumTests
- TeamTests, TravelSegmentTests, TripTests, TripPollTests
- TripPreferencesTests, TripStopTests, SportTests

Service Tests:
- FreeScoreAPITests, RouteDescriptionGeneratorTests
- SuggestedTripsGeneratorTests

Export Tests:
- ShareableContentTests (card types, themes, dimensions)

Bug fixes discovered through TDD:
- ShareCardDimensions: mapSnapshotSize exceeded available width (960x480)
- ScenarioBPlanner: Added anchor game validation filter

All tests include:
- Specification tests (expected behavior)
- Invariant tests (properties that must always hold)
- Edge case tests (boundary conditions)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:07:41 -06:00

76 lines
2.7 KiB
Swift

//
// TripPlanningEngine.swift
// SportsTime
//
// Thin orchestrator that delegates to scenario-specific planners.
//
import Foundation
/// Main entry point for trip planning.
/// Delegates to scenario-specific planners via the ScenarioPlanner protocol.
///
/// - Expected Behavior:
/// - Uses ScenarioPlannerFactory.planner(for:) to select the right planner
/// - Delegates entirely to the selected scenario planner
/// - Applies repeat city filter to successful results
/// - If all options violate repeat city constraint .failure with .repeatCityViolation
/// - Passes through failures from scenario planners unchanged
///
/// - Invariants:
/// - Never modifies the logic of scenario planners
/// - Always returns a result (success or failure), never throws
/// - Repeat city filter only applied when allowRepeatCities is false
///
final class TripPlanningEngine {
/// Plans itineraries based on the request inputs.
/// Automatically detects which scenario applies and delegates to the appropriate planner.
///
/// - Parameter request: The planning request containing all inputs
/// - Returns: Ranked itineraries on success, or explicit failure with reason
func planItineraries(request: PlanningRequest) -> ItineraryResult {
// Detect scenario and get the appropriate planner
let planner = ScenarioPlannerFactory.planner(for: request)
// Delegate to the scenario planner
let result = planner.plan(request: request)
// Apply preference filters to successful results
return applyPreferenceFilters(to: result, request: request)
}
// MARK: - Private
/// Applies allowRepeatCities filter after scenario planners return.
/// Note: Region filtering is done during game selection in scenario planners.
private func applyPreferenceFilters(
to result: ItineraryResult,
request: PlanningRequest
) -> ItineraryResult {
guard case .success(let originalOptions) = result else {
return result
}
var options = originalOptions
// Filter repeat cities (this is enforced during beam search, but double-check here)
options = RouteFilters.filterRepeatCities(
options,
allow: request.preferences.allowRepeatCities
)
if options.isEmpty && !request.preferences.allowRepeatCities {
let violatingCities = RouteFilters.findRepeatCities(in: originalOptions)
return .failure(PlanningFailure(
reason: .repeatCityViolation(cities: violatingCities)
))
}
// Region filtering is applied during game selection in scenario planners
return .success(options)
}
}