//
//  PlanningModelsTests.swift
//  SportsTimeTests
//
//  TDD specification + property tests for PlanningModels.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("PlanningModels")
struct PlanningModelsTests {

    // MARK: - DrivingConstraints Tests

    @Suite("DrivingConstraints")
    struct DrivingConstraintsTests {

        @Test("default has 1 driver and 8 hours per day")
        func defaultConstraints() {
            let constraints = DrivingConstraints.default

            #expect(constraints.numberOfDrivers == 1)
            #expect(constraints.maxHoursPerDriverPerDay == 8.0)
            #expect(constraints.maxDailyDrivingHours == 8.0)
        }

        @Test("maxDailyDrivingHours equals drivers times hours")
        func maxDailyDrivingHours_calculation() {
            let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
            #expect(twoDrivers.maxDailyDrivingHours == 16.0)

            let threeLongDrivers = DrivingConstraints(numberOfDrivers: 3, maxHoursPerDriverPerDay: 10.0)
            #expect(threeLongDrivers.maxDailyDrivingHours == 30.0)
        }

        @Test("numberOfDrivers clamped to minimum 1")
        func numberOfDrivers_clampedToOne() {
            let zeroDrivers = DrivingConstraints(numberOfDrivers: 0, maxHoursPerDriverPerDay: 8.0)
            #expect(zeroDrivers.numberOfDrivers == 1)

            let negativeDrivers = DrivingConstraints(numberOfDrivers: -5, maxHoursPerDriverPerDay: 8.0)
            #expect(negativeDrivers.numberOfDrivers == 1)
        }

        @Test("maxHoursPerDriverPerDay clamped to minimum 1.0")
        func maxHoursPerDay_clampedToOne() {
            let zeroHours = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 0)
            #expect(zeroHours.maxHoursPerDriverPerDay == 1.0)

