//
//  TripTests.swift
//  SportsTimeTests
//
//  TDD specification tests for Trip model.
//

import Testing
import Foundation
@testable import SportsTime

@Suite("Trip")
struct TripTests {

    // MARK: - Test Data

    private var calendar: Calendar { Calendar.current }

    private func makePreferences() -> TripPreferences {
        TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            startDate: Date(),
            endDate: Date().addingTimeInterval(86400 * 7)
        )
    }

    private func makeStop(
        city: String,
        arrivalDate: Date,
        departureDate: Date,
        games: [String] = []
    ) -> TripStop {
        TripStop(
            stopNumber: 1,
            city: city,
            state: "XX",
            arrivalDate: arrivalDate,
            departureDate: departureDate,
            games: games
        )
    }

    // MARK: - Specification Tests: itineraryDays

    @Test("itineraryDays: returns one day per calendar day")
    func itineraryDays_oneDayPerCalendarDay() {
        let start = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
        let end = calendar.date(from: DateComponents(year: 2026, month: 6, day: 18))!

        let stop = makeStop(city: "NYC", arrivalDate: start, departureDate: end)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop]
        )

        let days = trip.itineraryDays()

        // 15th, 16th, 17th (18th is departure day, not an activity day)
        #expect(days.count == 3)
    }

    @Test("itineraryDays: dayNumber starts at 1")
    func itineraryDays_dayNumberStartsAtOne() {
        let start = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
        let end = calendar.date(from: DateComponents(year: 2026, month: 6, day: 17))!

        let stop = makeStop(city: "NYC", arrivalDate: start, departureDate: end)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop]
        )

        let days = trip.itineraryDays()

        #expect(days.first?.dayNumber == 1)
    }

    @Test("itineraryDays: dayNumber increments correctly")
    func itineraryDays_dayNumberIncrements() {
        let start = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
        let end = calendar.date(from: DateComponents(year: 2026, month: 6, day: 20))!

        let stop = makeStop(city: "NYC", arrivalDate: start, departureDate: end)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop]
        )

        let days = trip.itineraryDays()

        for (index, day) in days.enumerated() {
            #expect(day.dayNumber == index + 1)
        }
    }

    @Test("itineraryDays: empty for trip with no stops")
    func itineraryDays_emptyForNoStops() {
        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: []
        )

        let days = trip.itineraryDays()

        #expect(days.isEmpty)
    }

    // MARK: - Specification Tests: tripDuration

    @Test("tripDuration: minimum is 1 day")
    func tripDuration_minimumIsOne() {
        let date = Date()
        let stop = makeStop(city: "NYC", arrivalDate: date, departureDate: date)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop]
        )

        #expect(trip.tripDuration >= 1)
    }

    @Test("tripDuration: calculates days between first arrival and last departure")
    func tripDuration_calculatesCorrectly() {
        let start = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
        let end = calendar.date(from: DateComponents(year: 2026, month: 6, day: 22))!

        let stop = makeStop(city: "NYC", arrivalDate: start, departureDate: end)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop]
        )

        #expect(trip.tripDuration == 8) // 15th through 22nd = 8 days
    }

    @Test("tripDuration: is 0 for trip with no stops")
    func tripDuration_zeroForNoStops() {
        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: []
        )

        #expect(trip.tripDuration == 0)
    }

    // MARK: - Specification Tests: cities

    @Test("cities: returns deduplicated list preserving order")
    func cities_deduplicatedPreservingOrder() {
        let date = Date()

        let stop1 = makeStop(city: "NYC", arrivalDate: date, departureDate: date)
        let stop2 = makeStop(city: "Boston", arrivalDate: date, departureDate: date)
        let stop3 = makeStop(city: "NYC", arrivalDate: date, departureDate: date)
        let stop4 = makeStop(city: "Chicago", arrivalDate: date, departureDate: date)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop1, stop2, stop3, stop4]
        )

        #expect(trip.cities == ["NYC", "Boston", "Chicago"])
    }

    @Test("cities: empty for trip with no stops")
    func cities_emptyForNoStops() {
        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: []
        )

        #expect(trip.cities.isEmpty)
    }

    // MARK: - Specification Tests: displayName

    @Test("displayName: uses arrow separator between cities")
    func displayName_arrowSeparator() {
        let date = Date()

        let stop1 = makeStop(city: "NYC", arrivalDate: date, departureDate: date)
        let stop2 = makeStop(city: "Boston", arrivalDate: date, departureDate: date)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop1, stop2]
        )

        #expect(trip.displayName == "NYC → Boston")
    }

    @Test("displayName: uses trip name when no cities")
    func displayName_fallsBackToName() {
        let trip = Trip(
            name: "My Trip",
            preferences: makePreferences(),
            stops: []
        )

        #expect(trip.displayName == "My Trip")
    }

    // MARK: - Specification Tests: Unit Conversions

    @Test("totalDistanceMiles: converts meters to miles")
    func totalDistanceMiles_conversion() {
        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            totalDistanceMeters: 160934.4 // ~100 miles
        )

        #expect(abs(trip.totalDistanceMiles - 100.0) < 0.01)
    }

    @Test("totalDrivingHours: converts seconds to hours")
    func totalDrivingHours_conversion() {
        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            totalDrivingSeconds: 7200 // 2 hours
        )

        #expect(trip.totalDrivingHours == 2.0)
    }

    // MARK: - Invariant Tests

    @Test("Invariant: tripDuration >= 1 when stops exist")
    func invariant_tripDurationMinimum() {
        let testDates: [(start: DateComponents, end: DateComponents)] = [
            (DateComponents(year: 2026, month: 6, day: 15), DateComponents(year: 2026, month: 6, day: 15)),
            (DateComponents(year: 2026, month: 6, day: 15), DateComponents(year: 2026, month: 6, day: 16)),
            (DateComponents(year: 2026, month: 6, day: 15), DateComponents(year: 2026, month: 6, day: 22)),
        ]

        for (start, end) in testDates {
            let startDate = calendar.date(from: start)!
            let endDate = calendar.date(from: end)!
            let stop = makeStop(city: "NYC", arrivalDate: startDate, departureDate: endDate)

            let trip = Trip(
                name: "Test Trip",
                preferences: makePreferences(),
                stops: [stop]
            )

            #expect(trip.tripDuration >= 1)
        }
    }

    @Test("Invariant: cities has no duplicates")
    func invariant_citiesNoDuplicates() {
        let date = Date()

        // Create stops with duplicate cities
        let stops = [
            makeStop(city: "A", arrivalDate: date, departureDate: date),
            makeStop(city: "B", arrivalDate: date, departureDate: date),
            makeStop(city: "A", arrivalDate: date, departureDate: date),
            makeStop(city: "C", arrivalDate: date, departureDate: date),
            makeStop(city: "B", arrivalDate: date, departureDate: date),
        ]

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: stops
        )

        let cities = trip.cities
        let uniqueCities = Set(cities)
        #expect(cities.count == uniqueCities.count, "cities should not have duplicates")
    }

    @Test("Invariant: itineraryDays dayNumber starts at 1 and increments")
    func invariant_dayNumberSequence() {
        let start = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
        let end = calendar.date(from: DateComponents(year: 2026, month: 6, day: 20))!

        let stop = makeStop(city: "NYC", arrivalDate: start, departureDate: end)

        let trip = Trip(
            name: "Test Trip",
            preferences: makePreferences(),
            stops: [stop]
        )

        let days = trip.itineraryDays()

        guard !days.isEmpty else { return }

        #expect(days.first?.dayNumber == 1)

        for i in 1..<days.count {
            #expect(days[i].dayNumber == days[i - 1].dayNumber + 1)
        }
    }
}

