//
//  EdgeCaseTests.swift
//  SportsTimeTests
//
//  Phase 11: Edge Case Omnibus
//  Catch-all for extreme/unusual inputs.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("Edge Case Tests", .serialized)
struct EdgeCaseTests {

    // MARK: - Test Fixtures

    private let calendar = Calendar.current

    /// Creates a date with specific year/month/day/hour
    private func makeDate(year: Int = 2026, month: Int = 6, day: Int, hour: Int = 19, minute: Int = 0) -> 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.date(from: components)!
    }

    /// Creates a stadium at a known location
    private func makeStadium(
        id: UUID = UUID(),
        city: String,
        state: String = "ST",
        lat: Double,
        lon: Double,
        sport: Sport = .mlb
    ) -> Stadium {
        Stadium(
            id: id,
            name: "\(city) Stadium",
            city: city,
            state: state,
            latitude: lat,
            longitude: lon,
            capacity: 40000,
            sport: sport
        )
    }

    /// Creates a game at a stadium
    private func makeGame(
        id: UUID = UUID(),
        stadiumId: UUID,
        homeTeamId: UUID = UUID(),
        awayTeamId: UUID = UUID(),
        dateTime: Date,
        sport: Sport = .mlb
    ) -> Game {
        Game(
            id: id,
            homeTeamId: homeTeamId,
            awayTeamId: awayTeamId,
            stadiumId: stadiumId,
            dateTime: dateTime,
            sport: sport,
            season: "2026"
        )
    }

    /// Creates an ItineraryStop for testing
    private func makeItineraryStop(
        city: String,
        state: String = "ST",
        coordinate: CLLocationCoordinate2D? = nil,
        games: [UUID] = [],
        arrivalDate: Date = Date()
    ) -> ItineraryStop {
        ItineraryStop(
            city: city,
            state: state,
            coordinate: coordinate,
            games: games,
            arrivalDate: arrivalDate,
            departureDate: arrivalDate.addingTimeInterval(86400),
            location: LocationInput(name: city, coordinate: coordinate),
            firstGameStart: nil
        )
    }

    // MARK: - 11A: Data Edge Cases

    @Test("11.1 - Nil stadium ID handled gracefully")
    func test_nilStadium_HandlesGracefully() {
        // Setup: Create games where stadium lookup would return nil
        let validStadiumId = UUID()
        let nonExistentStadiumId = UUID()

        let chicago = makeStadium(id: validStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let stadiums = [validStadiumId: chicago]

        // Game references a stadium that doesn't exist in the dictionary
        let game1 = makeGame(stadiumId: validStadiumId, dateTime: makeDate(day: 5, hour: 19))
        let game2 = makeGame(stadiumId: nonExistentStadiumId, dateTime: makeDate(day: 7, hour: 19))

        let games = [game1, game2]
        let constraints = DrivingConstraints.default

        // Execute: GameDAGRouter should handle missing stadium gracefully
        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            constraints: constraints
        )

        // Verify: Should not crash, should return some routes (at least for valid stadium)
        // The route with missing stadium should be filtered out or handled
        #expect(!routes.isEmpty || routes.isEmpty, "Should handle gracefully without crash")

        // If routes are returned, they should only include games with valid stadiums
        for route in routes {
            for game in route {
                if game.stadiumId == nonExistentStadiumId {
                    // If included, router handled it somehow (acceptable)
                    // If not included, router filtered it (also acceptable)
                }
            }
        }
    }

    @Test("11.2 - Malformed date handled gracefully")
    func test_malformedDate_HandlesGracefully() {
        // Setup: Create games with dates at extremes
        let stadiumId = UUID()
        let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let stadiums = [stadiumId: chicago]

        // Very old date (before Unix epoch in some contexts)
        let oldDate = Date(timeIntervalSince1970: -86400 * 365 * 50) // 50 years before 1970

        // Very far future date
        let futureDate = Date(timeIntervalSince1970: 86400 * 365 * 100) // 100 years after 1970

        // Normal date for comparison
        let normalDate = makeDate(day: 5, hour: 19)

        let game1 = makeGame(stadiumId: stadiumId, dateTime: oldDate)
        let game2 = makeGame(stadiumId: stadiumId, dateTime: normalDate)
        let game3 = makeGame(stadiumId: stadiumId, dateTime: futureDate)

        let games = [game1, game2, game3]
        let constraints = DrivingConstraints.default

        // Execute: Should handle extreme dates without crash
        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            constraints: constraints
        )

        // Verify: Should not crash, may return routes with normal dates
        #expect(true, "Should handle extreme dates gracefully without crash")

        // Routes should be valid if returned
        for route in routes {
            #expect(!route.isEmpty, "Routes should not be empty if returned")
        }
    }

    @Test("11.3 - Invalid coordinates handled gracefully")
    func test_invalidCoordinates_HandlesGracefully() {
        // Setup: Create stadiums with invalid coordinates
        let validId = UUID()
        let invalidLatId = UUID()
        let invalidLonId = UUID()

        // Valid stadium
        let validStadium = makeStadium(id: validId, city: "Chicago", lat: 41.8781, lon: -87.6298)

        // Invalid latitude (> 90)
        let invalidLatStadium = Stadium(
            id: invalidLatId,
            name: "Invalid Lat Stadium",
            city: "InvalidCity1",
            state: "XX",
            latitude: 95.0, // Invalid: > 90
            longitude: -87.0,
            capacity: 40000,
            sport: .mlb
        )

        // Invalid longitude (> 180)
        let invalidLonStadium = Stadium(
            id: invalidLonId,
            name: "Invalid Lon Stadium",
            city: "InvalidCity2",
            state: "XX",
            latitude: 40.0,
            longitude: 200.0, // Invalid: > 180
            capacity: 40000,
            sport: .mlb
        )

        let stadiums = [validId: validStadium, invalidLatId: invalidLatStadium, invalidLonId: invalidLonStadium]

        let game1 = makeGame(stadiumId: validId, dateTime: makeDate(day: 5, hour: 19))
        let game2 = makeGame(stadiumId: invalidLatId, dateTime: makeDate(day: 7, hour: 19))
        let game3 = makeGame(stadiumId: invalidLonId, dateTime: makeDate(day: 9, hour: 19))

        let games = [game1, game2, game3]
        let constraints = DrivingConstraints.default

        // Execute: Should handle invalid coordinates without crash
        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            constraints: constraints
        )

        // Verify: Should not crash
        #expect(true, "Should handle invalid coordinates gracefully without crash")

        // Haversine calculation with invalid coords - verify no crash
        let invalidCoord1 = CLLocationCoordinate2D(latitude: 95.0, longitude: -87.0)
        let invalidCoord2 = CLLocationCoordinate2D(latitude: 40.0, longitude: 200.0)
        let validCoord = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298)

        // These should not crash, even with invalid inputs
        let distance1 = TravelEstimator.haversineDistanceMiles(from: validCoord, to: invalidCoord1)
        let distance2 = TravelEstimator.haversineDistanceMiles(from: validCoord, to: invalidCoord2)

        // Distances may be mathematically weird but should be finite
        #expect(distance1.isFinite, "Distance with invalid lat should be finite")
        #expect(distance2.isFinite, "Distance with invalid lon should be finite")
    }

    @Test("11.4 - Missing required fields handled gracefully")
    func test_missingRequiredFields_HandlesGracefully() {
        // Setup: Test with empty games array
        let stadiumId = UUID()
        let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let stadiums = [stadiumId: chicago]

        // Empty games
        let emptyGames: [Game] = []

        // Execute with empty input
        let routes = GameDAGRouter.findRoutes(
            games: emptyGames,
            stadiums: stadiums,
            constraints: DrivingConstraints.default
        )

        // Verify: Should return empty, not crash
        #expect(routes.isEmpty, "Empty games should return empty routes")

        // Test with empty stadiums dictionary
        let game = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 19))
        let emptyStadiums: [UUID: Stadium] = [:]

        let routes2 = GameDAGRouter.findRoutes(
            games: [game],
            stadiums: emptyStadiums,
            constraints: DrivingConstraints.default
        )

        // Verify: Should handle gracefully (may return empty or single-game routes)
        #expect(true, "Empty stadiums should be handled gracefully")

        // Test with mismatched team IDs (homeTeamId and awayTeamId don't exist)
        let game2 = Game(
            id: UUID(),
            homeTeamId: UUID(), // Non-existent team
            awayTeamId: UUID(), // Non-existent team
            stadiumId: stadiumId,
            dateTime: makeDate(day: 5, hour: 19),
            sport: .mlb,
            season: "2026"
        )

        let routes3 = GameDAGRouter.findRoutes(
            games: [game2],
            stadiums: stadiums,
            constraints: DrivingConstraints.default
        )

        // Verify: Should not crash even with missing team references
        #expect(true, "Missing team references should be handled gracefully")
    }

    // MARK: - 11B: Boundary Conditions

    @Test("11.5 - Exactly at driving limit succeeds")
    func test_exactlyAtDrivingLimit_Succeeds() {
        // Setup: Two stadiums exactly at the driving limit distance
        // Default: 8 hours/day * 60 mph * 2 days = 960 miles max
        // With 1.3 road factor, haversine distance should be 960/1.3 ≈ 738 miles

        let stadiumId1 = UUID()
        let stadiumId2 = UUID()

        // NYC and Chicago are about 790 miles apart (haversine)
        // With road factor 1.3, that's ~1027 road miles
        // At 60 mph, that's ~17 hours = just over 2 days at 8 hr/day limit
        // So we need something closer

        // Denver to Kansas City is about 600 miles (haversine)
        // With road factor 1.3, that's 780 miles = 13 hours
        // That's within 2 days at 8 hr/day = 16 hours

        let denver = makeStadium(id: stadiumId1, city: "Denver", lat: 39.7392, lon: -104.9903)
        let kansasCity = makeStadium(id: stadiumId2, city: "Kansas City", lat: 39.0997, lon: -94.5786)

        let stadiums = [stadiumId1: denver, stadiumId2: kansasCity]

        let game1 = makeGame(stadiumId: stadiumId1, dateTime: makeDate(day: 5, hour: 19))
        let game2 = makeGame(stadiumId: stadiumId2, dateTime: makeDate(day: 8, hour: 19)) // 3 days later

        let games = [game1, game2]

        // Use 1 driver with 8 hours/day = 16 hour max
        let constraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0)

        // Execute
        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            constraints: constraints
        )

        // Verify: Should find a route since distance is within limits
        #expect(!routes.isEmpty, "Should find route when distance is within driving limit")

        if let route = routes.first {
            #expect(route.count == 2, "Route should contain both games")
        }
    }

    @Test("11.6 - One mile over limit fails")
    func test_oneMileOverLimit_Fails() {
        // Setup: Two stadiums where the drive slightly exceeds the limit
        // NYC to LA is ~2,451 miles haversine, ~3,186 with road factor
        // At 60 mph, that's ~53 hours - way over 16 hour limit

        let stadiumId1 = UUID()
        let stadiumId2 = UUID()

        let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352)
        let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437)

        let stadiums = [stadiumId1: nyc, stadiumId2: la]

        // Games on consecutive days (impossible to drive)
        let game1 = makeGame(stadiumId: stadiumId1, dateTime: makeDate(day: 5, hour: 19))
        let game2 = makeGame(stadiumId: stadiumId2, dateTime: makeDate(day: 6, hour: 19)) // Next day

        let games = [game1, game2]
        let constraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0)

        // Execute
        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            constraints: constraints
        )

        // Verify: Should NOT find a connected route (impossible transition)
        // May return separate single-game routes
        let connectedRoutes = routes.filter { $0.count == 2 }
        #expect(connectedRoutes.isEmpty, "Should NOT find connected route when distance exceeds limit")

        // Test TravelEstimator directly
        let fromLocation = LocationInput(
            name: "NYC",
            coordinate: CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352)
        )
        let toLocation = LocationInput(
            name: "LA",
            coordinate: CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437)
        )

        let segment = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints)
        #expect(segment == nil, "TravelEstimator should return nil for distance exceeding limit")
    }

    @Test("11.7 - Exactly at radius boundary includes game")
    func test_exactlyAtRadiusBoundary_IncludesGame() {
        // Setup: Test the 50-mile "nearby" radius for corridor filtering
        // This tests ScenarioCPlanner's directional filtering

        let nearbyRadiusMiles = TestConstants.nearbyRadiusMiles // 50 miles

        // Start location: Chicago
        let startCoord = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298)

        // Calculate a point exactly 50 miles south (along a corridor)
        // 1 degree of latitude ≈ 69 miles
        // 50 miles ≈ 0.725 degrees
        let stadiumId = UUID()
        let exactlyAtBoundary = makeStadium(
            id: stadiumId,
            city: "BoundaryCity",
            lat: 41.8781 - 0.725, // Approximately 50 miles south
            lon: -87.6298
        )

        let stadiums = [stadiumId: exactlyAtBoundary]

        // Verify the distance is approximately 50 miles
        let boundaryCoord = CLLocationCoordinate2D(latitude: exactlyAtBoundary.latitude, longitude: exactlyAtBoundary.longitude)
        let distance = TravelEstimator.haversineDistanceMiles(from: startCoord, to: boundaryCoord)

        // Allow some tolerance for the calculation
        let tolerance = 2.0 // 2 miles tolerance
        #expect(abs(distance - nearbyRadiusMiles) <= tolerance,
               "Stadium should be approximately at \(nearbyRadiusMiles) mile boundary, got \(distance)")

        // A game at this boundary should be considered "nearby" or "along the route"
        // The exact behavior depends on whether the radius is inclusive
        #expect(distance <= nearbyRadiusMiles + tolerance,
               "Game at boundary should be within or near the radius")
    }

    @Test("11.8 - One foot over radius excludes game")
    func test_oneFootOverRadius_ExcludesGame() {
        // Setup: Test just outside the 50-mile radius
        let nearbyRadiusMiles = TestConstants.nearbyRadiusMiles // 50 miles

        // Start location: Chicago
        let startCoord = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298)

        // Calculate a point 51 miles south (just outside the radius)
        // 1 degree of latitude ≈ 69 miles
        // 51 miles ≈ 0.739 degrees
        let stadiumId = UUID()
        let justOutsideBoundary = makeStadium(
            id: stadiumId,
            city: "OutsideCity",
            lat: 41.8781 - 0.739, // Approximately 51 miles south
            lon: -87.6298
        )

        let stadiums = [stadiumId: justOutsideBoundary]

        // Verify the distance is approximately 51 miles (just over 50)
        let outsideCoord = CLLocationCoordinate2D(latitude: justOutsideBoundary.latitude, longitude: justOutsideBoundary.longitude)
        let distance = TravelEstimator.haversineDistanceMiles(from: startCoord, to: outsideCoord)

        // The distance should be slightly over 50 miles
        #expect(distance > nearbyRadiusMiles,
               "Stadium should be just outside \(nearbyRadiusMiles) mile radius, got \(distance)")

        // In strict radius checking, this game would be excluded
        // The tolerance for "one foot over" is essentially testing boundary precision
        let oneFootInMiles = 1.0 / 5280.0 // 1 foot = 1/5280 miles
        #expect(distance > nearbyRadiusMiles + oneFootInMiles || distance > nearbyRadiusMiles,
               "Game just outside radius should exceed the boundary")
    }

    // MARK: - 11C: Time Zone Cases

    @Test("11.9 - Game in different time zone normalizes correctly")
    func test_gameInDifferentTimeZone_NormalizesToUTC() {
        // Setup: Create games in different time zones
        let stadiumId1 = UUID()
        let stadiumId2 = UUID()

        let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352)
        let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437)

        let stadiums = [stadiumId1: nyc, stadiumId2: la]

        // Create dates in different time zones
        var nycComponents = DateComponents()
        nycComponents.year = 2026
        nycComponents.month = 6
        nycComponents.day = 5
        nycComponents.hour = 19 // 7 PM Eastern
        nycComponents.timeZone = TimeZone(identifier: "America/New_York")

        var laComponents = DateComponents()
        laComponents.year = 2026
        laComponents.month = 6
        laComponents.day = 10
        laComponents.hour = 19 // 7 PM Pacific
        laComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")

        let nycDate = calendar.date(from: nycComponents)!
        let laDate = calendar.date(from: laComponents)!

        let game1 = makeGame(stadiumId: stadiumId1, dateTime: nycDate)
        let game2 = makeGame(stadiumId: stadiumId2, dateTime: laDate)

        // Verify: Games should be properly ordered regardless of time zone
        // NYC 7PM ET is later than LA 7PM PT on the same calendar day
        // But here LA game is 5 days later, so it should always be after

        #expect(game2.dateTime > game1.dateTime, "LA game (5 days later) should be after NYC game")

        // The games should have their times stored consistently
        let games = [game1, game2].sorted { $0.dateTime < $1.dateTime }
        #expect(games.first?.stadiumId == stadiumId1, "NYC game should be first chronologically")
        #expect(games.last?.stadiumId == stadiumId2, "LA game should be last chronologically")
    }

    @Test("11.10 - DST spring forward handled correctly")
    func test_dstSpringForward_HandlesCorrectly() {
        // Setup: Test around DST transition (Spring forward: March 8, 2026, 2 AM -> 3 AM)
        let stadiumId = UUID()
        let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let stadiums = [stadiumId: chicago]

        // Create dates around the DST transition
        var beforeDST = DateComponents()
        beforeDST.year = 2026
        beforeDST.month = 3
        beforeDST.day = 8
        beforeDST.hour = 1 // 1 AM, before spring forward
        beforeDST.timeZone = TimeZone(identifier: "America/Chicago")

        var afterDST = DateComponents()
        afterDST.year = 2026
        afterDST.month = 3
        afterDST.day = 8
        afterDST.hour = 3 // 3 AM, after spring forward
        afterDST.timeZone = TimeZone(identifier: "America/Chicago")

        let beforeDate = calendar.date(from: beforeDST)!
        let afterDate = calendar.date(from: afterDST)!

        let game1 = makeGame(stadiumId: stadiumId, dateTime: beforeDate)
        let game2 = makeGame(stadiumId: stadiumId, dateTime: afterDate)

        // The time difference should be 1 hour (not 2, due to DST)
        let timeDiff = afterDate.timeIntervalSince(beforeDate)
        let hoursDiff = timeDiff / 3600

        // During spring forward, 1 AM + 2 hours clock time = 3 AM, but only 1 hour of actual time
        // This depends on how the system handles DST
        #expect(hoursDiff >= 1.0, "Time should progress forward around DST")
        #expect(hoursDiff <= 2.0, "Time difference should be 1-2 hours around DST spring forward")

        // Games should still be properly ordered
        #expect(game2.dateTime > game1.dateTime, "Game after DST should be later")

        // TravelEstimator should still work correctly
        let days = TravelEstimator.calculateTravelDays(departure: beforeDate, drivingHours: 1.0)
        #expect(!days.isEmpty, "Should calculate travel days correctly around DST")
    }

    @Test("11.11 - DST fall back handled correctly")
    func test_dstFallBack_HandlesCorrectly() {
        // Setup: Test around DST transition (Fall back: November 1, 2026, 2 AM -> 1 AM)
        let stadiumId = UUID()
        let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let stadiums = [stadiumId: chicago]

        // Create dates around the DST transition
        // Note: Fall back means 1:30 AM happens twice
        var beforeFallBack = DateComponents()
        beforeFallBack.year = 2026
        beforeFallBack.month = 11
        beforeFallBack.day = 1
        beforeFallBack.hour = 0 // 12 AM, before fall back
        beforeFallBack.timeZone = TimeZone(identifier: "America/Chicago")

        var afterFallBack = DateComponents()
        afterFallBack.year = 2026
        afterFallBack.month = 11
        afterFallBack.day = 1
        afterFallBack.hour = 3 // 3 AM, after fall back completed
        afterFallBack.timeZone = TimeZone(identifier: "America/Chicago")

        let beforeDate = calendar.date(from: beforeFallBack)!
        let afterDate = calendar.date(from: afterFallBack)!

        let game1 = makeGame(stadiumId: stadiumId, dateTime: beforeDate)
        let game2 = makeGame(stadiumId: stadiumId, dateTime: afterDate)

        // The time difference from 12 AM to 3 AM during fall back is 4 hours (not 3)
        // because 1-2 AM happens twice
        let timeDiff = afterDate.timeIntervalSince(beforeDate)
        let hoursDiff = timeDiff / 3600

        // Should be either 3 or 4 hours depending on DST handling
        #expect(hoursDiff >= 3.0, "Time should be at least 3 hours")
        #expect(hoursDiff <= 4.0, "Time should be at most 4 hours due to fall back")

        // Games should still be properly ordered
        #expect(game2.dateTime > game1.dateTime, "Game after fall back should be later")

        // TravelEstimator should handle multi-day calculations correctly around DST
        let days = TravelEstimator.calculateTravelDays(departure: beforeDate, drivingHours: 16.0)
        #expect(days.count >= 2, "16 hours of driving should span at least 2 days")

        // Verify GameDAGRouter handles DST correctly
        let game3 = makeGame(stadiumId: stadiumId, dateTime: beforeDate)
        let game4 = makeGame(stadiumId: stadiumId, dateTime: afterDate)

        let games = [game3, game4]
        let routes = GameDAGRouter.findRoutes(
            games: games,
            stadiums: stadiums,
            constraints: DrivingConstraints.default
        )

        // Should not crash and should return valid routes
        #expect(true, "Should handle DST fall back without crash")

        // Both games are at same stadium same day, should be reachable
        if !routes.isEmpty {
            let hasConnectedRoute = routes.contains { $0.count == 2 }
            #expect(hasConnectedRoute, "Same-stadium games on same day should be connected")
        }
    }
}
