//
//  ScenarioBPlannerTests.swift
//  SportsTimeTests
//
//  TDD specification tests for ScenarioBPlanner.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("ScenarioBPlanner")
struct ScenarioBPlannerTests {

    // MARK: - Test Data

    private let planner = ScenarioBPlanner()

    private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
    private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
    private let phillyCoord = CLLocationCoordinate2D(latitude: 39.9526, longitude: -75.1652)

    // MARK: - Specification Tests: No Selected Games

    @Test("plan: no selected games returns failure")
    func plan_noSelectedGames_returnsFailure() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: [],  // No selected games
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 1
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [],
            teams: [:],
            stadiums: [:]
        )

        let result = planner.plan(request: request)

        guard case .failure(let failure) = result else {
            Issue.record("Expected failure when no games selected")
            return
        }
        #expect(failure.reason == .noValidRoutes)
    }

    // MARK: - Specification Tests: Anchor Games

    @Test("plan: single selected game returns success with that game")
    func plan_singleSelectedGame_returnsSuccess() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)
        let gameDate = startDate.addingTimeInterval(86400 * 2)

        let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
        let game = makeGame(id: "game1", stadiumId: "stadium1", dateTime: gameDate)

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["game1"],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 1
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [game],
            teams: [:],
            stadiums: ["stadium1": stadium]
        )

        let result = planner.plan(request: request)

        guard case .success(let options) = result else {
            Issue.record("Expected success with single selected game")
            return
        }

        #expect(!options.isEmpty)
        let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
        #expect(allGameIds.contains("game1"), "Selected game must be in result")
    }

    @Test("plan: all selected games appear in every route")
    func plan_allSelectedGamesAppearInRoutes() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 10)

        let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
        let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
        let phillyStadium = makeStadium(id: "philly", city: "Philadelphia", coordinate: phillyCoord)

        let nycGame = makeGame(id: "nyc-game", stadiumId: "nyc", dateTime: startDate.addingTimeInterval(86400))
        let bostonGame = makeGame(id: "boston-game", stadiumId: "boston", dateTime: startDate.addingTimeInterval(86400 * 3))
        let phillyGame = makeGame(id: "philly-game", stadiumId: "philly", dateTime: startDate.addingTimeInterval(86400 * 5))

        // Select NYC and Boston games as anchors
        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["nyc-game", "boston-game"],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 2
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [nycGame, bostonGame, phillyGame],
            teams: [:],
            stadiums: ["nyc": nycStadium, "boston": bostonStadium, "philly": phillyStadium]
        )

        let result = planner.plan(request: request)

        if case .success(let options) = result {
            for option in options {
                let gameIds = option.stops.flatMap { $0.games }
                #expect(gameIds.contains("nyc-game"), "Every route must contain selected NYC game")
                #expect(gameIds.contains("boston-game"), "Every route must contain selected Boston game")
            }
        }
    }

    // MARK: - Regression Tests: Bonus Games in Date Range

    @Test("plan: gameFirst mode includes bonus games within date range")
    func plan_gameFirstMode_includesBonusGamesInDateRange() {
        // Regression test: When a single anchor game is selected, the planner should
        // find additional "bonus" games within the date range that fit geographically.
        // Bug: planTrip() was overriding the 7-day date range with just anchor dates,
        // causing only the anchor game to appear in results.

        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)  // 7-day span

        // NYC and Boston are geographically close (drivable)
        let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
        let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)

        // Anchor game on day 4
        let anchorGame = makeGame(id: "anchor-game", stadiumId: "nyc", dateTime: startDate.addingTimeInterval(86400 * 3))
        // Bonus game on day 2 (within date range, geographically sensible)
        let bonusGame = makeGame(id: "bonus-game", stadiumId: "boston", dateTime: startDate.addingTimeInterval(86400 * 1))

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["anchor-game"],  // Only anchor is selected
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 2
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [anchorGame, bonusGame],  // Both games available
            teams: [:],
            stadiums: ["nyc": nycStadium, "boston": bostonStadium]
        )

        let result = planner.plan(request: request)

        guard case .success(let options) = result else {
            Issue.record("Expected success with bonus game available")
            return
        }

        #expect(!options.isEmpty, "Should have trip options")

        // At least one option should include the bonus game
        let optionsWithBonus = options.filter { option in
            option.stops.flatMap { $0.games }.contains("bonus-game")
        }

        #expect(!optionsWithBonus.isEmpty, "At least one route should include bonus game from date range")

        // ALL options must still contain the anchor game
        for option in options {
            let gameIds = option.stops.flatMap { $0.games }
            #expect(gameIds.contains("anchor-game"), "Anchor game must be in every route")
        }
    }

    @Test("plan: gameFirst mode uses full date range not just anchor dates")
    func plan_gameFirstMode_usesFullDateRange() {
        // Regression test: Verify that the planner considers games across the entire
        // date range, not just on the anchor game dates.

        let startDate = Date()

        // 7-day date range
        let day1 = startDate
        let day3 = startDate.addingTimeInterval(86400 * 2)
        let day4 = startDate.addingTimeInterval(86400 * 3)  // Anchor game day
        let day6 = startDate.addingTimeInterval(86400 * 5)
        let endDate = startDate.addingTimeInterval(86400 * 7)

        let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
        let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
        let phillyStadium = makeStadium(id: "philly", city: "Philadelphia", coordinate: phillyCoord)

        // Anchor game on day 4
        let anchorGame = makeGame(id: "anchor", stadiumId: "nyc", dateTime: day4)
        // Games on other days
        let day1Game = makeGame(id: "day1-game", stadiumId: "philly", dateTime: day1)
        let day3Game = makeGame(id: "day3-game", stadiumId: "boston", dateTime: day3)
        let day6Game = makeGame(id: "day6-game", stadiumId: "philly", dateTime: day6)

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["anchor"],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 2
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [anchorGame, day1Game, day3Game, day6Game],
            teams: [:],
            stadiums: ["nyc": nycStadium, "boston": bostonStadium, "philly": phillyStadium]
        )

        let result = planner.plan(request: request)

        guard case .success(let options) = result else {
            Issue.record("Expected success")
            return
        }

        // Collect all game IDs across all options
        let allGameIdsInOptions = Set(options.flatMap { $0.stops.flatMap { $0.games } })

        // At least some non-anchor games should appear in the results
        // (we don't require ALL because geographic constraints may exclude some)
        let bonusGamesFound = allGameIdsInOptions.subtracting(["anchor"])
        #expect(!bonusGamesFound.isEmpty, "Planner should find bonus games from full date range, not just anchor date")
    }

    // MARK: - Specification Tests: Sliding Window

    @Test("plan: gameFirst mode uses sliding window")
    func plan_gameFirstMode_usesSlidingWindow() {
        let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)

        // Game on a specific date
        let gameDate = Date().addingTimeInterval(86400 * 5)
        let game = makeGame(id: "game1", stadiumId: "stadium1", dateTime: gameDate)

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["game1"],
            startDate: Date(),
            endDate: Date().addingTimeInterval(86400 * 30),
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 1,
            gameFirstTripDuration: 7  // 7-day trip
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [game],
            teams: [:],
            stadiums: ["stadium1": stadium]
        )

        let result = planner.plan(request: request)

        // Should succeed even without explicit dates because of sliding window
        if case .success(let options) = result {
            #expect(!options.isEmpty)
        }
        // May also fail if no valid date ranges, which is acceptable
    }

    // MARK: - Specification Tests: Arrival Time Validation

    @Test("plan: uses arrivalBeforeGameStart validator")
    func plan_usesArrivalValidator() {
        // This test verifies that ScenarioB uses arrival time validation
        // by creating a scenario where travel time makes arrival impossible

        let now = Date()
        let game1Date = now.addingTimeInterval(86400)  // Tomorrow
        let game2Date = now.addingTimeInterval(86400 + 3600)  // Tomorrow + 1 hour (impossible to drive from coast to coast)

        // NYC to LA is ~40 hours of driving
        let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
        let laStadium = makeStadium(id: "la", city: "Los Angeles", coordinate: CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673))

        let nycGame = makeGame(id: "nyc-game", stadiumId: "nyc", dateTime: game1Date)
        let laGame = makeGame(id: "la-game", stadiumId: "la", dateTime: game2Date)

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["nyc-game", "la-game"],
            startDate: now,
            endDate: now.addingTimeInterval(86400 * 7),
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 1
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [nycGame, laGame],
            teams: [:],
            stadiums: ["nyc": nycStadium, "la": laStadium]
        )

        let result = planner.plan(request: request)

        // Should fail because it's impossible to arrive in LA 1 hour after leaving NYC
        guard case .failure = result else {
            Issue.record("Expected failure when travel time makes arrival impossible")
            return
        }
    }

    // MARK: - Invariant Tests

    @Test("Invariant: selected games cannot be dropped")
    func invariant_selectedGamesCannotBeDropped() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 14)

        let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
        let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)

        let nycGame = makeGame(id: "nyc-anchor", stadiumId: "nyc", dateTime: startDate.addingTimeInterval(86400 * 2))
        let bostonGame = makeGame(id: "boston-anchor", stadiumId: "boston", dateTime: startDate.addingTimeInterval(86400 * 5))

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["nyc-anchor", "boston-anchor"],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 2
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [nycGame, bostonGame],
            teams: [:],
            stadiums: ["nyc": nycStadium, "boston": bostonStadium]
        )

        let result = planner.plan(request: request)

        if case .success(let options) = result {
            for option in options {
                let gameIds = Set(option.stops.flatMap { $0.games })
                #expect(gameIds.contains("nyc-anchor"), "Anchor game cannot be dropped: nyc-anchor")
                #expect(gameIds.contains("boston-anchor"), "Anchor game cannot be dropped: boston-anchor")
            }
        }
    }

    // MARK: - Property Tests

    @Test("Property: success with selected games includes all anchors")
    func property_successIncludesAllAnchors() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)
        let gameDate = startDate.addingTimeInterval(86400 * 2)

        let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
        let game = makeGame(id: "anchor1", stadiumId: "stadium1", dateTime: gameDate)

        let prefs = TripPreferences(
            planningMode: .gameFirst,
            sports: [.mlb],
            mustSeeGameIds: ["anchor1"],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 1
        )

        let request = PlanningRequest(
            preferences: prefs,
            availableGames: [game],
            teams: [:],
            stadiums: ["stadium1": stadium]
        )

        let result = planner.plan(request: request)

        if case .success(let options) = result {
            #expect(!options.isEmpty, "Success must have options")
            for option in options {
                let allGames = option.stops.flatMap { $0.games }
                #expect(allGames.contains("anchor1"), "Every option must include anchor")
            }
        }
    }

    // MARK: - Helper Methods

    private func makeStadium(
        id: String,
        city: String,
        coordinate: CLLocationCoordinate2D
    ) -> Stadium {
        Stadium(
            id: id,
            name: "\(city) Stadium",
            city: city,
            state: "XX",
            latitude: coordinate.latitude,
            longitude: coordinate.longitude,
            capacity: 40000,
            sport: .mlb
        )
    }

    private func makeGame(
        id: String,
        stadiumId: String,
        dateTime: Date
    ) -> Game {
        Game(
            id: id,
            homeTeamId: "team1",
            awayTeamId: "team2",
            stadiumId: stadiumId,
            dateTime: dateTime,
            sport: .mlb,
            season: "2026",
            isPlayoff: false
        )
    }
}
