// // FixtureGenerator.swift // SportsTimeTests // // Generates synthetic test data for unit and integration tests. // Uses deterministic seeding for reproducible test results. // import Foundation import CoreLocation @testable import SportsTime // MARK: - Random Number Generator with Seed struct SeededRandomNumberGenerator: RandomNumberGenerator { private var state: UInt64 init(seed: UInt64) { self.state = seed } mutating func next() -> UInt64 { // xorshift64 algorithm for reproducibility state ^= state << 13 state ^= state >> 7 state ^= state << 17 return state } } // MARK: - Fixture Generator struct FixtureGenerator { // MARK: - Configuration struct Configuration { var seed: UInt64 = 12345 var gameCount: Int = 50 var stadiumCount: Int = 30 var teamCount: Int = 30 var dateRange: ClosedRange = { let start = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 1))! let end = Calendar.current.date(from: DateComponents(year: 2026, month: 9, day: 30))! return start...end }() var sports: Set = [.mlb, .nba, .nhl] var geographicSpread: GeographicSpread = .nationwide enum GeographicSpread { case nationwide // Full US coverage case regional // Concentrated in one region case corridor // Along a route (e.g., East Coast) case cluster // Single metro area } static var `default`: Configuration { Configuration() } static var minimal: Configuration { Configuration(gameCount: 5, stadiumCount: 5, teamCount: 5) } static var small: Configuration { Configuration(gameCount: 50, stadiumCount: 15, teamCount: 15) } static var medium: Configuration { Configuration(gameCount: 500, stadiumCount: 30, teamCount: 30) } static var large: Configuration { Configuration(gameCount: 2000, stadiumCount: 30, teamCount: 60) } static var stress: Configuration { Configuration(gameCount: 10000, stadiumCount: 30, teamCount: 60) } } // MARK: - Generated Data Container struct GeneratedData { let stadiums: [Stadium] let teams: [Team] let games: [Game] let stadiumsById: [UUID: Stadium] let teamsById: [UUID: Team] func richGame(from game: Game) -> RichGame? { guard let homeTeam = teamsById[game.homeTeamId], let awayTeam = teamsById[game.awayTeamId], let stadium = stadiumsById[game.stadiumId] else { return nil } return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium) } func richGames() -> [RichGame] { games.compactMap { richGame(from: $0) } } } // MARK: - City Data for Realistic Generation private static let cityData: [(name: String, state: String, lat: Double, lon: Double, region: Region)] = [ // East Coast ("New York", "NY", 40.7128, -73.9352, .east), ("Boston", "MA", 42.3601, -71.0589, .east), ("Philadelphia", "PA", 39.9526, -75.1652, .east), ("Washington", "DC", 38.9072, -77.0369, .east), ("Baltimore", "MD", 39.2904, -76.6122, .east), ("Miami", "FL", 25.7617, -80.1918, .east), ("Tampa", "FL", 27.9506, -82.4572, .east), ("Atlanta", "GA", 33.7490, -84.3880, .east), ("Charlotte", "NC", 35.2271, -80.8431, .east), ("Pittsburgh", "PA", 40.4406, -79.9959, .east), // Central ("Chicago", "IL", 41.8781, -87.6298, .central), ("Detroit", "MI", 42.3314, -83.0458, .central), ("Cleveland", "OH", 41.4993, -81.6944, .central), ("Cincinnati", "OH", 39.1031, -84.5120, .central), ("Milwaukee", "WI", 43.0389, -87.9065, .central), ("Minneapolis", "MN", 44.9778, -93.2650, .central), ("St. Louis", "MO", 38.6270, -90.1994, .central), ("Kansas City", "MO", 39.0997, -94.5786, .central), ("Dallas", "TX", 32.7767, -96.7970, .central), ("Houston", "TX", 29.7604, -95.3698, .central), // West Coast ("Los Angeles", "CA", 34.0522, -118.2437, .west), ("San Francisco", "CA", 37.7749, -122.4194, .west), ("San Diego", "CA", 32.7157, -117.1611, .west), ("Seattle", "WA", 47.6062, -122.3321, .west), ("Portland", "OR", 45.5152, -122.6784, .west), ("Phoenix", "AZ", 33.4484, -112.0740, .west), ("Denver", "CO", 39.7392, -104.9903, .west), ("Salt Lake City", "UT", 40.7608, -111.8910, .west), ("Las Vegas", "NV", 36.1699, -115.1398, .west), ("Oakland", "CA", 37.8044, -122.2712, .west), ] private static let teamNames = [ "Eagles", "Tigers", "Bears", "Lions", "Panthers", "Hawks", "Wolves", "Sharks", "Dragons", "Knights", "Royals", "Giants", "Cardinals", "Mariners", "Brewers", "Rangers", "Padres", "Dodgers", "Mets", "Yankees", "Cubs", "Sox", "Twins", "Rays", "Marlins", "Nationals", "Braves", "Reds", "Pirates", "Orioles" ] // MARK: - Generation static func generate(with config: Configuration = .default) -> GeneratedData { var rng = SeededRandomNumberGenerator(seed: config.seed) // Generate stadiums let stadiums = generateStadiums(config: config, rng: &rng) let stadiumsById = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0) }) // Generate teams (2 per stadium typically) let teams = generateTeams(stadiums: stadiums, config: config, rng: &rng) let teamsById = Dictionary(uniqueKeysWithValues: teams.map { ($0.id, $0) }) // Generate games let games = generateGames(teams: teams, stadiums: stadiums, config: config, rng: &rng) return GeneratedData( stadiums: stadiums, teams: teams, games: games, stadiumsById: stadiumsById, teamsById: teamsById ) } private static func generateStadiums( config: Configuration, rng: inout SeededRandomNumberGenerator ) -> [Stadium] { let cities = selectCities(for: config.geographicSpread, count: config.stadiumCount, rng: &rng) return cities.enumerated().map { index, city in let sport = config.sports.randomElement(using: &rng) ?? .mlb return Stadium( id: UUID(), name: "\(city.name) \(sport.rawValue) Stadium", city: city.name, state: city.state, latitude: city.lat + Double.random(in: -0.05...0.05, using: &rng), longitude: city.lon + Double.random(in: -0.05...0.05, using: &rng), capacity: Int.random(in: 20000...60000, using: &rng), sport: sport, yearOpened: Int.random(in: 1990...2024, using: &rng) ) } } private static func generateTeams( stadiums: [Stadium], config: Configuration, rng: inout SeededRandomNumberGenerator ) -> [Team] { var teams: [Team] = [] var usedNames = Set() for stadium in stadiums { // Each stadium gets 1-2 teams let teamCountForStadium = Int.random(in: 1...2, using: &rng) for _ in 0.. [Game] { var games: [Game] = [] let calendar = Calendar.current let dateRangeDays = calendar.dateComponents([.day], from: config.dateRange.lowerBound, to: config.dateRange.upperBound).day ?? 180 for _ in 0..= 2 else { break } // Pick random home team let homeTeam = teams.randomElement(using: &rng)! // Pick random away team (different from home) var awayTeam: Team repeat { awayTeam = teams.randomElement(using: &rng)! } while awayTeam.id == homeTeam.id // Find home team's stadium let stadium = stadiums.first { $0.id == homeTeam.stadiumId } ?? stadiums[0] // Random date within range let daysOffset = Int.random(in: 0.. [(name: String, state: String, lat: Double, lon: Double, region: Region)] { let cities: [(name: String, state: String, lat: Double, lon: Double, region: Region)] switch spread { case .nationwide: cities = cityData.shuffled(using: &rng) case .regional: let region = Region.allCases.randomElement(using: &rng) ?? .east cities = cityData.filter { $0.region == region }.shuffled(using: &rng) case .corridor: // East Coast corridor cities = cityData.filter { $0.region == .east }.shuffled(using: &rng) case .cluster: // Just pick one city and create variations let baseCity = cityData.randomElement(using: &rng)! cities = (0.. Trip { let prefs = preferences ?? TripPreferences( planningMode: .dateRange, sports: [.mlb], startDate: startDate, endDate: Calendar.current.date(byAdding: .day, value: stops * 2, to: startDate)! ) var tripStops: [TripStop] = [] var currentDate = startDate for i in 0.. [Game] { cities.map { city in let stadiumId = UUID() return Game( id: UUID(), homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: stadiumId, dateTime: date, sport: .mlb, season: "2026" ) } } /// Generate a stadium at a specific location static func makeStadium( id: UUID = UUID(), name: String = "Test Stadium", city: String = "Test City", state: String = "TS", latitude: Double = 40.0, longitude: Double = -100.0, capacity: Int = 40000, sport: Sport = .mlb ) -> Stadium { Stadium( id: id, name: name, city: city, state: state, latitude: latitude, longitude: longitude, capacity: capacity, sport: sport ) } /// Generate a team static func makeTeam( id: UUID = UUID(), name: String = "Test Team", abbreviation: String = "TST", sport: Sport = .mlb, city: String = "Test City", stadiumId: UUID ) -> Team { Team( id: id, name: name, abbreviation: abbreviation, sport: sport, city: city, stadiumId: stadiumId ) } /// Generate a game static func makeGame( id: UUID = UUID(), homeTeamId: UUID, awayTeamId: UUID, stadiumId: UUID, dateTime: Date = Date(), sport: Sport = .mlb, season: String = "2026", isPlayoff: Bool = false ) -> Game { Game( id: id, homeTeamId: homeTeamId, awayTeamId: awayTeamId, stadiumId: stadiumId, dateTime: dateTime, sport: sport, season: season, isPlayoff: isPlayoff ) } /// Generate a travel segment static func makeTravelSegment( from: LocationInput, to: LocationInput, distanceMiles: Double = 100, durationHours: Double = 2 ) -> TravelSegment { TravelSegment( fromLocation: from, toLocation: to, travelMode: .drive, distanceMeters: distanceMiles * TestConstants.metersPerMile, durationSeconds: durationHours * 3600 ) } /// Generate a trip stop static func makeTripStop( stopNumber: Int = 1, city: String = "Test City", state: String = "TS", coordinate: CLLocationCoordinate2D? = nil, arrivalDate: Date = Date(), departureDate: Date? = nil, games: [UUID] = [], stadium: UUID? = nil, isRestDay: Bool = false ) -> TripStop { TripStop( stopNumber: stopNumber, city: city, state: state, coordinate: coordinate, arrivalDate: arrivalDate, departureDate: departureDate ?? Calendar.current.date(byAdding: .day, value: 1, to: arrivalDate)!, games: games, stadium: stadium, isRestDay: isRestDay ) } // MARK: - Known Locations for Testing struct KnownLocations { static let nyc = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352) static let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437) static let chicago = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298) static let boston = CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589) static let miami = CLLocationCoordinate2D(latitude: 25.7617, longitude: -80.1918) static let seattle = CLLocationCoordinate2D(latitude: 47.6062, longitude: -122.3321) static let denver = CLLocationCoordinate2D(latitude: 39.7392, longitude: -104.9903) // Antipodal point (for testing haversine at extreme distances) static let antipodal = CLLocationCoordinate2D(latitude: -40.7128, longitude: 106.0648) } }