# Unit Test Rewrite Design

## Goal

Delete all existing broken tests and create comprehensive new test coverage using Swift Testing framework, covering all edge cases for Planning Engine, Domain Models, and Services.

## Requirements

- **Framework:** Swift Testing (`@Test`, `#expect`, `@Suite`)
- **Structure:** Mirror source folder layout
- **Parallelization:** Tests run in parallel across multiple simulator devices
- **Coverage:** ~52 source files, ~150-200 tests total

## Test Structure

```
SportsTimeTests/
├── Planning/
│   ├── TripPlanningEngineTests.swift
│   ├── GameDAGRouterTests.swift
│   ├── ItineraryBuilderTests.swift
│   ├── RouteFiltersTests.swift
│   ├── TravelEstimatorTests.swift
│   ├── ScenarioPlannerTests.swift
│   ├── ScenarioAPlannerTests.swift
│   ├── ScenarioBPlannerTests.swift
│   ├── ScenarioCPlannerTests.swift
│   ├── ScenarioDPlannerTests.swift
│   └── PlanningModelsTests.swift
├── Domain/
│   ├── GameTests.swift
│   ├── TripTests.swift
│   ├── TripStopTests.swift
│   ├── StadiumTests.swift
│   ├── TeamTests.swift
│   ├── SportTests.swift
│   ├── RegionTests.swift
│   ├── DivisionTests.swift
│   ├── TravelSegmentTests.swift
│   ├── ProgressTests.swift
│   ├── TripPreferencesTests.swift
│   ├── DynamicSportTests.swift
│   ├── TripPollTests.swift
│   ├── AnySportTests.swift
│   └── AchievementDefinitionsTests.swift
├── Services/
│   ├── CloudKitServiceTests.swift
│   ├── DataProviderTests.swift
│   ├── LocationServiceTests.swift
│   ├── AchievementEngineTests.swift
│   ├── PollServiceTests.swift
│   ├── GameMatcherTests.swift
│   ├── EVChargingServiceTests.swift
│   ├── StadiumProximityMatcherTests.swift
│   ├── RouteDescriptionGeneratorTests.swift
│   ├── SuggestedTripsGeneratorTests.swift
│   └── (remaining services...)
└── Helpers/
    ├── TestFixtures.swift
    └── MockServices.swift
```

## Parallel Execution Configuration

### Xcode Test Plan (`SportsTimeTests.xctestplan`)

```json
{
  "configurations": [
    {
      "name": "Parallel Configuration",
      "options": {
        "targetForVariableExpansion": {
          "containerPath": "container:SportsTime.xcodeproj",
          "identifier": "SportsTimeTests"
        },
        "maximumParallelSimulators": 4,
        "parallelExecutionEnabled": true
      }
    }
  ],
  "defaultOptions": {
    "maximumParallelSimulators": 4,
    "parallelExecutionEnabled": true
  }
}
```

### Command Line Parallel Execution

```bash
# Run tests in parallel across 4 simulators
xcodebuild test \
  -project SportsTime.xcodeproj \
  -scheme SportsTime \
  -parallel-testing-enabled YES \
  -parallel-testing-worker-count 4 \
  -destination 'platform=iOS Simulator,name=iPhone 17' \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
  -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M4)'
```

### Swift Testing Parallelization

Swift Testing runs tests in parallel by default. For tests that must run serially (shared state), use:

```swift
@Suite(.serialized)
struct DatabaseTests {
    // These tests run one at a time
}
```

## Test Patterns

### Standard Test Structure

```swift
import Testing
@testable import SportsTime

@Suite("GameDAGRouter")
struct GameDAGRouterTests {

    // MARK: - findRoutes

    @Test("Returns valid routes for standard input")
    func findRoutes_validInput_returnsRoutes() async {
        let games = TestFixtures.games(count: 5)
        let stadiums = TestFixtures.stadiumMap(for: games)

        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            maxDailyDrivingMiles: 500,
            beamWidth: 10
        )

        #expect(!routes.isEmpty)
        #expect(routes.allSatisfy { $0.count <= games.count })
    }

    @Test("Returns empty for empty game list")
    func findRoutes_emptyGames_returnsEmpty() async {
        let routes = GameDAGRouter.findRoutes(
            games: [],
            stadiums: [:],
            maxDailyDrivingMiles: 500,
            beamWidth: 10
        )

        #expect(routes.isEmpty)
    }

    @Test("Handles single game")
    func findRoutes_singleGame_returnsSingleRoute() async {
        let game = TestFixtures.game()
        let stadiums = TestFixtures.stadiumMap(for: [game])

        let routes = GameDAGRouter.findRoutes(
            games: [game],
            stadiums: stadiums,
            maxDailyDrivingMiles: 500,
            beamWidth: 10
        )

        #expect(routes.count == 1)
        #expect(routes.first?.count == 1)
    }

    // MARK: - Parameterized Scale Tests

    @Test("Scales with game count", arguments: [5, 10, 25, 50, 100])
    func findRoutes_scales(gameCount: Int) async {
        let games = TestFixtures.games(count: gameCount)
        let stadiums = TestFixtures.stadiumMap(for: games)

        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            maxDailyDrivingMiles: 500,
            beamWidth: 10
        )

        #expect(routes.count > 0)
    }

    // MARK: - canTransition Edge Cases

    @Test("Rejects impossible same-day transitions")
    func canTransition_impossibleSameDay_returnsFalse() async {
        let nyGame = TestFixtures.game(city: "New York", date: Date())
        let laGame = TestFixtures.game(city: "Los Angeles", date: Date())

        let canTransit = GameDAGRouter.canTransition(
            from: nyGame,
            to: laGame,
            maxDailyDrivingMiles: 500
        )

        #expect(!canTransit)
    }
}
```

