//
//  ScenarioBPlannerTests.swift
//  SportsTimeTests
//
//  Phase 5: ScenarioBPlanner Tests
//  Scenario B: User selects specific games (must-see), planner builds route.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("ScenarioBPlanner Tests", .serialized)
struct ScenarioBPlannerTests {

    // MARK: - Test Fixtures

    private let calendar = Calendar.current
    private let planner = ScenarioBPlanner()

    /// Creates a date with specific year/month/day/hour
    private func makeDate(year: Int = 2026, month: Int = 6, day: Int, hour: Int = 19) -> Date {
        var components = DateComponents()
        components.year = year
        components.month = month
        components.day = day
        components.hour = hour
        components.minute = 0
        return calendar.date(from: components)!
    }

    /// Creates a stadium at a known location
    private func makeStadium(
        id: UUID = UUID(),
        city: String,
        lat: Double,
        lon: Double,
        sport: Sport = .mlb
    ) -> Stadium {
        Stadium(
            id: id,
            name: "\(city) Stadium",
            city: city,
            state: "ST",
            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 a PlanningRequest for Scenario B (must-see games mode)
    private func makePlanningRequest(
        startDate: Date,
        endDate: Date,
        allGames: [Game],
        mustSeeGameIds: Set<UUID>,
        stadiums: [UUID: Stadium],
        teams: [UUID: Team] = [:],
        allowRepeatCities: Bool = true,
        numberOfDrivers: Int = 1,
        maxDrivingHoursPerDriver: Double = 8.0
    ) -> PlanningRequest {
        let preferences = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: mustSeeGameIds,
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            numberOfDrivers: numberOfDrivers,
            maxDrivingHoursPerDriver: maxDrivingHoursPerDriver,
            allowRepeatCities: allowRepeatCities
        )

        return PlanningRequest(
            preferences: preferences,
            availableGames: allGames,
            teams: teams,
            stadiums: stadiums
        )
    }

    // MARK: - 5A: Valid Inputs

    @Test("5.1 - Single must-see game returns trip with that game")
    func test_mustSeeGames_SingleGame_ReturnsTripWithThatGame() {
        // Setup: Single must-see game
        let stadiumId = UUID()
        let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let stadiums = [stadiumId: stadium]

        let gameId = UUID()
        let game = makeGame(id: gameId, stadiumId: stadiumId, dateTime: makeDate(day: 10, hour: 19))

        let request = makePlanningRequest(
            startDate: makeDate(day: 5, hour: 0),
            endDate: makeDate(day: 15, hour: 23),
            allGames: [game],
            mustSeeGameIds: [gameId],
            stadiums: stadiums
        )

        // Execute
        let result = planner.plan(request: request)

        // Verify
        #expect(result.isSuccess, "Should succeed with single must-see game")
        #expect(!result.options.isEmpty, "Should return at least one option")

        if let firstOption = result.options.first {
            #expect(firstOption.totalGames >= 1, "Should have at least the must-see game")
            let allGameIds = firstOption.stops.flatMap { $0.games }
            #expect(allGameIds.contains(gameId), "Must-see game must be in the itinerary")
        }
    }

    @Test("5.2 - Multiple must-see games returns optimal route")
    func test_mustSeeGames_MultipleGames_ReturnsOptimalRoute() {
        // Setup: 3 must-see games in nearby cities (all Central region for single-region search)
        // Region boundary: Central is -110 to -85 longitude
        let chicagoId = UUID()
        let milwaukeeId = UUID()
        let stLouisId = UUID()

        // All cities in Central region (longitude between -110 and -85)
        let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
        let stLouis = makeStadium(id: stLouisId, city: "St. Louis", lat: 38.6270, lon: -90.1994)

        let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, stLouisId: stLouis]

        let game1Id = UUID()
        let game2Id = UUID()
        let game3Id = UUID()

        let game1 = makeGame(id: game1Id, stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
        let game2 = makeGame(id: game2Id, stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19))
        let game3 = makeGame(id: game3Id, stadiumId: stLouisId, dateTime: makeDate(day: 9, hour: 19))

        let request = makePlanningRequest(
            startDate: makeDate(day: 4, hour: 0),
            endDate: makeDate(day: 10, hour: 23),
            allGames: [game1, game2, game3],
            mustSeeGameIds: [game1Id, game2Id, game3Id],
            stadiums: stadiums
        )

        // Execute
        let result = planner.plan(request: request)

        // Verify
        #expect(result.isSuccess, "Should succeed with multiple must-see games")
        #expect(!result.options.isEmpty, "Should return at least one option")

        if let firstOption = result.options.first {
            let allGameIds = Set(firstOption.stops.flatMap { $0.games })
            #expect(allGameIds.contains(game1Id), "Must include game 1")
            #expect(allGameIds.contains(game2Id), "Must include game 2")
            #expect(allGameIds.contains(game3Id), "Must include game 3")

            // Route should be in chronological order (respecting game times)
            #expect(firstOption.stops.count >= 3, "Should have at least 3 stops for 3 games in different cities")
        }
    }

