//
//  ScenarioAPlannerTests.swift
//  SportsTimeTests
//
//  TDD specification tests for ScenarioAPlanner.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("ScenarioAPlanner")
struct ScenarioAPlannerTests {

    // MARK: - Test Data

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

    // Coordinates for testing
    private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
    private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
    private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
    private let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)

    // MARK: - Specification Tests: No Games

    @Test("plan: no games in date range returns noGamesInRange failure")
    func plan_noGamesInRange_returnsFailure() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)

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

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

        let result = planner.plan(request: request)

        guard case .failure(let failure) = result else {
            Issue.record("Expected failure, got success")
            return
        }
        #expect(failure.reason == .noGamesInRange)
    }

    @Test("plan: games outside date range returns noGamesInRange")
    func plan_gamesOutsideDateRange_returnsNoGamesInRange() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)

        // Game is after the date range
        let gameDate = endDate.addingTimeInterval(86400 * 30)

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

        let prefs = TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            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 .failure(let failure) = result else {
            Issue.record("Expected failure, got success")
            return
        }
        #expect(failure.reason == .noGamesInRange)
    }

    // MARK: - Specification Tests: Region Filtering

    @Test("plan: with selectedRegions filters to those regions")
    func plan_withSelectedRegions_filtersGames() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)
        let gameDate = startDate.addingTimeInterval(86400 * 2)

        // NYC stadium (East coast)
        let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
        // LA stadium (West coast)
        let laStadium = makeStadium(id: "la", city: "Los Angeles", coordinate: laCoord)

        let nycGame = makeGame(id: "game-nyc", stadiumId: "nyc", dateTime: gameDate)
        let laGame = makeGame(id: "game-la", stadiumId: "la", dateTime: gameDate.addingTimeInterval(86400))

        var prefs = TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 1
        )
        prefs.selectedRegions = [.east]  // Only East coast

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

        let result = planner.plan(request: request)

        // Should succeed with only NYC game (East coast)
        guard case .success(let options) = result else {
            // May fail for other reasons (no valid routes), but shouldn't include LA
            return
        }

        // If success, verify only East coast games included
        let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
        #expect(!allGameIds.contains("game-la"), "LA game should be filtered out by East region filter")
    }

    // MARK: - Specification Tests: Must-Stop Filtering

    @Test("plan: with mustStopLocation filters to that city")
    func plan_withMustStopLocation_filtersToCity() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 14)
        let gameDate = startDate.addingTimeInterval(86400 * 2)

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

        let nycGame = makeGame(id: "game-nyc", stadiumId: "nyc", dateTime: gameDate)
        let bostonGame = makeGame(id: "game-boston", stadiumId: "boston", dateTime: gameDate.addingTimeInterval(86400))

        let prefs = TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            mustStopLocations: [LocationInput(name: "New York", coordinate: nycCoord)],
            lodgingType: .hotel,
            numberOfDrivers: 1
        )

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

        let result = planner.plan(request: request)

        // If success, should only include NYC games
        if case .success(let options) = result {
            let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
            #expect(allGameIds.contains("game-nyc"), "NYC game should be included")
            // Boston game may or may not be included depending on route logic
        }
        // Could also fail with noGamesInRange if must-stop filter is strict
    }

    @Test("plan: mustStopLocation with no games in that city returns noGamesInRange")
    func plan_mustStopNoGamesInCity_returnsNoGamesInRange() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)
        let gameDate = startDate.addingTimeInterval(86400 * 2)

        let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
        let bostonGame = makeGame(id: "game-boston", stadiumId: "boston", dateTime: gameDate)

        // Must stop in Chicago, but no games there
        let prefs = TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            mustStopLocations: [LocationInput(name: "Chicago", coordinate: chicagoCoord)],
            lodgingType: .hotel,
            numberOfDrivers: 1
        )

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

        let result = planner.plan(request: request)

        guard case .failure(let failure) = result else {
            Issue.record("Expected failure when must-stop city has no games")
            return
        }
        #expect(failure.reason == .noGamesInRange)
    }

    // MARK: - Specification Tests: Successful Planning

    @Test("plan: single game in range returns success with one option")
    func plan_singleGame_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: .dateRange,
            sports: [.mlb],
            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 game")
            return
        }
        #expect(!options.isEmpty)
        #expect(options.first?.stops.first?.games.contains("game1") == true)
    }

    @Test("plan: multiple games at same stadium creates single stop")
    func plan_multipleGamesAtSameStadium_createsSingleStop() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)

        let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
        let game1 = makeGame(id: "game1", stadiumId: "stadium1", dateTime: startDate.addingTimeInterval(86400))
        let game2 = makeGame(id: "game2", stadiumId: "stadium1", dateTime: startDate.addingTimeInterval(86400 * 2))

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

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

        let result = planner.plan(request: request)

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

        // Both games at same stadium should be grouped into one stop
        if let firstOption = options.first {
            let nycStops = firstOption.stops.filter { $0.city == "New York" }
            // Should have 1 stop with 2 games (not 2 stops)
            let totalGamesInNYC = nycStops.flatMap { $0.games }.count
            #expect(totalGamesInNYC >= 2, "Both games should be in the route")
        }
    }

    // MARK: - Invariant Tests

    @Test("Invariant: returned games are within date range")
    func invariant_returnedGamesWithinDateRange() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)

        let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
        let gameInRange = makeGame(id: "in-range", stadiumId: "stadium1", dateTime: startDate.addingTimeInterval(86400 * 2))
        let gameOutOfRange = makeGame(id: "out-of-range", stadiumId: "stadium1", dateTime: endDate.addingTimeInterval(86400 * 10))

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

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

        let result = planner.plan(request: request)

        if case .success(let options) = result {
            let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
            #expect(allGameIds.contains("in-range"))
            #expect(!allGameIds.contains("out-of-range"), "Game outside date range should not be included")
        }
    }

    @Test("Invariant: A-B-A creates 3 stops not 2")
    func invariant_visitSameCityTwice_createsThreeStops() {
        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)

        // NYC -> Boston -> NYC sequence
        let game1 = makeGame(id: "nyc1", stadiumId: "nyc", dateTime: startDate.addingTimeInterval(86400))
        let game2 = makeGame(id: "boston1", stadiumId: "boston", dateTime: startDate.addingTimeInterval(86400 * 3))
        let game3 = makeGame(id: "nyc2", stadiumId: "nyc", dateTime: startDate.addingTimeInterval(86400 * 5))

        var prefs = TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            startDate: startDate,
            endDate: endDate,
            leisureLevel: .moderate,
            lodgingType: .hotel,
            numberOfDrivers: 2  // More drivers to ensure feasibility
        )
        prefs.allowRepeatCities = true

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

        let result = planner.plan(request: request)

        if case .success(let options) = result {
            // Look for an option that includes all 3 games
            let optionWithAllGames = options.first { option in
                let allGames = option.stops.flatMap { $0.games }
                return allGames.contains("nyc1") && allGames.contains("boston1") && allGames.contains("nyc2")
            }

            if let option = optionWithAllGames {
                // NYC appears first and last, so should have at least 3 stops
                #expect(option.stops.count >= 3, "A-B-A pattern should create 3 stops")
            }
        }
    }

    // MARK: - Property Tests

    @Test("Property: success always has non-empty options")
    func property_successHasNonEmptyOptions() {
        let startDate = Date()
        let endDate = startDate.addingTimeInterval(86400 * 7)

        let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
        let game = makeGame(id: "game1", stadiumId: "stadium1", dateTime: startDate.addingTimeInterval(86400 * 2))

        let prefs = TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            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 at least one option")
            for option in options {
                #expect(!option.stops.isEmpty, "Each option must have at least one stop")
            }
        }
    }

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