//
//  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
    }
}