    @Test("5.3 - Games in different cities are connected")
    func test_mustSeeGames_GamesInDifferentCities_ConnectsThem() {
        // Setup: 2 must-see games in distant but reachable cities
        let nycId = UUID()
        let bostonId = UUID()

        // NYC to Boston is ~215 miles (~4 hours driving)
        let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
        let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)

        let stadiums = [nycId: nyc, bostonId: boston]

        let game1Id = UUID()
        let game2Id = UUID()

        // Games 2 days apart - plenty of time to drive
        let game1 = makeGame(id: game1Id, stadiumId: nycId, dateTime: makeDate(day: 5, hour: 19))
        let game2 = makeGame(id: game2Id, stadiumId: bostonId, dateTime: makeDate(day: 7, hour: 19))

        let request = makePlanningRequest(
            startDate: makeDate(day: 4, hour: 0),
            endDate: makeDate(day: 8, hour: 23),
            allGames: [game1, game2],
            mustSeeGameIds: [game1Id, game2Id],
            stadiums: stadiums
        )

        // Execute
        let result = planner.plan(request: request)

        // Verify
        #expect(result.isSuccess, "Should succeed connecting NYC and Boston")
        #expect(!result.options.isEmpty, "Should return at least one option")

        if let firstOption = result.options.first {
            let allGameIds = Set(firstOption.stops.flatMap { $0.games })
            #expect(allGameIds.contains(game1Id), "Must include NYC game")
            #expect(allGameIds.contains(game2Id), "Must include Boston game")

            // Should have travel segment between cities
            #expect(firstOption.travelSegments.count >= 1, "Should have travel segment(s)")

            // Verify cities are connected in the route
            let cities = firstOption.stops.map { $0.city }
            #expect(cities.contains("New York"), "Route should include New York")
            #expect(cities.contains("Boston"), "Route should include Boston")
        }
    }

    // MARK: - 5B: Edge Cases

    @Test("5.4 - Empty selection returns failure")
    func test_mustSeeGames_EmptySelection_ThrowsError() {
        // Setup: No must-see games selected
        let stadiumId = UUID()
        let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
        let stadiums = [stadiumId: stadium]

        let game = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 10, hour: 19))

        // Empty must-see set
        let request = makePlanningRequest(
            startDate: makeDate(day: 5, hour: 0),
            endDate: makeDate(day: 15, hour: 23),
            allGames: [game],
            mustSeeGameIds: [],  // Empty selection
            stadiums: stadiums
        )

        // Execute
        let result = planner.plan(request: request)

        // Verify: Should fail with appropriate error
        #expect(!result.isSuccess, "Should fail when no games selected")
        #expect(result.failure?.reason == .noValidRoutes,
               "Should return noValidRoutes (no selected games)")
    }

    @Test("5.5 - Impossible to connect games returns failure")
    func test_mustSeeGames_ImpossibleToConnect_ThrowsError() {
        // Setup: Games on same day in cities ~850 miles apart (impossible in 8 hours)
        // Both cities in East region (> -85 longitude) so regional search covers both
        let nycId = UUID()
        let atlantaId = UUID()

        // NYC to Atlanta is ~850 miles (~13 hours driving) - impossible with 8-hour limit
        let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
        let atlanta = makeStadium(id: atlantaId, city: "Atlanta", lat: 33.7490, lon: -84.3880)

        let stadiums = [nycId: nyc, atlantaId: atlanta]

        let game1Id = UUID()
        let game2Id = UUID()

        // Same day games 6 hours apart - even if you left right after game 1,
        // you can't drive 850 miles in 6 hours with 8-hour daily limit
        let game1 = makeGame(id: game1Id, stadiumId: nycId, dateTime: makeDate(day: 5, hour: 13))
        let game2 = makeGame(id: game2Id, stadiumId: atlantaId, dateTime: makeDate(day: 5, hour: 19))

        let request = makePlanningRequest(
            startDate: makeDate(day: 5, hour: 0),
            endDate: makeDate(day: 5, hour: 23),
            allGames: [game1, game2],
            mustSeeGameIds: [game1Id, game2Id],
            stadiums: stadiums,
            numberOfDrivers: 1,
            maxDrivingHoursPerDriver: 8.0
        )

        // Execute
        let result = planner.plan(request: request)

        // Verify: Should fail because it's impossible to connect these games
        // The planner should not find any valid route containing BOTH must-see games
        #expect(!result.isSuccess, "Should fail when games are impossible to connect")
        // Either noValidRoutes or constraintsUnsatisfiable are acceptable
        let validFailureReasons: [PlanningFailure.FailureReason] = [.noValidRoutes, .constraintsUnsatisfiable]
        #expect(validFailureReasons.contains(result.failure?.reason ?? .noValidRoutes),
               "Should return appropriate failure reason")
    }

    @Test("5.6 - Max games selected handles gracefully", .timeLimit(.minutes(5)))
    func test_mustSeeGames_MaxGamesSelected_HandlesGracefully() {
        // Setup: Generate many games and select a large subset
        let config = FixtureGenerator.Configuration(
            seed: 42,
            gameCount: 500,
            stadiumCount: 30,
            teamCount: 60,
            dateRange: makeDate(day: 1, hour: 0)...makeDate(month: 8, day: 31, hour: 23),
            geographicSpread: .regional  // Keep games in one region for feasibility
        )
        let data = FixtureGenerator.generate(with: config)

        // Select 50 games as must-see (a stress test for the planner)
        let mustSeeGames = Array(data.games.prefix(50))
        let mustSeeIds = Set(mustSeeGames.map { $0.id })

        let request = makePlanningRequest(
            startDate: makeDate(day: 1, hour: 0),
            endDate: makeDate(month: 8, day: 31, hour: 23),
            allGames: data.games,
            mustSeeGameIds: mustSeeIds,
            stadiums: data.stadiumsById
        )

        // Execute with timing
        let startTime = Date()
        let result = planner.plan(request: request)
        let elapsed = Date().timeIntervalSince(startTime)

        // Verify: Should complete without crash/hang
        #expect(elapsed < TestConstants.performanceTimeout,
               "Should complete within performance timeout")

        // Result could be success or failure depending on feasibility
        // The key is that it doesn't crash or hang
        if result.isSuccess {
            // If successful, verify anchor games are included where possible
            if let firstOption = result.options.first {
                let includedGames = Set(firstOption.stops.flatMap { $0.games })
                let includedMustSee = includedGames.intersection(mustSeeIds)
                // Some must-see games should be included
                #expect(!includedMustSee.isEmpty, "Should include some must-see games")
            }
        }
        // Failure is also acceptable for extreme constraints
    }

    // MARK: - 5C: Optimality Verification

    @Test("5.7 - Small input matches brute force optimal")
    func test_mustSeeGames_SmallInput_MatchesBruteForceOptimal() {
        // Setup: 5 must-see games (within brute force threshold of 8)
        // All cities in East region (> -85 longitude) for single-region search
        // Geographic progression from north to south along the East Coast
        let boston = makeStadium(city: "Boston", lat: 42.3601, lon: -71.0589)
        let nyc = makeStadium(city: "New York", lat: 40.7128, lon: -73.9352)
        let philadelphia = makeStadium(city: "Philadelphia", lat: 39.9526, lon: -75.1652)
        let baltimore = makeStadium(city: "Baltimore", lat: 39.2904, lon: -76.6122)
        let dc = makeStadium(city: "Washington DC", lat: 38.9072, lon: -77.0369)

        let stadiums = [
            boston.id: boston,
            nyc.id: nyc,
            philadelphia.id: philadelphia,
            baltimore.id: baltimore,
            dc.id: dc
        ]

        // Games spread over 2 weeks with clear geographic progression
        let game1 = makeGame(stadiumId: boston.id, dateTime: makeDate(day: 1, hour: 19))
        let game2 = makeGame(stadiumId: nyc.id, dateTime: makeDate(day: 3, hour: 19))
        let game3 = makeGame(stadiumId: philadelphia.id, dateTime: makeDate(day: 6, hour: 19))
        let game4 = makeGame(stadiumId: baltimore.id, dateTime: makeDate(day: 9, hour: 19))
        let game5 = makeGame(stadiumId: dc.id, dateTime: makeDate(day: 12, hour: 19))

        let allGames = [game1, game2, game3, game4, game5]
        let mustSeeIds = Set(allGames.map { $0.id })

        let request = makePlanningRequest(
            startDate: makeDate(day: 1, hour: 0),
            endDate: makeDate(day: 15, hour: 23),
            allGames: allGames,
            mustSeeGameIds: mustSeeIds,
            stadiums: stadiums
        )

        // Execute
        let result = planner.plan(request: request)

        // Verify success
        #expect(result.isSuccess, "Should succeed with 5 must-see games")
        guard let firstOption = result.options.first else {
            Issue.record("No options returned")
            return
        }

        // Verify all must-see games are included
        let includedGameIds = Set(firstOption.stops.flatMap { $0.games })
        for gameId in mustSeeIds {
            #expect(includedGameIds.contains(gameId), "All must-see games should be included")
        }

        // Build coordinate map for brute force verification
        var stopCoordinates: [UUID: CLLocationCoordinate2D] = [:]
        for stop in firstOption.stops {
            if let coord = stop.coordinate {
                stopCoordinates[stop.id] = coord
            }
        }

        // Only verify if we have enough stops with coordinates
        guard stopCoordinates.count >= 2 && stopCoordinates.count <= TestConstants.bruteForceMaxStops else {
            return
        }

        let stopIds = firstOption.stops.map { $0.id }
        let verificationResult = BruteForceRouteVerifier.verify(
            proposedRoute: stopIds,
            stops: stopCoordinates,
            tolerance: 0.15  // 15% tolerance for heuristic algorithms
        )

        let message = verificationResult.failureMessage ?? "Route should be near-optimal"
        #expect(verificationResult.isOptimal, Comment(rawValue: message))
    }

    @Test("5.8 - Large input has no obviously better route")
    func test_mustSeeGames_LargeInput_NoObviouslyBetterRoute() {
        // Setup: Generate more games than brute force can handle
        let config = FixtureGenerator.Configuration(
            seed: 123,
            gameCount: 200,
            stadiumCount: 20,
            teamCount: 40,
            dateRange: makeDate(day: 1, hour: 0)...makeDate(month: 7, day: 31, hour: 23),
            geographicSpread: .regional
        )
        let data = FixtureGenerator.generate(with: config)

        // Select 15 games as must-see (more than brute force threshold)
        let mustSeeGames = Array(data.games.prefix(15))
        let mustSeeIds = Set(mustSeeGames.map { $0.id })

        let request = makePlanningRequest(
            startDate: makeDate(day: 1, hour: 0),
            endDate: makeDate(month: 7, day: 31, hour: 23),
            allGames: data.games,
            mustSeeGameIds: mustSeeIds,
            stadiums: data.stadiumsById
        )

        // Execute
        let result = planner.plan(request: request)

        // If planning fails, that's acceptable for complex constraints
        guard result.isSuccess, let firstOption = result.options.first else {
            return
        }

        // Verify some must-see games are included
        let includedGameIds = Set(firstOption.stops.flatMap { $0.games })
        let includedMustSee = includedGameIds.intersection(mustSeeIds)
        #expect(!includedMustSee.isEmpty, "Should include some must-see games")

        // Build coordinate map
        var stopCoordinates: [UUID: CLLocationCoordinate2D] = [:]
        for stop in firstOption.stops {
            if let coord = stop.coordinate {
                stopCoordinates[stop.id] = coord
            }
        }

        // Check that there's no obviously better route (10% threshold)
        guard stopCoordinates.count >= 2 else { return }

        let stopIds = firstOption.stops.map { $0.id }
        let (hasBetter, improvement) = BruteForceRouteVerifier.hasObviouslyBetterRoute(
            proposedRoute: stopIds,
            stops: stopCoordinates,
            threshold: 0.10  // 10% improvement would be "obviously better"
        )

        if hasBetter, let imp = improvement {
            // Only fail if the improvement is very significant
            #expect(imp < 0.25, "Route should not be more than 25% suboptimal")
        }
    }
}
