//
//  TripPollTests.swift
//  SportsTimeTests
//
//  TDD specification tests for TripPoll, PollVote, and PollResults models.
//

import Testing
import Foundation
@testable import SportsTime

@Suite("TripPoll")
struct TripPollTests {

    // MARK: - Test Data

    private func makeTrip(
        name: String = "Test Trip",
        stops: [TripStop] = [],
        games: [String] = []
    ) -> Trip {
        Trip(
            name: name,
            preferences: TripPreferences(
                planningMode: .dateRange,
                sports: [.mlb],
                startDate: Date(),
                endDate: Date().addingTimeInterval(86400 * 7)
            ),
            stops: stops
        )
    }

    private func makePoll(trips: [Trip] = []) -> TripPoll {
        TripPoll(
            title: "Test Poll",
            ownerId: "owner_123",
            tripSnapshots: trips
        )
    }

    // MARK: - Specification Tests: generateShareCode

    @Test("generateShareCode: returns 6 characters")
    func generateShareCode_length() {
        let code = TripPoll.generateShareCode()

        #expect(code.count == 6)
    }

    @Test("generateShareCode: contains only allowed characters")
    func generateShareCode_allowedChars() {
        let allowedChars = Set("ABCDEFGHJKMNPQRSTUVWXYZ23456789")

        // Generate multiple codes to test randomness
        for _ in 0..<100 {
            let code = TripPoll.generateShareCode()
            for char in code {
                #expect(allowedChars.contains(char), "Character \(char) should be allowed")
            }
        }
    }

    @Test("generateShareCode: excludes ambiguous characters (O, I, L, 0, 1)")
    func generateShareCode_excludesAmbiguous() {
        let ambiguousChars = Set("OIL01")

        // Generate many codes to test
        for _ in 0..<100 {
            let code = TripPoll.generateShareCode()
            for char in code {
                #expect(!ambiguousChars.contains(char), "Character \(char) should not be in share code")
            }
        }
    }

    @Test("generateShareCode: is uppercase")
    func generateShareCode_uppercase() {
        for _ in 0..<50 {
            let code = TripPoll.generateShareCode()
            #expect(code == code.uppercased())
        }
    }

    // MARK: - Specification Tests: computeTripHash

    @Test("computeTripHash: produces deterministic hash for same trip")
    func computeTripHash_deterministic() {
        let trip = makeTrip(name: "Consistent Trip")

        let hash1 = TripPoll.computeTripHash(trip)
        let hash2 = TripPoll.computeTripHash(trip)

        #expect(hash1 == hash2)
    }

    @Test("computeTripHash: different trips produce different hashes")
    func computeTripHash_differentTrips() {
        let calendar = Calendar.current
        let date1 = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
        let date2 = calendar.date(from: DateComponents(year: 2026, month: 6, day: 16))!

        let stop1 = TripStop(
            stopNumber: 1,
            city: "NYC",
            state: "NY",
            arrivalDate: date1,
            departureDate: date1
        )

        let stop2 = TripStop(
            stopNumber: 1,
            city: "Boston",
            state: "MA",
            arrivalDate: date2,
            departureDate: date2
        )

        let trip1 = Trip(
            name: "Trip 1",
            preferences: TripPreferences(
                planningMode: .dateRange,
                sports: [.mlb],
                startDate: date1,
                endDate: date1.addingTimeInterval(86400)
            ),
            stops: [stop1]
        )

        let trip2 = Trip(
            name: "Trip 2",
            preferences: TripPreferences(
                planningMode: .dateRange,
                sports: [.mlb],
                startDate: date2,
                endDate: date2.addingTimeInterval(86400)
            ),
            stops: [stop2]
        )

        let hash1 = TripPoll.computeTripHash(trip1)
        let hash2 = TripPoll.computeTripHash(trip2)

        #expect(hash1 != hash2)
    }

    // MARK: - Specification Tests: shareURL

    @Test("shareURL: uses sportstime://poll/ prefix")
    func shareURL_format() {
        let poll = TripPoll(
            title: "Test",
            ownerId: "owner",
            shareCode: "ABC123",
            tripSnapshots: []
        )

        #expect(poll.shareURL.absoluteString == "sportstime://poll/ABC123")
    }

