# Test Suite Implementation Plan

**Scope:** TO-DOS items 20.0 - 20.10
**Approach:** TDD — tests define expected behavior, then app code is updated to pass
**Framework:** Swift Testing (`@Test`, `#expect`, `@Suite`) with XCTest fallback if needed
**Status:** Use checkboxes to track progress

---

## Pre-Implementation Setup

### Phase 0: Test Infrastructure
> Set up foundations before writing any feature tests.

- [x] **0.1** Create `SportsTimeTests/` directory structure:
  ```
  SportsTimeTests/
  ├── Fixtures/
  │   └── FixtureGenerator.swift
  ├── Mocks/
  │   ├── MockCloudKitService.swift
  │   ├── MockLocationService.swift
  │   └── MockAppDataProvider.swift
  ├── Helpers/
  │   ├── BruteForceRouteVerifier.swift
  │   └── TestConstants.swift
  ├── Planning/
  │   ├── GameDAGRouterTests.swift
  │   ├── ScenarioAPlannerTests.swift
  │   ├── ScenarioBPlannerTests.swift
  │   ├── ScenarioCPlannerTests.swift
  │   ├── TravelEstimatorTests.swift
  │   ├── ItineraryBuilderTests.swift
  │   └── TripPlanningEngineTests.swift
  └── Performance/
      └── PlannerScaleTests.swift
  ```

- [x] **0.2** Implement `FixtureGenerator.swift`:
  - Generate synthetic `Game` objects with configurable:
    - Count (5, 50, 500, 2000, 10000)
    - Date range
    - Geographic spread
    - Stadium/team distribution
  - Generate synthetic `Stadium` dictionary
  - Generate synthetic `Trip` with configurable stops
  - Deterministic seeding for reproducible tests

- [x] **0.3** Implement `MockCloudKitService.swift`:
  - Stub all public methods
  - Return fixture data
  - Configurable error injection

- [x] **0.4** Implement `MockLocationService.swift`:
  - Stub geocoding
  - Stub routing (return pre-calculated distances)
  - Configurable latency simulation

- [x] **0.5** Implement `MockAppDataProvider.swift`:
  - Return fixture stadiums/teams/games
  - Configurable to simulate empty data

- [x] **0.6** Implement `BruteForceRouteVerifier.swift`:
  - For inputs with ≤8 stops, exhaustively enumerate all permutations
  - Compare engine result against true optimal
  - Used to validate "no obviously better route exists"

- [x] **0.7** Implement `TestConstants.swift`:
  ```swift
  enum TestConstants {
      static let nearbyRadiusMiles: Double = 50.0
      static let performanceTimeout: TimeInterval = 300.0 // 5 min
      static let hangTimeout: TimeInterval = 30.0
      // Baselines TBD after initial runs
      static var baseline500Games: TimeInterval = 0
      static var baseline2000Games: TimeInterval = 0
      static var baseline10000Games: TimeInterval = 0
  }
  ```

---

## Feature Test Phases

### Phase 1: TravelEstimator Tests
> Foundation — all planners depend on this.

**File:** `TravelEstimatorTests.swift`

- [x] **1.1** `test_haversineDistanceMiles_KnownDistance`
  NYC to LA = ~2,451 miles (verify within 1% tolerance)

- [x] **1.2** `test_haversineDistanceMiles_SamePoint_ReturnsZero`

- [x] **1.3** `test_haversineDistanceMiles_Antipodal_ReturnsHalfEarthCircumference`

- [x] **1.4** `test_estimate_NilCoordinates_ReturnsNil`

- [x] **1.5** `test_estimate_ExceedsMaxDailyHours_ReturnsNil`
  Distance requiring 3+ days of driving should return nil

- [x] **1.6** `test_estimate_ValidTrip_ReturnsSegment`
  Verify `distanceMeters`, `durationSeconds`, `travelMode`

- [x] **1.7** `test_calculateTravelDays_SingleDayDrive`
  4 hours driving = 1 day

- [x] **1.8** `test_calculateTravelDays_MultiDayDrive`
  20 hours driving = 3 days (ceil(20/8))

- [x] **1.9** `test_estimateFallbackDistance_SameCity_ReturnsZero`