### Test Fixtures

```swift
// TestFixtures.swift
import Foundation
@testable import SportsTime

enum TestFixtures {

    // MARK: - Games

    static func game(
        id: String = UUID().uuidString,
        sport: Sport = .mlb,
        city: String = "New York",
        date: Date = Date(),
        homeTeamId: String = "nyy",
        awayTeamId: String = "bos"
    ) -> Game {
        Game(
            id: id,
            sport: sport,
            homeTeamId: homeTeamId,
            awayTeamId: awayTeamId,
            stadiumId: "\(city.lowercased())-stadium",
            dateTime: date
        )
    }

    static func games(count: Int, spread: TimeInterval = 86400) -> [Game] {
        (0..<count).map { i in
            game(
                id: "game-\(i)",
                date: Date().addingTimeInterval(Double(i) * spread)
            )
        }
    }

    // MARK: - Stadiums

    static func stadium(
        id: String = UUID().uuidString,
        name: String = "Test Stadium",
        city: String = "New York",
        latitude: Double = 40.7128,
        longitude: Double = -74.0060
    ) -> Stadium {
        Stadium(
            id: id,
            name: name,
            city: city,
            state: "NY",
            latitude: latitude,
            longitude: longitude,
            sport: .mlb,
            teamIds: ["test-team"]
        )
    }

    static func stadiumMap(for games: [Game]) -> [String: Stadium] {
        var map: [String: Stadium] = [:]
        for game in games {
            if map[game.stadiumId] == nil {
                map[game.stadiumId] = stadium(id: game.stadiumId)
            }
        }
        return map
    }

    // MARK: - Trips

    static func trip(
        id: String = UUID().uuidString,
        stops: [TripStop] = []
    ) -> Trip {
        Trip(
            id: id,
            name: "Test Trip",
            stops: stops,
            createdAt: Date(),
            status: .planned
        )
    }

    // MARK: - Planning Requests

    static func planningRequest(
        mode: PlanningMode = .dateRange,
        sports: Set<Sport> = [.mlb],
        startDate: Date = Date(),
        endDate: Date = Date().addingTimeInterval(86400 * 7),
        regions: Set<Region> = [.east]
    ) -> PlanningRequest {
        PlanningRequest(
            mode: mode,
            sports: sports,
            startDate: startDate,
            endDate: endDate,
            regions: regions,
            routePreference: .balanced,
            allowRepeatCities: false,
            mustStopLocations: []
        )
    }
}
```

### Mock Services

```swift
// MockServices.swift
import Foundation
@testable import SportsTime

// MARK: - Mock CloudKit Service

final class MockCloudKitService: CloudKitServiceProtocol {
    var stubbedGames: [Game] = []
    var stubbedStadiums: [Stadium] = []
    var stubbedTeams: [Team] = []
    var shouldFail = false
    var failureError: Error = NSError(domain: "test", code: -1)

    func fetchGames() async throws -> [Game] {
        if shouldFail { throw failureError }
        return stubbedGames
    }

    func fetchStadiums() async throws -> [Stadium] {
        if shouldFail { throw failureError }
        return stubbedStadiums
    }

    func fetchTeams() async throws -> [Team] {
        if shouldFail { throw failureError }
        return stubbedTeams
    }
}

// MARK: - Mock Location Service

final class MockLocationService: LocationServiceProtocol {
    var stubbedDistance: Double = 100.0
    var stubbedTravelTime: TimeInterval = 3600
    var shouldFail = false

    func calculateDistance(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) async throws -> Double {
        if shouldFail { throw LocationError.unavailable }
        return stubbedDistance
    }

    func calculateTravelTime(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) async throws -> TimeInterval {
        if shouldFail { throw LocationError.unavailable }
        return stubbedTravelTime
    }
}
```

## Edge Cases by Module

### Planning Engine

| Function | Edge Cases |
|----------|------------|
| `planItineraries` | Empty request, past dates, single day, 30+ days, no matching games |
| `findRoutes` | 0 games, 1 game, 100+ games, all same city, all different cities |
| `canTransition` | Same day/different coast, next day/same city, impossible distances |
| `build` | Empty route, circular route, gaps in dates |
| `filterRepeatCities` | No repeats, all repeats, alternating cities |
| `estimate` | Zero distance, cross-country, international coords |

### Domain Models

| Model | Edge Cases |
|-------|------------|
| `Game` | Nil teams, past dates, invalid stadium ID |
| `Trip` | Empty stops, single stop, duplicate stops |
| `Stadium` | Edge coordinates (0,0), (90,180), negative values |
| `Region.classify` | Boundary longitudes (-85, -110), exact boundaries |
| `Sport` | All enum cases, raw value round-trip |

### Services

| Service | Edge Cases |
|---------|------------|
| `CloudKitService` | Network failure, empty response, partial data, quota exceeded |
| `DataProvider` | Not configured, empty cache, stale data |
| `LocationService` | No permission, unavailable, invalid coordinates |
| `AchievementEngine` | No visits, first visit, duplicate check |

## Implementation Order

1. **Delete existing tests** - Remove all 38 broken test files
2. **Create infrastructure** - TestFixtures.swift, MockServices.swift
3. **Planning tests** (highest priority - core business logic)
4. **Domain tests** (data integrity)
5. **Services tests** (integrations)

## Success Criteria

- All tests pass
- Tests run in parallel across 4 simulators
- No flaky tests (deterministic, no timing dependencies)
- Coverage of all public functions
- Edge cases documented and tested