    // MARK: - Specification Tests: tripVersions

    @Test("tripVersions: count equals tripSnapshots count")
    func tripVersions_countMatchesSnapshots() {
        let trips = [makeTrip(name: "Trip 1"), makeTrip(name: "Trip 2"), makeTrip(name: "Trip 3")]
        let poll = makePoll(trips: trips)

        #expect(poll.tripVersions.count == poll.tripSnapshots.count)
    }

    // MARK: - Invariant Tests

    @Test("Invariant: shareCode is exactly 6 characters")
    func invariant_shareCodeLength() {
        for _ in 0..<20 {
            let poll = makePoll()
            #expect(poll.shareCode.count == 6)
        }
    }

    @Test("Invariant: tripVersions.count == tripSnapshots.count")
    func invariant_versionsMatchSnapshots() {
        let trips = [makeTrip(), makeTrip(), makeTrip()]
        let poll = makePoll(trips: trips)

        #expect(poll.tripVersions.count == poll.tripSnapshots.count)
    }
}

// MARK: - PollVote Tests

@Suite("PollVote")
struct PollVoteTests {

    // MARK: - Specification Tests: calculateScores

    @Test("calculateScores: Borda count gives tripCount points to first choice")
    func calculateScores_firstChoice() {
        // 3 trips, first choice gets 3 points
        let scores = PollVote.calculateScores(rankings: [0, 1, 2], tripCount: 3)

        #expect(scores[0] == 3) // First choice
    }

    @Test("calculateScores: Borda count gives decreasing points by rank")
    func calculateScores_decreasingPoints() {
        // Rankings: trip 2 first, trip 0 second, trip 1 third
        let scores = PollVote.calculateScores(rankings: [2, 0, 1], tripCount: 3)

        #expect(scores[2] == 3) // First choice gets 3
        #expect(scores[0] == 2) // Second choice gets 2
        #expect(scores[1] == 1) // Third choice gets 1
    }

    @Test("calculateScores: last choice gets 1 point")
    func calculateScores_lastChoice() {
        let scores = PollVote.calculateScores(rankings: [0, 1, 2], tripCount: 3)

        #expect(scores[2] == 1) // Last choice
    }

    @Test("calculateScores: handles invalid trip index gracefully")
    func calculateScores_invalidIndex() {
        // Trip index 5 is out of bounds for tripCount 3
        let scores = PollVote.calculateScores(rankings: [0, 1, 5], tripCount: 3)

        #expect(scores[0] == 3)
        #expect(scores[1] == 2)
        // Index 5 is ignored since it's >= tripCount
    }

    @Test("calculateScores: empty rankings returns zeros")
    func calculateScores_emptyRankings() {
        let scores = PollVote.calculateScores(rankings: [], tripCount: 3)

        #expect(scores == [0, 0, 0])
    }

    // MARK: - Invariant Tests

    @Test("Invariant: Borda points range from 1 to tripCount")
    func invariant_bordaPointsRange() {
        let tripCount = 5
        let rankings = [0, 1, 2, 3, 4]
        let scores = PollVote.calculateScores(rankings: rankings, tripCount: tripCount)

        // Each trip should have a score from 1 to tripCount
        let nonZeroScores = scores.filter { $0 > 0 }
        for score in nonZeroScores {
            #expect(score >= 1)
            #expect(score <= tripCount)
        }
    }
}

// MARK: - PollResults Tests

@Suite("PollResults")
struct PollResultsTests {

    // MARK: - Test Data

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

    private func makePoll(tripCount: Int) -> TripPoll {
        let trips = (0..<tripCount).map { makeTrip(name: "Trip \($0)") }
        return TripPoll(
            title: "Test Poll",
            ownerId: "owner",
            tripSnapshots: trips
        )
    }

    private func makeVote(pollId: UUID, rankings: [Int]) -> PollVote {
        PollVote(
            pollId: pollId,
            odg: "voter_\(UUID())",
            rankings: rankings
        )
    }

    // MARK: - Specification Tests: voterCount