- [x] **1.10** `test_estimateFallbackDistance_DifferentCity_Returns300`

---

### Phase 2: GameDAGRouter Tests — Core Logic
> The "scary to touch" component — extensive edge case coverage.

**File:** `GameDAGRouterTests.swift`

#### 2A: Empty & Single-Element Cases

- [x] **2.1** `test_findRoutes_EmptyGames_ReturnsEmptyArray`

- [x] **2.2** `test_findRoutes_SingleGame_ReturnsSingleRoute`

- [x] **2.3** `test_findRoutes_SingleGame_WithMatchingAnchor_ReturnsSingleRoute`

- [x] **2.4** `test_findRoutes_SingleGame_WithNonMatchingAnchor_ReturnsEmpty`

#### 2B: Two-Game Cases

- [x] **2.5** `test_findRoutes_TwoGames_FeasibleTransition_ReturnsBothInOrder`

- [x] **2.6** `test_findRoutes_TwoGames_InfeasibleTransition_ReturnsSeparateRoutes`

- [x] **2.7** `test_findRoutes_TwoGames_SameStadiumSameDay_Succeeds`
  (Doubleheader scenario)

#### 2C: Anchor Game Constraints

- [x] **2.8** `test_findRoutes_WithAnchors_OnlyReturnsRoutesContainingAllAnchors`

- [x] **2.9** `test_findRoutes_ImpossibleAnchors_ReturnsEmpty`
  Anchors are geographically/temporally impossible to connect

- [x] **2.10** `test_findRoutes_MultipleAnchors_RouteMustContainAll`

#### 2D: Repeat Cities Toggle

- [x] **2.11** `test_findRoutes_AllowRepeatCities_SameCityMultipleDays_Allowed`

- [x] **2.12** `test_findRoutes_DisallowRepeatCities_SkipsSecondVisit`

- [x] **2.13** `test_findRoutes_DisallowRepeatCities_OnlyOptionIsRepeat_OverridesWithWarning`
  **TDD Note:** This test defines a new `Trip.warnings: [PlanningWarning]` property

#### 2E: Driving Constraints

- [x] **2.14** `test_findRoutes_ExceedsMaxDailyDriving_TransitionRejected`

- [x] **2.15** `test_findRoutes_MultiDayDrive_Allowed_IfWithinDailyLimits`

- [x] **2.16** `test_findRoutes_SameDayDifferentStadiums_ChecksAvailableTime`

#### 2F: Calendar Day Logic

- [x] **2.17** `test_findRoutes_MaxDayLookahead_RespectsLimit`
  Games > 5 days apart should not connect directly

- [x] **2.18** `test_findRoutes_DSTTransition_HandlesCorrectly`
  Spring forward / fall back edge case

- [x] **2.19** `test_findRoutes_MidnightGame_AssignsToCorrectDay`
  Game at 12:05 AM belongs to new day

#### 2G: Diversity Selection

- [x] **2.20** `test_selectDiverseRoutes_ShortAndLongTrips_BothRepresented`

- [x] **2.21** `test_selectDiverseRoutes_HighAndLowMileage_BothRepresented`

- [x] **2.22** `test_selectDiverseRoutes_FewAndManyCities_BothRepresented`

- [x] **2.23** `test_selectDiverseRoutes_DuplicateRoutes_Deduplicated`

#### 2H: Cycle Handling

- [x] **2.24** `test_findRoutes_GraphWithPotentialCycle_HandlesSilently`
  Verify no infinite loop, returns valid routes

#### 2I: Beam Search Behavior

- [x] **2.25** `test_findRoutes_LargeDataset_ScalesBeamWidth`
  Verify `effectiveBeamWidth` kicks in for 5000+ games

- [x] **2.26** `test_findRoutes_EarlyTermination_TriggersWhenBeamFull`

---

### Phase 3: GameDAGRouter Tests — Scale & Performance
> Stress tests for 10K+ objects. Periodic/manual execution.

**File:** `GameDAGRouterTests.swift` (continued) or separate `GameDAGRouterScaleTests.swift`

#### 3A: Scale Tests