// MARK: - TripScore Tests

@Suite("TripScore")
struct TripScoreTests {

    // MARK: - Specification Tests: scoreGrade

    @Test("scoreGrade: 90-100 returns A+")
    func scoreGrade_APlus() {
        let score90 = TripScore(overallScore: 90, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        let score100 = TripScore(overallScore: 100, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)

        #expect(score90.scoreGrade == "A+")
        #expect(score100.scoreGrade == "A+")
    }

    @Test("scoreGrade: 85-89.99 returns A")
    func scoreGrade_A() {
        let score = TripScore(overallScore: 87, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "A")
    }

    @Test("scoreGrade: 80-84.99 returns A-")
    func scoreGrade_AMinus() {
        let score = TripScore(overallScore: 82, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "A-")
    }

    @Test("scoreGrade: 75-79.99 returns B+")
    func scoreGrade_BPlus() {
        let score = TripScore(overallScore: 77, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "B+")
    }

    @Test("scoreGrade: 70-74.99 returns B")
    func scoreGrade_B() {
        let score = TripScore(overallScore: 72, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "B")
    }

    @Test("scoreGrade: 65-69.99 returns B-")
    func scoreGrade_BMinus() {
        let score = TripScore(overallScore: 67, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "B-")
    }

    @Test("scoreGrade: 60-64.99 returns C+")
    func scoreGrade_CPlus() {
        let score = TripScore(overallScore: 62, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "C+")
    }

    @Test("scoreGrade: 55-59.99 returns C")
    func scoreGrade_C() {
        let score = TripScore(overallScore: 57, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "C")
    }

    @Test("scoreGrade: below 55 returns C-")
    func scoreGrade_CMinus() {
        let score = TripScore(overallScore: 50, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.scoreGrade == "C-")
    }

    @Test("scoreGrade: boundary values")
    func scoreGrade_boundaries() {
        // Test exact boundary values
        let testCases: [(score: Double, expected: String)] = [
            (90.0, "A+"),
            (89.9999, "A"),
            (85.0, "A"),
            (84.9999, "A-"),
            (80.0, "A-"),
            (79.9999, "B+"),
            (75.0, "B+"),
            (74.9999, "B"),
            (70.0, "B"),
            (69.9999, "B-"),
            (65.0, "B-"),
            (64.9999, "C+"),
            (60.0, "C+"),
            (59.9999, "C"),
            (55.0, "C"),
            (54.9999, "C-"),
        ]

        for (scoreValue, expected) in testCases {
            let score = TripScore(overallScore: scoreValue, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
            #expect(score.scoreGrade == expected, "Score \(scoreValue) should be \(expected)")
        }
    }

    // MARK: - Property Tests

    @Test("Property: formattedOverallScore rounds to integer")
    func property_formattedOverallScore() {
        let score = TripScore(overallScore: 85.7, gameQualityScore: 0, routeEfficiencyScore: 0, leisureBalanceScore: 0, preferenceAlignmentScore: 0)
        #expect(score.formattedOverallScore == "86")
    }
}