    @Test("voterCount: returns number of votes")
    func voterCount_returnsVoteCount() {
        let poll = makePoll(tripCount: 3)
        let votes = [
            makeVote(pollId: poll.id, rankings: [0, 1, 2]),
            makeVote(pollId: poll.id, rankings: [1, 0, 2]),
            makeVote(pollId: poll.id, rankings: [0, 2, 1]),
        ]

        let results = PollResults(poll: poll, votes: votes)

        #expect(results.voterCount == 3)
    }

    // MARK: - Specification Tests: tripScores

    @Test("tripScores: sums all votes correctly")
    func tripScores_sumsVotes() {
        let poll = makePoll(tripCount: 3)
        let votes = [
            makeVote(pollId: poll.id, rankings: [0, 1, 2]), // Trip 0: 3, Trip 1: 2, Trip 2: 1
            makeVote(pollId: poll.id, rankings: [0, 2, 1]), // Trip 0: 3, Trip 2: 2, Trip 1: 1
        ]
        // Total: Trip 0: 6, Trip 1: 3, Trip 2: 3

        let results = PollResults(poll: poll, votes: votes)
        let scores = Dictionary(uniqueKeysWithValues: results.tripScores)

        #expect(scores[0] == 6)
        #expect(scores[1] == 3)
        #expect(scores[2] == 3)
    }

    @Test("tripScores: sorted descending by score")
    func tripScores_sortedDescending() {
        let poll = makePoll(tripCount: 3)
        let votes = [
            makeVote(pollId: poll.id, rankings: [1, 0, 2]), // Trip 1 wins
            makeVote(pollId: poll.id, rankings: [1, 2, 0]), // Trip 1 wins
        ]

        let results = PollResults(poll: poll, votes: votes)

        // First result should have highest score
        let scores = results.tripScores.map { $0.score }
        for i in 1..<scores.count {
            #expect(scores[i] <= scores[i - 1])
        }
    }

    @Test("tripScores: returns zeros when no votes")
    func tripScores_noVotes() {
        let poll = makePoll(tripCount: 3)
        let results = PollResults(poll: poll, votes: [])

        let scores = results.tripScores
        #expect(scores.count == 3)
        for (_, score) in scores {
            #expect(score == 0)
        }
    }

    // MARK: - Specification Tests: maxScore

    @Test("maxScore: returns highest score")
    func maxScore_returnsHighest() {
        let poll = makePoll(tripCount: 3)
        let votes = [
            makeVote(pollId: poll.id, rankings: [0, 1, 2]),
            makeVote(pollId: poll.id, rankings: [0, 1, 2]),
        ]
        // Trip 0: 6 (highest)

        let results = PollResults(poll: poll, votes: votes)

        #expect(results.maxScore == 6)
    }

    @Test("maxScore: 0 when no votes")
    func maxScore_noVotes() {
        let poll = makePoll(tripCount: 3)
        let results = PollResults(poll: poll, votes: [])

        #expect(results.maxScore == 0)
    }

    // MARK: - Specification Tests: scorePercentage

    @Test("scorePercentage: returns score/maxScore")
    func scorePercentage_calculation() {
        let poll = makePoll(tripCount: 2)
        let votes = [
            makeVote(pollId: poll.id, rankings: [0, 1]), // Trip 0: 2, Trip 1: 1
        ]

        let results = PollResults(poll: poll, votes: votes)

        #expect(results.scorePercentage(for: 0) == 1.0) // 2/2
        #expect(results.scorePercentage(for: 1) == 0.5) // 1/2
    }

    @Test("scorePercentage: returns 0 when maxScore is 0")
    func scorePercentage_maxScoreZero() {
        let poll = makePoll(tripCount: 3)
        let results = PollResults(poll: poll, votes: [])

        #expect(results.scorePercentage(for: 0) == 0)
    }

    // MARK: - Invariant Tests

    @Test("Invariant: tripScores contains all trip indices")
    func invariant_tripScoresContainsAllIndices() {
        let poll = makePoll(tripCount: 4)
        let votes = [makeVote(pollId: poll.id, rankings: [0, 1, 2, 3])]

        let results = PollResults(poll: poll, votes: votes)
        let indices = Set(results.tripScores.map { $0.tripIndex })

        #expect(indices == Set(0..<4))
    }
}