            let negativeHours = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: -5)
            #expect(negativeHours.maxHoursPerDriverPerDay == 1.0)
        }

        @Test("init from preferences extracts values correctly")
        func initFromPreferences() {
            let prefs = TripPreferences(
                numberOfDrivers: 3,
                maxDrivingHoursPerDriver: 6.0
            )

            let constraints = DrivingConstraints(from: prefs)

            #expect(constraints.numberOfDrivers == 3)
            #expect(constraints.maxHoursPerDriverPerDay == 6.0)
            #expect(constraints.maxDailyDrivingHours == 18.0)
        }

        @Test("init from preferences defaults to 8 hours when nil")
        func initFromPreferences_nilHoursDefaultsTo8() {
            let prefs = TripPreferences(
                numberOfDrivers: 2,
                maxDrivingHoursPerDriver: nil
            )

            let constraints = DrivingConstraints(from: prefs)

            #expect(constraints.maxHoursPerDriverPerDay == 8.0)
        }

        // Property tests
        @Test("Property: maxDailyDrivingHours always >= 1.0")
        func property_maxDailyHoursAlwaysPositive() {
            for drivers in [-10, 0, 1, 5, 100] {
                for hours in [-10.0, 0.0, 0.5, 1.0, 8.0, 24.0] {
                    let constraints = DrivingConstraints(
                        numberOfDrivers: drivers,
                        maxHoursPerDriverPerDay: hours
                    )
                    #expect(constraints.maxDailyDrivingHours >= 1.0)
                }
            }
        }
    }

    // MARK: - ItineraryOption Tests

    @Suite("ItineraryOption")
    struct ItineraryOptionTests {

        // MARK: - isValid Tests

        @Test("isValid: single stop with no travel segments is valid")
        func isValid_singleStop_noSegments_valid() {
            let option = makeOption(stopsCount: 1, segmentsCount: 0)
            #expect(option.isValid)
        }

        @Test("isValid: two stops with one segment is valid")
        func isValid_twoStops_oneSegment_valid() {
            let option = makeOption(stopsCount: 2, segmentsCount: 1)
            #expect(option.isValid)
        }

        @Test("isValid: three stops with two segments is valid")
        func isValid_threeStops_twoSegments_valid() {
            let option = makeOption(stopsCount: 3, segmentsCount: 2)
            #expect(option.isValid)
        }

        @Test("isValid: mismatched stops and segments is invalid")
        func isValid_mismatchedCounts_invalid() {
            let tooFewSegments = makeOption(stopsCount: 3, segmentsCount: 1)
            #expect(!tooFewSegments.isValid)

            let tooManySegments = makeOption(stopsCount: 2, segmentsCount: 3)
            #expect(!tooManySegments.isValid)
        }

        @Test("isValid: zero stops with zero segments is valid")
        func isValid_zeroStops_valid() {
            let option = makeOption(stopsCount: 0, segmentsCount: 0)
            #expect(option.isValid)
        }

        // MARK: - totalGames Tests

        @Test("totalGames: sums games across all stops")
        func totalGames_sumsAcrossStops() {
            let option = ItineraryOption(
                rank: 1,
                stops: [
                    makeStop(games: ["g1", "g2"]),
                    makeStop(games: ["g3"]),
                    makeStop(games: [])
                ],
                travelSegments: [],
                totalDrivingHours: 0,
                totalDistanceMiles: 0,
                geographicRationale: ""
            )

            #expect(option.totalGames == 3)
        }

        @Test("totalGames: empty stops returns zero")
        func totalGames_emptyStops_returnsZero() {
            let option = makeOption(stopsCount: 0, segmentsCount: 0)
            #expect(option.totalGames == 0)
        }

        // MARK: - sortByLeisure Tests

        @Test("sortByLeisure: empty options returns empty")
        func sortByLeisure_empty_returnsEmpty() {
            let result = ItineraryOption.sortByLeisure([], leisureLevel: .packed)
            #expect(result.isEmpty)
        }

        @Test("sortByLeisure: packed prefers most games")
        func sortByLeisure_packed_prefersMoreGames() {
            let fewerGames = makeOptionWithGamesAndHours(games: 2, hours: 5)
            let moreGames = makeOptionWithGamesAndHours(games: 5, hours: 10)

            let result = ItineraryOption.sortByLeisure([fewerGames, moreGames], leisureLevel: .packed)

            #expect(result.first?.totalGames == 5)
        }

        @Test("sortByLeisure: packed with same games prefers less driving")
        func sortByLeisure_packed_sameGames_prefersLessDriving() {
            let moreDriving = makeOptionWithGamesAndHours(games: 5, hours: 10)
            let lessDriving = makeOptionWithGamesAndHours(games: 5, hours: 5)

            let result = ItineraryOption.sortByLeisure([moreDriving, lessDriving], leisureLevel: .packed)

            #expect(result.first?.totalDrivingHours == 5)
        }

        @Test("sortByLeisure: relaxed prefers less driving")
        func sortByLeisure_relaxed_prefersLessDriving() {
            let moreDriving = makeOptionWithGamesAndHours(games: 5, hours: 10)
            let lessDriving = makeOptionWithGamesAndHours(games: 2, hours: 3)

            let result = ItineraryOption.sortByLeisure([moreDriving, lessDriving], leisureLevel: .relaxed)

            #expect(result.first?.totalDrivingHours == 3)
        }

        @Test("sortByLeisure: relaxed with same driving prefers fewer games")
        func sortByLeisure_relaxed_sameDriving_prefersFewerGames() {
            let moreGames = makeOptionWithGamesAndHours(games: 5, hours: 10)
            let fewerGames = makeOptionWithGamesAndHours(games: 2, hours: 10)

            let result = ItineraryOption.sortByLeisure([moreGames, fewerGames], leisureLevel: .relaxed)

            #expect(result.first?.totalGames == 2)
        }

        @Test("sortByLeisure: moderate prefers best efficiency")
        func sortByLeisure_moderate_prefersBestEfficiency() {
            // 5 games / 10 hours = 0.5 efficiency
            let lowEfficiency = makeOptionWithGamesAndHours(games: 5, hours: 10)
            // 4 games / 4 hours = 1.0 efficiency
            let highEfficiency = makeOptionWithGamesAndHours(games: 4, hours: 4)

            let result = ItineraryOption.sortByLeisure([lowEfficiency, highEfficiency], leisureLevel: .moderate)

            // High efficiency should come first
            #expect(result.first?.totalGames == 4)
        }

        @Test("sortByLeisure: reassigns ranks sequentially")
        func sortByLeisure_reassignsRanks() {
            let options = [
                makeOptionWithGamesAndHours(games: 1, hours: 1),
                makeOptionWithGamesAndHours(games: 3, hours: 3),
                makeOptionWithGamesAndHours(games: 2, hours: 2)
            ]

            let result = ItineraryOption.sortByLeisure(options, leisureLevel: .packed)

            #expect(result[0].rank == 1)
            #expect(result[1].rank == 2)
            #expect(result[2].rank == 3)
        }

        @Test("sortByLeisure: all options are returned")
        func sortByLeisure_allOptionsReturned() {
            let options = [
                makeOptionWithGamesAndHours(games: 1, hours: 1),
                makeOptionWithGamesAndHours(games: 2, hours: 2),
                makeOptionWithGamesAndHours(games: 3, hours: 3)
            ]

            for leisure in [LeisureLevel.packed, .moderate, .relaxed] {
                let result = ItineraryOption.sortByLeisure(options, leisureLevel: leisure)
                #expect(result.count == options.count, "All options should be returned for \(leisure)")
            }
        }

        // Property tests
        @Test("Property: sortByLeisure output count equals input count")
        func property_sortByLeisure_preservesCount() {
            let options = (0..<10).map { _ in
                makeOptionWithGamesAndHours(games: Int.random(in: 1...10), hours: Double.random(in: 1...20))
            }

            for leisure in [LeisureLevel.packed, .moderate, .relaxed] {
                let result = ItineraryOption.sortByLeisure(options, leisureLevel: leisure)
                #expect(result.count == options.count)
            }
        }

        @Test("Property: sortByLeisure ranks are sequential starting at 1")
        func property_sortByLeisure_sequentialRanks() {
            let options = (0..<5).map { _ in
                makeOptionWithGamesAndHours(games: Int.random(in: 1...10), hours: Double.random(in: 1...20))
            }

            let result = ItineraryOption.sortByLeisure(options, leisureLevel: .moderate)

            for (index, option) in result.enumerated() {
                #expect(option.rank == index + 1, "Rank should be \(index + 1), got \(option.rank)")
            }
        }

        // Helper methods
        private func makeOption(stopsCount: Int, segmentsCount: Int) -> ItineraryOption {
            ItineraryOption(
                rank: 1,
                stops: (0..<stopsCount).map { _ in makeStop(games: []) },
                travelSegments: (0..<segmentsCount).map { _ in makeSegment() },
                totalDrivingHours: 0,
                totalDistanceMiles: 0,
                geographicRationale: ""
            )
        }

        private func makeOptionWithGamesAndHours(games: Int, hours: Double) -> ItineraryOption {
            let gameIds = (0..<games).map { "game\($0)" }
            return ItineraryOption(
                rank: 1,
                stops: [makeStop(games: gameIds)],
                travelSegments: [],
                totalDrivingHours: hours,
                totalDistanceMiles: hours * 60,  // 60 mph average
                geographicRationale: ""
            )
        }

        private func makeStop(games: [String]) -> ItineraryStop {
            ItineraryStop(
                city: "TestCity",
                state: "XX",
                coordinate: nil,
                games: games,
                arrivalDate: Date(),
                departureDate: Date(),
                location: LocationInput(name: "Test", coordinate: nil),
                firstGameStart: nil
            )
        }

        private func makeSegment() -> TravelSegment {
            TravelSegment(
                fromLocation: LocationInput(name: "A", coordinate: nil),
                toLocation: LocationInput(name: "B", coordinate: nil),
                travelMode: .drive,
                distanceMeters: 10000,
                durationSeconds: 3600
            )
        }
    }

    // MARK: - ItineraryStop Tests

    @Suite("ItineraryStop")
    struct ItineraryStopTests {

        @Test("hasGames: true when games array is not empty")
        func hasGames_notEmpty_true() {
            let stop = makeStop(games: ["game1"])
            #expect(stop.hasGames)
        }

        @Test("hasGames: false when games array is empty")
        func hasGames_empty_false() {
            let stop = makeStop(games: [])
            #expect(!stop.hasGames)
        }

        @Test("equality based on id only")
        func equality_basedOnId() {
            let stop1 = ItineraryStop(
                city: "New York",
                state: "NY",
                coordinate: nil,
                games: ["g1"],
                arrivalDate: Date(),
                departureDate: Date(),
                location: LocationInput(name: "NY", coordinate: nil),
                firstGameStart: nil
            )

            // Same id via same instance
            #expect(stop1 == stop1)
        }

        private func makeStop(games: [String]) -> ItineraryStop {
            ItineraryStop(
                city: "TestCity",
                state: "XX",
                coordinate: nil,
                games: games,
                arrivalDate: Date(),
                departureDate: Date(),
                location: LocationInput(name: "Test", coordinate: nil),
                firstGameStart: nil
            )
        }
    }

    // MARK: - ItineraryResult Tests

    @Suite("ItineraryResult")
    struct ItineraryResultTests {

        @Test("isSuccess: true for success case")
        func isSuccess_success_true() {
            let result = ItineraryResult.success([])
            #expect(result.isSuccess)
        }

        @Test("isSuccess: false for failure case")
        func isSuccess_failure_false() {
            let result = ItineraryResult.failure(PlanningFailure(reason: .noGamesInRange))
            #expect(!result.isSuccess)
        }

        @Test("options: returns options for success")
        func options_success_returnsOptions() {
            let option = makeSimpleOption()
            let result = ItineraryResult.success([option])
            #expect(result.options.count == 1)
        }

        @Test("options: returns empty for failure")
        func options_failure_returnsEmpty() {
            let result = ItineraryResult.failure(PlanningFailure(reason: .noGamesInRange))
            #expect(result.options.isEmpty)
        }

        @Test("failure: returns failure for failure case")
        func failure_failure_returnsFailure() {
            let result = ItineraryResult.failure(PlanningFailure(reason: .noValidRoutes))
            #expect(result.failure?.reason == .noValidRoutes)
        }

        @Test("failure: returns nil for success")
        func failure_success_returnsNil() {
            let result = ItineraryResult.success([])
            #expect(result.failure == nil)
        }

        private func makeSimpleOption() -> ItineraryOption {
            ItineraryOption(
                rank: 1,
                stops: [],
                travelSegments: [],
                totalDrivingHours: 0,
                totalDistanceMiles: 0,
                geographicRationale: ""
            )
        }
    }

    // MARK: - PlanningFailure Tests

    @Suite("PlanningFailure")
    struct PlanningFailureTests {

        @Test("message: noGamesInRange")
        func message_noGamesInRange() {
            let failure = PlanningFailure(reason: .noGamesInRange)
            #expect(failure.message.contains("No games found"))
        }

        @Test("message: noValidRoutes")
        func message_noValidRoutes() {
            let failure = PlanningFailure(reason: .noValidRoutes)
            #expect(failure.message.contains("No valid routes"))
        }

        @Test("message: repeatCityViolation includes cities")
        func message_repeatCityViolation_includesCities() {
            let failure = PlanningFailure(reason: .repeatCityViolation(cities: ["Boston", "Chicago"]))
            #expect(failure.message.contains("Boston"))
            #expect(failure.message.contains("Chicago"))
        }

        @Test("message: repeatCityViolation truncates long list")
        func message_repeatCityViolation_truncates() {
            let cities = ["A", "B", "C", "D", "E"]
            let failure = PlanningFailure(reason: .repeatCityViolation(cities: cities))
            // Should show first 3 and "and 2 more"
            #expect(failure.message.contains("and 2 more"))
        }

        @Test("FailureReason equality")
        func failureReason_equality() {
            #expect(PlanningFailure.FailureReason.noGamesInRange == .noGamesInRange)
            #expect(PlanningFailure.FailureReason.noValidRoutes != .noGamesInRange)
            #expect(PlanningFailure.FailureReason.repeatCityViolation(cities: ["A"]) == .repeatCityViolation(cities: ["A"]))
            #expect(PlanningFailure.FailureReason.repeatCityViolation(cities: ["A"]) != .repeatCityViolation(cities: ["B"]))
        }
    }

    // MARK: - MustStopConfig Tests

    @Suite("MustStopConfig")
    struct MustStopConfigTests {

        @Test("default proximity is 25 miles")
        func defaultProximity() {
            let config = MustStopConfig()
            #expect(config.proximityMiles == 25)
        }

        @Test("custom proximity preserved")
        func customProximity() {
            let config = MustStopConfig(proximityMiles: 50)
            #expect(config.proximityMiles == 50)
        }
    }
}