- [x] **3.1** `test_findRoutes_5Games_CompletesWithin5Minutes`

- [x] **3.2** `test_findRoutes_50Games_CompletesWithin5Minutes`

- [x] **3.3** `test_findRoutes_500Games_CompletesWithin5Minutes`

- [x] **3.4** `test_findRoutes_2000Games_CompletesWithin5Minutes`

- [x] **3.5** `test_findRoutes_10000Games_CompletesWithin5Minutes`

- [x] **3.6** `test_findRoutes_50000Nodes_CompletesWithin5Minutes`
  Stress test — may need timeout adjustment

#### 3B: Performance Baselines

- [x] **3.7** Record baseline times for 500/2000/10000 games (first run)

- [x] **3.8** After baselines established, add regression assertions

#### 3C: Memory Tests

- [x] **3.9** `test_findRoutes_RepeatedCalls_NoMemoryLeak`
  Run 100 iterations, verify allocation/deallocation balance

- [x] **3.10** `test_findRoutes_LargeDataset_MemoryBounded`
  10K games should not exceed reasonable memory

---

### Phase 4: ScenarioAPlanner Tests (Plan by Dates)
> User provides dates, planner finds games.

**File:** `ScenarioAPlannerTests.swift`

#### 4A: Valid Inputs

- [x] **4.1** `test_planByDates_ValidDateRange_ReturnsGamesInRange`

- [x] **4.2** `test_planByDates_SingleDayRange_ReturnsGamesOnThatDay`

- [x] **4.3** `test_planByDates_MultiWeekRange_ReturnsMultipleGames`

#### 4B: Edge Cases

- [x] **4.4** `test_planByDates_NoGamesInRange_ThrowsError`
  **TDD Note:** Uses existing `PlanningFailure.FailureReason.noGamesInRange`

- [x] **4.5** `test_planByDates_EndDateBeforeStartDate_ThrowsError`

- [x] **4.6** `test_planByDates_SingleGameInRange_ReturnsSingleGameRoute`

- [x] **4.7** `test_planByDates_MaxGamesInRange_HandlesGracefully`
  10K games in range — verify no crash/hang

#### 4C: Integration with DAG

- [x] **4.8** `test_planByDates_UsesDAGRouterForRouting`

- [x] **4.9** `test_planByDates_RespectsDriverConstraints`

---

### Phase 5: ScenarioBPlanner Tests (Must-See Games)
> User selects specific games, planner builds route.

**File:** `ScenarioBPlannerTests.swift`

#### 5A: Valid Inputs

- [x] **5.1** `test_mustSeeGames_SingleGame_ReturnsTripWithThatGame`

- [x] **5.2** `test_mustSeeGames_MultipleGames_ReturnsOptimalRoute`

- [x] **5.3** `test_mustSeeGames_GamesInDifferentCities_ConnectsThem`

#### 5B: Edge Cases

- [x] **5.4** `test_mustSeeGames_EmptySelection_ThrowsError`

- [x] **5.5** `test_mustSeeGames_ImpossibleToConnect_ThrowsError`
  Games on same day in cities 850+ miles apart (same region)

- [x] **5.6** `test_mustSeeGames_MaxGamesSelected_HandlesGracefully`

#### 5C: Optimality Verification

- [x] **5.7** `test_mustSeeGames_SmallInput_MatchesBruteForceOptimal`
  ≤8 games — verify against `BruteForceRouteVerifier`

- [x] **5.8** `test_mustSeeGames_LargeInput_NoObviouslyBetterRoute`

---

### Phase 6: ScenarioCPlanner Tests (Depart/Return Cities)
> User specifies origin and destination.

**File:** `ScenarioCPlannerTests.swift`

#### 6A: Valid Inputs

- [x] **6.1** `test_departReturn_SameCity_ReturnsRoundTrip`

- [x] **6.2** `test_departReturn_DifferentCities_ReturnsOneWayRoute`

- [x] **6.3** `test_departReturn_GamesAlongCorridor_IncludesNearbyGames`
  Uses 50-mile radius

#### 6B: Edge Cases

- [x] **6.4** `test_departReturn_NoGamesAlongRoute_ThrowsError`

