//
//  RouteFiltersTests.swift
//  SportsTimeTests
//
//  TDD specification + property tests for RouteFilters.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("RouteFilters")
struct RouteFiltersTests {

    // MARK: - Test Data

    private let calendar = Calendar.current
    private let today = Calendar.current.startOfDay(for: Date())

    private var tomorrow: Date {
        calendar.date(byAdding: .day, value: 1, to: today)!
    }

    private var nextWeek: Date {
        calendar.date(byAdding: .day, value: 7, to: today)!
    }

    // MARK: - Specification Tests: filterRepeatCities

    @Test("filterRepeatCities: allow=true returns all options unchanged")
    func filterRepeatCities_allowTrue_returnsAllOptions() {
        let options = [
            makeOption(stops: [
                makeStop(city: "New York", date: today),
                makeStop(city: "New York", date: tomorrow)  // Repeat city, different day
            ])
        ]

        let result = RouteFilters.filterRepeatCities(options, allow: true)

        #expect(result.count == options.count)
    }

    @Test("filterRepeatCities: allow=false removes repeat city violations")
    func filterRepeatCities_allowFalse_removesViolations() {
        let violating = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "New York", date: tomorrow)
        ])
        let valid = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "Boston", date: tomorrow)
        ])

        let result = RouteFilters.filterRepeatCities([violating, valid], allow: false)

        #expect(result.count == 1)
    }

    @Test("filterRepeatCities: empty options returns empty")
    func filterRepeatCities_emptyOptions_returnsEmpty() {
        let result = RouteFilters.filterRepeatCities([], allow: false)
        #expect(result.isEmpty)
    }

    @Test("filterRepeatCities: same city same day is allowed")
    func filterRepeatCities_sameCitySameDay_allowed() {
        let option = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "New York", date: today)  // Same city, same day
        ])

        let result = RouteFilters.filterRepeatCities([option], allow: false)

        #expect(result.count == 1)
    }

    // MARK: - Specification Tests: hasRepeatCityViolation

    @Test("hasRepeatCityViolation: single stop returns false")
    func hasRepeatCityViolation_singleStop_returnsFalse() {
        let option = makeOption(stops: [makeStop(city: "New York", date: today)])
        #expect(!RouteFilters.hasRepeatCityViolation(option))
    }

    @Test("hasRepeatCityViolation: different cities returns false")
    func hasRepeatCityViolation_differentCities_returnsFalse() {
        let option = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "Boston", date: tomorrow)
        ])
        #expect(!RouteFilters.hasRepeatCityViolation(option))
    }

    @Test("hasRepeatCityViolation: same city same day returns false")
    func hasRepeatCityViolation_sameCitySameDay_returnsFalse() {
        let option = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "New York", date: today)
        ])
        #expect(!RouteFilters.hasRepeatCityViolation(option))
    }

    @Test("hasRepeatCityViolation: same city different day returns true")
    func hasRepeatCityViolation_sameCityDifferentDay_returnsTrue() {
        let option = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "New York", date: tomorrow)
        ])
        #expect(RouteFilters.hasRepeatCityViolation(option))
    }

    // MARK: - Specification Tests: findRepeatCities

    @Test("findRepeatCities: empty options returns empty")
    func findRepeatCities_emptyOptions_returnsEmpty() {
        let result = RouteFilters.findRepeatCities(in: [])
        #expect(result.isEmpty)
    }

    @Test("findRepeatCities: no violations returns empty")
    func findRepeatCities_noViolations_returnsEmpty() {
        let option = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "Boston", date: tomorrow)
        ])
        let result = RouteFilters.findRepeatCities(in: [option])
        #expect(result.isEmpty)
    }

    @Test("findRepeatCities: returns violating cities sorted")
    func findRepeatCities_returnsViolatingCitiesSorted() {
        let option = makeOption(stops: [
            makeStop(city: "Boston", date: today),
            makeStop(city: "Chicago", date: today),
            makeStop(city: "Boston", date: tomorrow),
            makeStop(city: "Chicago", date: tomorrow)
        ])

        let result = RouteFilters.findRepeatCities(in: [option])

        #expect(result == ["Boston", "Chicago"])  // Sorted alphabetically
    }

    @Test("findRepeatCities: aggregates across all options")
    func findRepeatCities_aggregatesAcrossOptions() {
        let option1 = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "New York", date: tomorrow)
        ])
        let option2 = makeOption(stops: [
            makeStop(city: "Boston", date: today),
            makeStop(city: "Boston", date: tomorrow)
        ])

        let result = RouteFilters.findRepeatCities(in: [option1, option2])

        #expect(result.contains("New York"))
        #expect(result.contains("Boston"))
    }

    // MARK: - Specification Tests: filterBySport

    @Test("filterBySport: empty sports returns all trips")
    func filterBySport_emptySports_returnsAllTrips() {
        let trips = [
            makeTrip(sports: [.mlb]),
            makeTrip(sports: [.nba])
        ]

        let result = RouteFilters.filterBySport(trips, sports: [])

        #expect(result.count == trips.count)
    }

    @Test("filterBySport: matching sport includes trip")
    func filterBySport_matchingSport_includesTrip() {
        let mlbTrip = makeTrip(sports: [.mlb])
        let nbaTrip = makeTrip(sports: [.nba])

        let result = RouteFilters.filterBySport([mlbTrip, nbaTrip], sports: [.mlb])

        #expect(result.count == 1)
    }

    @Test("filterBySport: no matching sport excludes trip")
    func filterBySport_noMatchingSport_excludesTrip() {
        let nhlTrip = makeTrip(sports: [.nhl])

        let result = RouteFilters.filterBySport([nhlTrip], sports: [.mlb, .nba])

        #expect(result.isEmpty)
    }

    @Test("filterBySport: trip with multiple sports, one matches, includes trip")
    func filterBySport_multiSportTripOneMatches_includesTrip() {
        let multiSportTrip = makeTrip(sports: [.mlb, .nba, .nhl])

        let result = RouteFilters.filterBySport([multiSportTrip], sports: [.nba])

        #expect(result.count == 1)
    }

    // MARK: - Specification Tests: filterByDateRange

    @Test("filterByDateRange: trip fully inside range included")
    func filterByDateRange_tripInsideRange_included() {
        let dayAfterTomorrow = calendar.date(byAdding: .day, value: 2, to: today)!
        let trip = makeTrip(startDate: tomorrow, endDate: dayAfterTomorrow)

        let result = RouteFilters.filterByDateRange([trip], start: today, end: nextWeek)

        #expect(result.count == 1)
    }

    @Test("filterByDateRange: trip fully outside range excluded")
    func filterByDateRange_tripOutsideRange_excluded() {
        let twoWeeksAgo = calendar.date(byAdding: .day, value: -14, to: today)!
        let oneWeekAgo = calendar.date(byAdding: .day, value: -7, to: today)!
        let trip = makeTrip(startDate: twoWeeksAgo, endDate: oneWeekAgo)

        let result = RouteFilters.filterByDateRange([trip], start: today, end: nextWeek)

        #expect(result.isEmpty)
    }

    @Test("filterByDateRange: trip partially overlapping included")
    func filterByDateRange_tripPartiallyOverlapping_included() {
        let tripEnd = calendar.date(byAdding: .day, value: 3, to: today)!
        let trip = makeTrip(startDate: today, endDate: tripEnd)

        let rangeStart = calendar.date(byAdding: .day, value: 2, to: today)!
        let rangeEnd = calendar.date(byAdding: .day, value: 5, to: today)!

        let result = RouteFilters.filterByDateRange([trip], start: rangeStart, end: rangeEnd)

        #expect(result.count == 1)
    }

    @Test("filterByDateRange: trip ending on range start included")
    func filterByDateRange_tripEndingOnRangeStart_included() {
        let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
        let trip = makeTrip(startDate: yesterday, endDate: today)

        let result = RouteFilters.filterByDateRange([trip], start: today, end: nextWeek)

        #expect(result.count == 1)
    }

    @Test("filterByDateRange: trip starting on range end included")
    func filterByDateRange_tripStartingOnRangeEnd_included() {
        let dayAfterNextWeek = calendar.date(byAdding: .day, value: 1, to: nextWeek)!
        let trip = makeTrip(startDate: nextWeek, endDate: dayAfterNextWeek)

        let result = RouteFilters.filterByDateRange([trip], start: today, end: nextWeek)

        #expect(result.count == 1)
    }

    // MARK: - Specification Tests: filterByStatus

    @Test("filterByStatus: returns matching status only")
    func filterByStatus_returnsMatchingOnly() {
        let plannedTrip = makeTrip(status: .planned)
        let inProgressTrip = makeTrip(status: .inProgress)
        let completedTrip = makeTrip(status: .completed)

        let result = RouteFilters.filterByStatus([plannedTrip, inProgressTrip, completedTrip], status: .inProgress)

        #expect(result.count == 1)
    }

    @Test("filterByStatus: empty trips returns empty")
    func filterByStatus_emptyTrips_returnsEmpty() {
        let result = RouteFilters.filterByStatus([], status: .planned)
        #expect(result.isEmpty)
    }

    // MARK: - Specification Tests: applyFilters

    @Test("applyFilters: nil criteria skips filter")
    func applyFilters_nilCriteria_skipsFilter() {
        let trips = [makeTrip(sports: [.mlb]), makeTrip(sports: [.nba])]

        let result = RouteFilters.applyFilters(trips, sports: nil, dateRange: nil, status: nil)

        #expect(result.count == trips.count)
    }

    @Test("applyFilters: empty sports set skips sport filter")
    func applyFilters_emptySports_skipsFilter() {
        let trips = [makeTrip(sports: [.mlb]), makeTrip(sports: [.nba])]

        let result = RouteFilters.applyFilters(trips, sports: [], dateRange: nil, status: nil)

        #expect(result.count == trips.count)
    }

    @Test("applyFilters: multiple criteria applies AND logic")
    func applyFilters_multipleCriteria_appliesAnd() {
        let mlbPlanned = makeTrip(sports: [.mlb], status: .planned)
        let mlbInProgress = makeTrip(sports: [.mlb], status: .inProgress)
        let nbaPlanned = makeTrip(sports: [.nba], status: .planned)

        let result = RouteFilters.applyFilters(
            [mlbPlanned, mlbInProgress, nbaPlanned],
            sports: [.mlb],
            dateRange: nil,
            status: .planned
        )

        #expect(result.count == 1)  // Only mlbPlanned matches both
    }

    // MARK: - Property Tests

    @Test("Property: filtering is idempotent for repeat cities")
    func property_filteringIdempotentRepeatCities() {
        let options = [
            makeOption(stops: [
                makeStop(city: "New York", date: today),
                makeStop(city: "New York", date: tomorrow)
            ]),
            makeOption(stops: [
                makeStop(city: "New York", date: today),
                makeStop(city: "Boston", date: tomorrow)
            ])
        ]

        let once = RouteFilters.filterRepeatCities(options, allow: false)
        let twice = RouteFilters.filterRepeatCities(once, allow: false)

        #expect(once.count == twice.count)
    }

    @Test("Property: filtering is idempotent for sports")
    func property_filteringIdempotentSports() {
        let trips = [
            makeTrip(sports: [.mlb]),
            makeTrip(sports: [.nba]),
            makeTrip(sports: [.nhl])
        ]

        let once = RouteFilters.filterBySport(trips, sports: [.mlb])
        let twice = RouteFilters.filterBySport(once, sports: [.mlb])

        #expect(once.count == twice.count)
    }

    @Test("Property: filtering never adds items")
    func property_filteringNeverAdds() {
        let trips = [makeTrip(sports: [.mlb]), makeTrip(sports: [.nba])]

        let result = RouteFilters.filterBySport(trips, sports: [.mlb])

        #expect(result.count <= trips.count)
    }

    @Test("Property: empty input always returns empty")
    func property_emptyInputReturnsEmpty() {
        let emptyTrips: [Trip] = []
        let emptyOptions: [ItineraryOption] = []

        #expect(RouteFilters.filterBySport(emptyTrips, sports: [.mlb]).isEmpty)
        #expect(RouteFilters.filterByStatus(emptyTrips, status: .planned).isEmpty)
        #expect(RouteFilters.filterRepeatCities(emptyOptions, allow: false).isEmpty)
        #expect(RouteFilters.findRepeatCities(in: emptyOptions).isEmpty)
    }

    // MARK: - Edge Case Tests

    @Test("Edge: city names are case-sensitive")
    func edge_cityNamesCaseSensitive() {
        let option = makeOption(stops: [
            makeStop(city: "New York", date: today),
            makeStop(city: "new york", date: tomorrow)  // Different case
        ])

        // Currently case-sensitive, so these are different cities
        #expect(!RouteFilters.hasRepeatCityViolation(option))
    }

    @Test("Edge: very long trip with many stops")
    func edge_veryLongTrip() {
        var stops: [ItineraryStop] = []
        for i in 0..<100 {
            let date = calendar.date(byAdding: .day, value: i, to: today)!
            stops.append(makeStop(city: "City\(i)", date: date))
        }
        let option = makeOption(stops: stops)

        // No repeat cities
        #expect(!RouteFilters.hasRepeatCityViolation(option))
    }

    @Test("Edge: all trips filtered out")
    func edge_allTripsFilteredOut() {
        let trips = [
            makeTrip(sports: [.mlb]),
            makeTrip(sports: [.nba])
        ]

        let result = RouteFilters.filterBySport(trips, sports: [.nhl])

        #expect(result.isEmpty)
    }

    @Test("Edge: date range with same start and end")
    func edge_dateRangeSameStartEnd() {
        let trip = makeTrip(startDate: today, endDate: tomorrow)

        let result = RouteFilters.filterByDateRange([trip], start: today, end: today)

        #expect(result.count == 1)  // Trip overlaps single-day range
    }

    // MARK: - Helper Methods

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

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

    private func makeTrip(
        sports: Set<Sport> = [.mlb],
        startDate: Date? = nil,
        endDate: Date? = nil,
        status: TripStatus = .planned
    ) -> Trip {
        let start = startDate ?? today
        let end = endDate ?? calendar.date(byAdding: .day, value: 3, to: start)!

        return Trip(
            name: "Test Trip",
            preferences: TripPreferences(
                planningMode: .dateRange,
                sports: sports,
                startDate: start,
                endDate: end,
                leisureLevel: .moderate,
                lodgingType: .hotel,
                numberOfDrivers: 1
            ),
            status: status
        )
    }
}
