// // TestFixtures.swift // SportsTimeTests // // Factory methods for creating test data. These fixtures create realistic // domain objects with sensible defaults that can be customized per test. // // Usage: // let game = TestFixtures.game() // Default game // let game = TestFixtures.game(sport: .nba, city: "Boston") // Customized // import Foundation import CoreLocation @testable import SportsTime // MARK: - Test Fixtures enum TestFixtures { // MARK: - Reference Data /// Real stadium coordinates for realistic distance calculations static let coordinates: [String: CLLocationCoordinate2D] = [ "New York": CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855), // NYC (Midtown) "Boston": CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972), // Fenway Park "Chicago": CLLocationCoordinate2D(latitude: 41.9484, longitude: -87.6553), // Wrigley Field "Los Angeles": CLLocationCoordinate2D(latitude: 34.0739, longitude: -118.2400), // Dodger Stadium "San Francisco": CLLocationCoordinate2D(latitude: 37.7786, longitude: -122.3893), // Oracle Park "Seattle": CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3325), // T-Mobile Park "Denver": CLLocationCoordinate2D(latitude: 39.7559, longitude: -104.9942), // Coors Field "Houston": CLLocationCoordinate2D(latitude: 29.7573, longitude: -95.3555), // Minute Maid "Miami": CLLocationCoordinate2D(latitude: 25.7781, longitude: -80.2197), // LoanDepot Park "Atlanta": CLLocationCoordinate2D(latitude: 33.7553, longitude: -84.4006), // Truist Park "Phoenix": CLLocationCoordinate2D(latitude: 33.4455, longitude: -112.0667), // Chase Field "Dallas": CLLocationCoordinate2D(latitude: 32.7473, longitude: -97.0945), // Globe Life Field "Philadelphia": CLLocationCoordinate2D(latitude: 39.9061, longitude: -75.1665), // Citizens Bank "Detroit": CLLocationCoordinate2D(latitude: 42.3390, longitude: -83.0485), // Comerica Park "Minneapolis": CLLocationCoordinate2D(latitude: 44.9817, longitude: -93.2776), // Target Field ] /// Time zones for realistic local time testing static let timeZones: [String: String] = [ "New York": "America/New_York", "Boston": "America/New_York", "Chicago": "America/Chicago", "Los Angeles": "America/Los_Angeles", "San Francisco": "America/Los_Angeles", "Seattle": "America/Los_Angeles", "Denver": "America/Denver", "Houston": "America/Chicago", "Miami": "America/New_York", "Atlanta": "America/New_York", "Phoenix": "America/Phoenix", "Dallas": "America/Chicago", "Philadelphia": "America/New_York", "Detroit": "America/Detroit", "Minneapolis": "America/Chicago", ] /// State abbreviations static let states: [String: String] = [ "New York": "NY", "Boston": "MA", "Chicago": "IL", "Los Angeles": "CA", "San Francisco": "CA", "Seattle": "WA", "Denver": "CO", "Houston": "TX", "Miami": "FL", "Atlanta": "GA", "Phoenix": "AZ", "Dallas": "TX", "Philadelphia": "PA", "Detroit": "MI", "Minneapolis": "MN", ] // MARK: - Game Factory /// Creates a Game with realistic defaults. /// /// - Expected Behavior: /// - Returns a valid Game with all required fields populated /// - ID follows canonical format: "game_{sport}_{season}_{away}_{home}_{mmdd}" /// - DateTime defaults to noon tomorrow in specified city's timezone static func game( id: String? = nil, sport: Sport = .mlb, city: String = "New York", dateTime: Date? = nil, homeTeamId: String? = nil, awayTeamId: String? = nil, stadiumId: String? = nil, season: String = "2026", isPlayoff: Bool = false ) -> Game { let actualDateTime = dateTime ?? Calendar.current.date(byAdding: .day, value: 1, to: Date())! let homeId = homeTeamId ?? "team_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))" let awayId = awayTeamId ?? "team_\(sport.rawValue.lowercased())_visitor" let stadId = stadiumId ?? "stadium_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))" let formatter = DateFormatter() formatter.dateFormat = "MMdd" let dateStr = formatter.string(from: actualDateTime) let actualId = id ?? "game_\(sport.rawValue.lowercased())_\(season)_\(awayId.split(separator: "_").last ?? "vis")_\(homeId.split(separator: "_").last ?? "home")_\(dateStr)" return Game( id: actualId, homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadId, dateTime: actualDateTime, sport: sport, season: season, isPlayoff: isPlayoff ) } /// Creates multiple games spread across time and cities. /// /// - Parameter count: Number of games to create /// - Parameter cities: Cities to distribute games across (cycles through list) /// - Parameter startDate: First game date (subsequent games spread by daySpread) /// - Parameter daySpread: Days between games static func games( count: Int, sport: Sport = .mlb, cities: [String] = ["New York", "Boston", "Chicago", "Los Angeles"], startDate: Date = Date(), daySpread: Int = 1 ) -> [Game] { (0.. [Game] { cities.enumerated().map { index, city in // Stagger times by 3 hours let time = Calendar.current.date(byAdding: .hour, value: 13 + (index * 3), to: Calendar.current.startOfDay(for: date))! return game(sport: sport, city: city, dateTime: time) } } // MARK: - Stadium Factory /// Creates a Stadium with realistic defaults. /// /// - Expected Behavior: /// - Uses real coordinates for known cities /// - ID follows canonical format: "stadium_{sport}_{city}" /// - TimeZone populated for known cities static func stadium( id: String? = nil, name: String? = nil, city: String = "New York", state: String? = nil, sport: Sport = .mlb, capacity: Int = 40000, yearOpened: Int? = nil ) -> Stadium { let coordinate = coordinates[city] ?? CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0) let actualState = state ?? states[city] ?? "NY" let actualName = name ?? "\(city) \(sport.rawValue) Stadium" let actualId = id ?? "stadium_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))" return Stadium( id: actualId, name: actualName, city: city, state: actualState, latitude: coordinate.latitude, longitude: coordinate.longitude, capacity: capacity, sport: sport, yearOpened: yearOpened, timeZoneIdentifier: timeZones[city] ) } /// Creates a stadium map for a set of games. static func stadiumMap(for games: [Game]) -> [String: Stadium] { var map: [String: Stadium] = [:] for game in games { if map[game.stadiumId] == nil { // Extract city from stadium ID (assumes format stadium_sport_city) let parts = game.stadiumId.split(separator: "_") let city = parts.count > 2 ? parts[2...].joined(separator: " ").capitalized : "Unknown" map[game.stadiumId] = stadium(id: game.stadiumId, city: city, sport: game.sport) } } return map } /// Creates stadiums at specific coordinates for distance testing. static func stadiumsForDistanceTest() -> [Stadium] { [ stadium(city: "New York"), // East stadium(city: "Chicago"), // Central stadium(city: "Denver"), // Mountain stadium(city: "Los Angeles"), // West ] } // MARK: - Team Factory /// Creates a Team with realistic defaults. static func team( id: String? = nil, name: String = "Test Team", abbreviation: String? = nil, sport: Sport = .mlb, city: String = "New York", stadiumId: String? = nil ) -> Team { let actualId = id ?? "team_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))" let actualAbbr = abbreviation ?? String(city.prefix(3)).uppercased() let actualStadiumId = stadiumId ?? "stadium_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))" return Team( id: actualId, name: name, abbreviation: actualAbbr, sport: sport, city: city, stadiumId: actualStadiumId ) } // MARK: - TripStop Factory /// Creates a TripStop with realistic defaults. static func tripStop( stopNumber: Int = 1, city: String = "New York", state: String? = nil, arrivalDate: Date? = nil, departureDate: Date? = nil, games: [String] = [], isRestDay: Bool = false ) -> TripStop { let coordinate = coordinates[city] let actualState = state ?? states[city] ?? "NY" let arrival = arrivalDate ?? Date() let departure = departureDate ?? Calendar.current.date(byAdding: .day, value: 1, to: arrival)! return TripStop( stopNumber: stopNumber, city: city, state: actualState, coordinate: coordinate, arrivalDate: arrival, departureDate: departure, games: games, isRestDay: isRestDay ) } /// Creates a sequence of trip stops for a multi-city trip. static func tripStops( cities: [String], startDate: Date = Date(), daysPerStop: Int = 1 ) -> [TripStop] { var stops: [TripStop] = [] var currentDate = startDate for (index, city) in cities.enumerated() { let departure = Calendar.current.date(byAdding: .day, value: daysPerStop, to: currentDate)! stops.append(tripStop( stopNumber: index + 1, city: city, arrivalDate: currentDate, departureDate: departure )) currentDate = departure } return stops } // MARK: - TravelSegment Factory /// Creates a TravelSegment between two cities. static func travelSegment( from: String = "New York", to: String = "Boston", travelMode: TravelMode = .drive ) -> TravelSegment { let fromCoord = coordinates[from] ?? CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0) let toCoord = coordinates[to] ?? CLLocationCoordinate2D(latitude: 42.0, longitude: -71.0) // Calculate approximate distance (haversine) let distance = haversineDistance(from: fromCoord, to: toCoord) // Estimate driving time at 60 mph average let duration = distance / 60.0 * 3600.0 return TravelSegment( fromLocation: LocationInput(name: from, coordinate: fromCoord), toLocation: LocationInput(name: to, coordinate: toCoord), travelMode: travelMode, distanceMeters: distance * 1609.34, // miles to meters durationSeconds: duration ) } // MARK: - TripPreferences Factory /// Creates TripPreferences with common defaults. static func preferences( mode: PlanningMode = .dateRange, sports: Set = [.mlb], startDate: Date? = nil, endDate: Date? = nil, regions: Set = [.east, .central, .west], leisureLevel: LeisureLevel = .moderate, travelMode: TravelMode = .drive, needsEVCharging: Bool = false, maxDrivingHoursPerDriver: Double? = nil ) -> TripPreferences { let start = startDate ?? Date() let end = endDate ?? Calendar.current.date(byAdding: .day, value: 7, to: start)! return TripPreferences( planningMode: mode, sports: sports, travelMode: travelMode, startDate: start, endDate: end, leisureLevel: leisureLevel, routePreference: .balanced, needsEVCharging: needsEVCharging, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, selectedRegions: regions ) } // MARK: - Trip Factory /// Creates a complete Trip with stops and segments. static func trip( name: String = "Test Trip", stops: [TripStop]? = nil, preferences: TripPreferences? = nil, status: TripStatus = .planned ) -> Trip { let actualStops = stops ?? tripStops(cities: ["New York", "Boston"]) let actualPrefs = preferences ?? TestFixtures.preferences() // Calculate totals from stops let totalGames = actualStops.reduce(0) { $0 + $1.games.count } return Trip( name: name, preferences: actualPrefs, stops: actualStops, totalGames: totalGames, status: status ) } // MARK: - RichGame Factory /// Creates a RichGame with resolved team and stadium references. static func richGame( game: Game? = nil, homeCity: String = "New York", awayCity: String = "Boston", sport: Sport = .mlb ) -> RichGame { let actualGame = game ?? TestFixtures.game(sport: sport, city: homeCity) let homeTeam = team(sport: sport, city: homeCity) let awayTeam = team(sport: sport, city: awayCity) let gameStadium = stadium(city: homeCity, sport: sport) return RichGame( game: actualGame, homeTeam: homeTeam, awayTeam: awayTeam, stadium: gameStadium ) } // MARK: - TripScore Factory /// Creates a TripScore with customizable component scores. static func tripScore( overall: Double = 85.0, gameQuality: Double = 90.0, routeEfficiency: Double = 80.0, leisureBalance: Double = 85.0, preferenceAlignment: Double = 85.0 ) -> TripScore { TripScore( overallScore: overall, gameQualityScore: gameQuality, routeEfficiencyScore: routeEfficiency, leisureBalanceScore: leisureBalance, preferenceAlignmentScore: preferenceAlignment ) } // MARK: - Date Helpers /// Creates a date at a specific time (for testing time-sensitive logic). static func date( year: Int = 2026, month: Int = 6, day: Int = 15, hour: Int = 19, minute: Int = 5 ) -> Date { var components = DateComponents() components.year = year components.month = month components.day = day components.hour = hour components.minute = minute components.timeZone = TimeZone(identifier: "America/New_York") return Calendar.current.date(from: components)! } /// Creates dates for a range of days. static func dateRange(start: Date = Date(), days: Int) -> (start: Date, end: Date) { let end = Calendar.current.date(byAdding: .day, value: days, to: start)! return (start, end) } // MARK: - Private Helpers /// Haversine distance calculation (returns miles). private static func haversineDistance( from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ) -> Double { let R = 3958.8 // Earth radius in miles let lat1 = from.latitude * .pi / 180 let lat2 = to.latitude * .pi / 180 let deltaLat = (to.latitude - from.latitude) * .pi / 180 let deltaLon = (to.longitude - from.longitude) * .pi / 180 let a = sin(deltaLat / 2) * sin(deltaLat / 2) + cos(lat1) * cos(lat2) * sin(deltaLon / 2) * sin(deltaLon / 2) let c = 2 * atan2(sqrt(a), sqrt(1 - a)) return R * c } } // MARK: - Coordinate Constants for Testing extension TestFixtures { /// Known distances between cities (in miles) for validation. static let knownDistances: [(from: String, to: String, miles: Double)] = [ ("New York", "Boston", 215), ("New York", "Chicago", 790), ("New York", "Los Angeles", 2790), ("Chicago", "Denver", 1000), ("Los Angeles", "San Francisco", 380), ("Seattle", "Los Angeles", 1135), ] /// Cities clearly in each region for boundary testing. static let eastCoastCities = ["New York", "Boston", "Miami", "Atlanta", "Philadelphia"] static let centralCities = ["Chicago", "Houston", "Dallas", "Minneapolis", "Detroit"] static let westCoastCities = ["Los Angeles", "San Francisco", "Seattle", "Phoenix"] }