//
//  ItineraryBuilderTests.swift
//  SportsTimeTests
//
//  Phase 8: ItineraryBuilder Tests
//  Builds day-by-day itinerary from route with travel segments.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("ItineraryBuilder Tests")
struct ItineraryBuilderTests {

    // MARK: - Test Constants

    private let defaultConstraints = DrivingConstraints.default
    private let calendar = Calendar.current

    // Known locations for testing
    private let nyc = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352)
    private let boston = CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589)
    private let chicago = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298)
    private let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437)

    // MARK: - 8.1 Single Game Creates Single Day

    @Test("Single stop creates itinerary with one stop and no travel segments")
    func test_builder_SingleGame_CreatesSingleDay() {
        // Arrange
        let gameId = UUID()
        let stop = makeItineraryStop(
            city: "New York",
            state: "NY",
            coordinate: nyc,
            games: [gameId]
        )

        // Act
        let result = ItineraryBuilder.build(
            stops: [stop],
            constraints: defaultConstraints
        )

        // Assert
        #expect(result != nil, "Single stop should produce a valid itinerary")

        if let itinerary = result {
            #expect(itinerary.stops.count == 1, "Should have exactly 1 stop")
            #expect(itinerary.travelSegments.isEmpty, "Should have no travel segments")
            #expect(itinerary.totalDrivingHours == 0, "Should have 0 driving hours")
            #expect(itinerary.totalDistanceMiles == 0, "Should have 0 distance")
        }
    }

    // MARK: - 8.2 Multi-City Creates Travel Segments Between

    @Test("Multiple cities creates travel segments between consecutive stops")
    func test_builder_MultiCity_CreatesTravelSegmentsBetween() {
        // Arrange
        let stops = [
            makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]),
            makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]),
            makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()])
        ]

        // Act
        let result = ItineraryBuilder.build(
            stops: stops,
            constraints: defaultConstraints
        )

        // Assert
        #expect(result != nil, "Multi-city trip should produce a valid itinerary")

        if let itinerary = result {
            #expect(itinerary.stops.count == 3, "Should have 3 stops")
            #expect(itinerary.travelSegments.count == 2, "Should have 2 travel segments (stops - 1)")

            // Verify segment 1: Boston -> NYC
            let segment1 = itinerary.travelSegments[0]
            #expect(segment1.fromLocation.name == "Boston", "First segment should start from Boston")
            #expect(segment1.toLocation.name == "New York", "First segment should end at New York")
            #expect(segment1.travelMode == .drive, "Travel mode should be drive")
            #expect(segment1.distanceMeters > 0, "Distance should be positive")
            #expect(segment1.durationSeconds > 0, "Duration should be positive")

            // Verify segment 2: NYC -> Chicago
            let segment2 = itinerary.travelSegments[1]
            #expect(segment2.fromLocation.name == "New York", "Second segment should start from New York")
            #expect(segment2.toLocation.name == "Chicago", "Second segment should end at Chicago")

            // Verify totals are accumulated
            #expect(itinerary.totalDrivingHours > 0, "Total driving hours should be positive")
            #expect(itinerary.totalDistanceMiles > 0, "Total distance should be positive")
        }
    }

    // MARK: - 8.3 Same City Multiple Games Groups On Same Day

    @Test("Same city multiple stops have zero distance travel between them")
    func test_builder_SameCity_MultipleGames_GroupsOnSameDay() {
        // Arrange - Two stops in the same city (different games, same location)
        let stops = [
            makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]),
            makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()])
        ]

        // Act
        let result = ItineraryBuilder.build(
            stops: stops,
            constraints: defaultConstraints
        )

        // Assert
        #expect(result != nil, "Same city stops should produce a valid itinerary")

        if let itinerary = result {
            #expect(itinerary.stops.count == 2, "Should have 2 stops")
            #expect(itinerary.travelSegments.count == 1, "Should have 1 travel segment")

            // Travel within same city should be minimal/zero distance
            let segment = itinerary.travelSegments[0]
            #expect(segment.estimatedDistanceMiles < 1.0,
                    "Same city travel should have near-zero distance, got \(segment.estimatedDistanceMiles)")
            #expect(segment.estimatedDrivingHours < 0.1,
                    "Same city travel should have near-zero duration, got \(segment.estimatedDrivingHours)")

            // Total driving should be minimal
            #expect(itinerary.totalDrivingHours < 0.1,
                    "Total driving hours should be near zero for same city")
        }
    }

    // MARK: - 8.4 Travel Days Inserted When Driving Exceeds 8 Hours

    @Test("Multi-day driving is calculated correctly when exceeding 8 hours per day")
    func test_builder_TravelDays_InsertedWhenDrivingExceeds8Hours() {
        // Arrange - Create a trip that requires multi-day driving
        // Boston to Chicago is ~850 miles haversine, ~1100 with road factor
        // At 60 mph, that's ~18 hours = 3 travel days (ceil(18/8) = 3)
        let stops = [
            makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]),
            makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()])
        ]

        // Use constraints that allow long trips
        let constraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)

        // Act
        let result = ItineraryBuilder.build(
            stops: stops,
            constraints: constraints
        )

        // Assert
        #expect(result != nil, "Long-distance trip should produce a valid itinerary")

        if let itinerary = result {
            // Get the travel segment
            #expect(itinerary.travelSegments.count == 1, "Should have 1 travel segment")

            let segment = itinerary.travelSegments[0]
            let drivingHours = segment.estimatedDrivingHours

            // Verify this is a multi-day drive
            #expect(drivingHours > 8.0, "Boston to Chicago should require more than 8 hours driving")

            // Calculate travel days using TravelEstimator
            let travelDays = TravelEstimator.calculateTravelDays(
                departure: Date(),
                drivingHours: drivingHours
            )

            // Should span multiple days (ceil(hours/8))
            let expectedDays = Int(ceil(drivingHours / 8.0))
            #expect(travelDays.count == expectedDays,
                    "Travel should span \(expectedDays) days for \(drivingHours) hours driving, got \(travelDays.count)")
        }
    }

    // MARK: - 8.5 Arrival Time Before Game Calculated

    @Test("Segment validator rejects trips where arrival is after game start")
    func test_builder_ArrivalTimeBeforeGame_Calculated() {
        // Arrange - Create stops where travel time makes arriving on time impossible
        let now = Date()
        let gameStartSoon = now.addingTimeInterval(2 * 3600) // Game starts in 2 hours

        let fromStop = makeItineraryStop(
            city: "Boston",
            state: "MA",
            coordinate: boston,
            games: [UUID()],
            departureDate: now
        )

        // NYC game starts in 2 hours, but travel is ~4 hours
        let toStop = makeItineraryStop(
            city: "New York",
            state: "NY",
            coordinate: nyc,
            games: [UUID()],
            firstGameStart: gameStartSoon
        )

        // Use the arrival validator
        let arrivalValidator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)

        // Act
        let result = ItineraryBuilder.build(
            stops: [fromStop, toStop],
            constraints: defaultConstraints,
            segmentValidator: arrivalValidator
        )

        // Assert - Should return nil because we can't arrive 1 hour before game
        // Boston to NYC is ~4 hours, game starts in 2 hours, need 1 hour buffer
        // 4 hours travel > 2 hours - 1 hour buffer = 1 hour available
        #expect(result == nil, "Should return nil when arrival would be after game start minus buffer")
    }

    @Test("Segment validator accepts trips where arrival is before game start")
    func test_builder_ArrivalTimeBeforeGame_Succeeds() {
        // Arrange - Create stops where there's plenty of time
        let now = Date()
        let gameLater = now.addingTimeInterval(10 * 3600) // Game in 10 hours

        let fromStop = makeItineraryStop(
            city: "Boston",
            state: "MA",
            coordinate: boston,
            games: [UUID()],
            departureDate: now
        )

        let toStop = makeItineraryStop(
            city: "New York",
            state: "NY",
            coordinate: nyc,
            games: [UUID()],
            firstGameStart: gameLater
        )

        let arrivalValidator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)

        // Act
        let result = ItineraryBuilder.build(
            stops: [fromStop, toStop],
            constraints: defaultConstraints,
            segmentValidator: arrivalValidator
        )

        // Assert - Should succeed, 4 hours travel leaves 5 hours before 10-hour deadline
        #expect(result != nil, "Should return valid itinerary when arrival is well before game")
    }

    // MARK: - 8.6 Empty Route Returns Empty Itinerary

    @Test("Empty stops array returns empty itinerary")
    func test_builder_EmptyRoute_ReturnsEmptyItinerary() {
        // Act
        let result = ItineraryBuilder.build(
            stops: [],
            constraints: defaultConstraints
        )

        // Assert
        #expect(result != nil, "Empty stops should still return a valid (empty) itinerary")

        if let itinerary = result {
            #expect(itinerary.stops.isEmpty, "Should have no stops")
            #expect(itinerary.travelSegments.isEmpty, "Should have no travel segments")
            #expect(itinerary.totalDrivingHours == 0, "Should have 0 driving hours")
            #expect(itinerary.totalDistanceMiles == 0, "Should have 0 distance")
        }
    }

    // MARK: - Helpers

    private func makeItineraryStop(
        city: String,
        state: String,
        coordinate: CLLocationCoordinate2D? = nil,
        games: [UUID] = [],
        arrivalDate: Date = Date(),
        departureDate: Date? = nil,
        firstGameStart: Date? = nil
    ) -> ItineraryStop {
        ItineraryStop(
            city: city,
            state: state,
            coordinate: coordinate,
            games: games,
            arrivalDate: arrivalDate,
            departureDate: departureDate ?? calendar.date(byAdding: .day, value: 1, to: arrivalDate)!,
            location: LocationInput(name: city, coordinate: coordinate),
            firstGameStart: firstGameStart
        )
    }
}
