// // MockLocationService.swift // SportsTimeTests // // Mock implementation of LocationService for testing without MapKit dependencies. // import Foundation import CoreLocation import MapKit @testable import SportsTime // MARK: - Mock Location Service actor MockLocationService { // MARK: - Configuration struct Configuration { var simulatedLatency: TimeInterval = 0 var shouldFailGeocode: Bool = false var shouldFailRoute: Bool = false var defaultDrivingSpeedMPH: Double = 60.0 var useHaversineForDistance: Bool = true static var `default`: Configuration { Configuration() } static var slow: Configuration { Configuration(simulatedLatency: 1.0) } static var failingGeocode: Configuration { Configuration(shouldFailGeocode: true) } static var failingRoute: Configuration { Configuration(shouldFailRoute: true) } } // MARK: - Pre-configured Responses private var geocodeResponses: [String: CLLocationCoordinate2D] = [:] private var routeResponses: [String: RouteInfo] = [:] // MARK: - Call Tracking private(set) var geocodeCallCount = 0 private(set) var reverseGeocodeCallCount = 0 private(set) var calculateRouteCallCount = 0 private(set) var searchLocationsCallCount = 0 // MARK: - Configuration private var config: Configuration // MARK: - Initialization init(config: Configuration = .default) { self.config = config } // MARK: - Configuration Methods func configure(_ newConfig: Configuration) { self.config = newConfig } func setGeocodeResponse(for address: String, coordinate: CLLocationCoordinate2D) { geocodeResponses[address.lowercased()] = coordinate } func setRouteResponse(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D, route: RouteInfo) { let key = routeKey(from: from, to: to) routeResponses[key] = route } func reset() { geocodeResponses = [:] routeResponses = [:] geocodeCallCount = 0 reverseGeocodeCallCount = 0 calculateRouteCallCount = 0 searchLocationsCallCount = 0 config = .default } // MARK: - Simulated Network private func simulateNetwork() async throws { if config.simulatedLatency > 0 { try await Task.sleep(nanoseconds: UInt64(config.simulatedLatency * 1_000_000_000)) } } // MARK: - Geocoding func geocode(_ address: String) async throws -> CLLocationCoordinate2D? { geocodeCallCount += 1 try await simulateNetwork() if config.shouldFailGeocode { throw LocationError.geocodingFailed } // Check pre-configured responses if let coordinate = geocodeResponses[address.lowercased()] { return coordinate } // Return nil for unknown addresses (simulating "not found") return nil } func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async throws -> String? { reverseGeocodeCallCount += 1 try await simulateNetwork() if config.shouldFailGeocode { throw LocationError.geocodingFailed } // Return a simple formatted string based on coordinates return "Location at \(String(format: "%.2f", coordinate.latitude)), \(String(format: "%.2f", coordinate.longitude))" } func resolveLocation(_ input: LocationInput) async throws -> LocationInput { if input.isResolved { return input } let searchText = input.address ?? input.name guard let coordinate = try await geocode(searchText) else { throw LocationError.geocodingFailed } return LocationInput( name: input.name, coordinate: coordinate, address: input.address ) } // MARK: - Location Search func searchLocations(_ query: String) async throws -> [LocationSearchResult] { searchLocationsCallCount += 1 try await simulateNetwork() if config.shouldFailGeocode { return [] } // Check if we have a pre-configured response for this query if let coordinate = geocodeResponses[query.lowercased()] { return [ LocationSearchResult( name: query, address: "Mocked Address", coordinate: coordinate ) ] } return [] } // MARK: - Distance Calculations func calculateDistance( from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ) -> CLLocationDistance { if config.useHaversineForDistance { return haversineDistance(from: from, to: to) } // Simple Euclidean approximation (less accurate but faster) let fromLocation = CLLocation(latitude: from.latitude, longitude: from.longitude) let toLocation = CLLocation(latitude: to.latitude, longitude: to.longitude) return fromLocation.distance(from: toLocation) } func calculateDrivingRoute( from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ) async throws -> RouteInfo { calculateRouteCallCount += 1 try await simulateNetwork() if config.shouldFailRoute { throw LocationError.routeNotFound } // Check pre-configured routes let key = routeKey(from: from, to: to) if let route = routeResponses[key] { return route } // Generate estimated route based on haversine distance let distanceMeters = haversineDistance(from: from, to: to) let distanceMiles = distanceMeters * 0.000621371 // Estimate driving time (add 20% for real-world conditions) let drivingHours = (distanceMiles / config.defaultDrivingSpeedMPH) * 1.2 let travelTimeSeconds = drivingHours * 3600 return RouteInfo( distance: distanceMeters, expectedTravelTime: travelTimeSeconds, polyline: nil ) } func calculateDrivingMatrix( origins: [CLLocationCoordinate2D], destinations: [CLLocationCoordinate2D] ) async throws -> [[RouteInfo?]] { var matrix: [[RouteInfo?]] = [] for origin in origins { var row: [RouteInfo?] = [] for destination in destinations { do { let route = try await calculateDrivingRoute(from: origin, to: destination) row.append(route) } catch { row.append(nil) } } matrix.append(row) } return matrix } // MARK: - Haversine Distance /// Calculate haversine distance between two coordinates in meters private func haversineDistance( from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ) -> CLLocationDistance { let earthRadiusMeters: Double = 6371000.0 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 earthRadiusMeters * c } // MARK: - Helpers private func routeKey(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> String { "\(from.latitude),\(from.longitude)->\(to.latitude),\(to.longitude)" } } // MARK: - Convenience Extensions extension MockLocationService { /// Pre-configure common city geocoding responses func loadCommonCities() async { await setGeocodeResponse(for: "New York, NY", coordinate: FixtureGenerator.KnownLocations.nyc) await setGeocodeResponse(for: "Los Angeles, CA", coordinate: FixtureGenerator.KnownLocations.la) await setGeocodeResponse(for: "Chicago, IL", coordinate: FixtureGenerator.KnownLocations.chicago) await setGeocodeResponse(for: "Boston, MA", coordinate: FixtureGenerator.KnownLocations.boston) await setGeocodeResponse(for: "Miami, FL", coordinate: FixtureGenerator.KnownLocations.miami) await setGeocodeResponse(for: "Seattle, WA", coordinate: FixtureGenerator.KnownLocations.seattle) await setGeocodeResponse(for: "Denver, CO", coordinate: FixtureGenerator.KnownLocations.denver) } /// Create a mock service with common cities pre-loaded static func withCommonCities() async -> MockLocationService { let mock = MockLocationService() await mock.loadCommonCities() return mock } } // MARK: - Test Helpers extension MockLocationService { /// Calculate expected travel time in hours for a given distance func expectedTravelHours(distanceMiles: Double) -> Double { (distanceMiles / config.defaultDrivingSpeedMPH) * 1.2 } /// Check if a coordinate is within radius of another func isWithinRadius( _ coordinate: CLLocationCoordinate2D, of center: CLLocationCoordinate2D, radiusMiles: Double ) -> Bool { let distanceMeters = haversineDistance(from: center, to: coordinate) let distanceMiles = distanceMeters * 0.000621371 return distanceMiles <= radiusMiles } }