- [x] **6.5** `test_departReturn_InvalidCity_ThrowsError`

- [x] **6.6** `test_departReturn_ExtremeDistance_RespectsConstraints`
  NYC to LA — verify driving constraints applied

#### 6C: Must-Stop Locations

- [x] **6.7** `test_departReturn_WithMustStopLocation_IncludesStop`

- [x] **6.8** `test_departReturn_MustStopNoNearbyGames_IncludesStopAnyway`
  Stop is included but may not have games

- [x] **6.9** `test_departReturn_MultipleMustStops_AllIncluded`

- [x] **6.10** `test_departReturn_MustStopConflictsWithRoute_FindsCompromise`

---

### Phase 7: TripPlanningEngine Integration Tests
> Main orchestrator — tests all scenarios together.

**File:** `TripPlanningEngineTests.swift`

#### 7A: Scenario Routing

- [x] **7.1** `test_engine_ScenarioA_DelegatesCorrectly`

- [x] **7.2** `test_engine_ScenarioB_DelegatesCorrectly`

- [x] **7.3** `test_engine_ScenarioC_DelegatesCorrectly`

- [x] **7.4** `test_engine_ScenariosAreMutuallyExclusive`
  Cannot mix scenarios in single request

#### 7B: Result Structure

- [x] **7.5** `test_engine_Result_ContainsTravelSegments`

- [x] **7.6** `test_engine_Result_ContainsItineraryDays`

- [x] **7.7** `test_engine_Result_IncludesWarnings_WhenApplicable`
  **TDD Note:** Validates `Trip.warnings` property exists

#### 7C: Constraint Application

- [x] **7.8** `test_engine_NumberOfDrivers_AffectsMaxDailyDriving`
  More drivers = can drive more per day

- [x] **7.9** `test_engine_MaxDrivingPerDay_Respected`

- [x] **7.10** `test_engine_AllowRepeatCities_PropagatedToDAG`

#### 7D: Error Handling

- [x] **7.11** `test_engine_ImpossibleConstraints_ReturnsNoResult`

- [x] **7.12** `test_engine_EmptyInput_ThrowsError`

---

### Phase 8: ItineraryBuilder Tests
> Builds day-by-day itinerary from route.

**File:** `ItineraryBuilderTests.swift`

- [x] **8.1** `test_builder_SingleGame_CreatesSingleDay`

- [x] **8.2** `test_builder_MultiCity_CreatesTravelSegmentsBetween`

- [x] **8.3** `test_builder_SameCity_MultipleGames_GroupsOnSameDay`

- [x] **8.4** `test_builder_TravelDays_InsertedWhenDrivingExceeds8Hours`

- [x] **8.5** `test_builder_ArrivalTimeBeforeGame_Calculated`

- [x] **8.6** `test_builder_EmptyRoute_ReturnsEmptyItinerary`

---

### Phase 9: RouteFilters Tests
> Filtering on All Trips list.

**File:** `RouteFiltersTests.swift`

#### 9A: Single Filters

- [x] **9.1** `test_filterBySport_SingleSport_ReturnsMatching`

- [x] **9.2** `test_filterBySport_MultipleSports_ReturnsUnion`

- [x] **9.3** `test_filterBySport_AllSports_ReturnsAll`

- [x] **9.4** `test_filterByDateRange_ReturnsTripsInRange`
  (+ bonus: `test_filterByDateRange_IncludesOverlappingTrips`)

- [x] **9.5** `test_filterByStatus_Planned_ReturnsPlanned`
  **TDD Note:** `Trip.status: TripStatus` property added

- [x] **9.6** `test_filterByStatus_InProgress_ReturnsInProgress`

- [x] **9.7** `test_filterByStatus_Completed_ReturnsCompleted`

#### 9B: Combined Filters

- [x] **9.8** `test_combinedFilters_SportAndDate_ReturnsIntersection`

- [x] **9.9** `test_combinedFilters_AllFilters_ReturnsIntersection`

#### 9C: Edge Cases

- [x] **9.10** `test_filter_NoMatches_ReturnsEmptyArray`

- [x] **9.11** `test_filter_AllMatch_ReturnsAll`

- [x] **9.12** `test_filter_EmptyInput_ReturnsEmptyArray`

---

### Phase 10: Concurrency Tests
> Prove current implementation is NOT thread-safe (for future work).

**File:** `ConcurrencyTests.swift`

- [x] **10.1** `test_engine_ConcurrentRequests_CurrentlyUnsafe`
  Document existing behavior (may crash/race)

- [x] **10.2** `test_engine_SequentialRequests_Succeeds`

**Future (out of scope for now):**
- [ ] `test_engine_ConcurrentRequests_ThreadSafe` (after refactor)

---

### Phase 11: Edge Case Omnibus
> Catch-all for extreme/unusual inputs.

**File:** `EdgeCaseTests.swift`

#### 11A: Data Edge Cases

- [x] **11.1** `test_nilStadium_HandlesGracefully`

- [x] **11.2** `test_malformedDate_HandlesGracefully`

- [x] **11.3** `test_invalidCoordinates_HandlesGracefully`
  Lat > 90, Lon > 180

- [x] **11.4** `test_missingRequiredFields_HandlesGracefully`

#### 11B: Boundary Conditions

- [x] **11.5** `test_exactlyAtDrivingLimit_Succeeds`

- [x] **11.6** `test_oneMileOverLimit_Fails`

- [x] **11.7** `test_exactlyAtRadiusBoundary_IncludesGame`
  Game at exactly 50 miles

- [x] **11.8** `test_oneFootOverRadius_ExcludesGame`

#### 11C: Time Zone Cases

- [x] **11.9** `test_gameInDifferentTimeZone_NormalizesToUTC`

- [x] **11.10** `test_dstSpringForward_HandlesCorrectly`

- [x] **11.11** `test_dstFallBack_HandlesCorrectly`

---

## API Proposals (TDD Discoveries)

These APIs will be defined by tests and need implementation:

| Property/Type | Defined In | Description |
|--------------|------------|-------------|
| `Trip.warnings: [PlanningWarning]` | 2.13 | Warnings when planner overrides preferences |
| `PlanningWarning` enum | 2.13 | `.repeatCityOverridden(city: String)`, etc. |
| `PlanningError.noGamesFound` | 4.4 | Thrown when date range has no games |
| `Trip.status: TripStatus` | 9.5 | `.planned`, `.inProgress`, `.completed` |

---

## Execution Order

**Recommended batch order for TDD cycle:**

1. **Phase 0** — Infrastructure (no app changes)
2. **Phase 1** — TravelEstimator (foundation, likely passes)
3. **Phase 2** — GameDAGRouter core (highest risk, most edge cases)
4. **Phase 3** — GameDAGRouter scale (set baselines)
5. **Phase 4-6** — Scenario planners (A, B, C)
6. **Phase 7** — TripPlanningEngine integration
7. **Phase 8** — ItineraryBuilder
8. **Phase 9** — RouteFilters
9. **Phase 10** — Concurrency (documentation tests)
10. **Phase 11** — Edge cases

---

## Progress Tracking

| Phase | Tests | Passing | Status |
|-------|-------|---------|--------|
| 0 | 7 | 7 | Complete |
| 1 | 10 | 10 | Complete |
| 2 | 26 | 26 | Complete |
| 3 | 10 | 10 | Complete |
| 4 | 9 | 9 | Complete |
| 5 | 8 | 8 | Complete |
| 6 | 10 | 10 | Complete |
| 7 | 12 | 12 | Complete |
| 8 | 6 | 6 | Complete |
| 9 | 13 | 13 | Complete |
| 10 | 2 | 2 | Complete |
| 11 | 11 | 11 | Complete |
| **Total** | **124** | **124** | |

---

## Notes

- **Hang timeout:** 30 seconds (tests marked as hanging if exceeded)
- **Performance timeout:** 5 minutes per test (scale tests)
- **Nearby radius:** 50 miles driving distance (`TestConstants.nearbyRadiusMiles`)
- **Brute force threshold:** ≤8 stops for exact optimality verification
- **Baselines:** Will be recorded after initial runs, then hardcoded

---

*Last updated: 2026-01-11 (Phase 11 complete — All tests passing)*
