//
//  PollTests.swift
//  SportsTimeTests
//
//  Tests for TripPoll, PollVote, and PollResults domain models
//

import Testing
@testable import SportsTime
import Foundation

// MARK: - TripPoll Tests

struct TripPollTests {

    // MARK: - Share Code Tests

    @Test("Share code has correct length")
    func shareCode_HasCorrectLength() {
        let code = TripPoll.generateShareCode()
        #expect(code.count == 6)
    }

    @Test("Share code contains only allowed characters")
    func shareCode_ContainsOnlyAllowedCharacters() {
        let allowedCharacters = Set("ABCDEFGHJKMNPQRSTUVWXYZ23456789")

        for _ in 0..<100 {
            let code = TripPoll.generateShareCode()
            for char in code {
                #expect(allowedCharacters.contains(char), "Unexpected character: \(char)")
            }
        }
    }

    @Test("Share code excludes ambiguous characters")
    func shareCode_ExcludesAmbiguousCharacters() {
        let ambiguousCharacters = Set("0O1IL")

        for _ in 0..<100 {
            let code = TripPoll.generateShareCode()
            for char in code {
                #expect(!ambiguousCharacters.contains(char), "Found ambiguous character: \(char)")
            }
        }
    }

    @Test("Share codes are unique")
    func shareCode_IsUnique() {
        var codes = Set<String>()
        for _ in 0..<1000 {
            let code = TripPoll.generateShareCode()
            codes.insert(code)
        }
        // With 6 chars from 32 possibilities, collisions in 1000 samples should be rare
        #expect(codes.count >= 990, "Too many collisions in share code generation")
    }

    // MARK: - Share URL Tests

    @Test("Share URL is correctly formatted")
    func shareURL_IsCorrectlyFormatted() {
        let poll = makeTestPoll(shareCode: "ABC123")
        #expect(poll.shareURL.absoluteString == "sportstime://poll/ABC123")
    }

    // MARK: - Trip Hash Tests

    @Test("Trip hash is deterministic")
    func tripHash_IsDeterministic() {
        let trip = makeTestTrip(cities: ["Chicago", "Detroit"])
        let hash1 = TripPoll.computeTripHash(trip)
        let hash2 = TripPoll.computeTripHash(trip)
        #expect(hash1 == hash2)
    }

    @Test("Trip hash differs for different cities")
    func tripHash_DiffersForDifferentCities() {
        let trip1 = makeTestTrip(cities: ["Chicago", "Detroit"])
        let trip2 = makeTestTrip(cities: ["Chicago", "Milwaukee"])
        let hash1 = TripPoll.computeTripHash(trip1)
        let hash2 = TripPoll.computeTripHash(trip2)
        #expect(hash1 != hash2)
    }

    @Test("Trip hash differs for different dates")
    func tripHash_DiffersForDifferentDates() {
        let baseDate = Date()
        let trip1 = makeTestTrip(cities: ["Chicago"], startDate: baseDate)
        let trip2 = makeTestTrip(cities: ["Chicago"], startDate: baseDate.addingTimeInterval(86400))
        let hash1 = TripPoll.computeTripHash(trip1)
        let hash2 = TripPoll.computeTripHash(trip2)
        #expect(hash1 != hash2)
    }

    // MARK: - Initialization Tests

    @Test("Poll initializes with trip versions")
    func poll_InitializesWithTripVersions() {
        let trip1 = makeTestTrip(cities: ["Chicago"])
        let trip2 = makeTestTrip(cities: ["Detroit"])
        let poll = TripPoll(
            title: "Test Poll",
            ownerId: "user123",
            tripSnapshots: [trip1, trip2]
        )

        #expect(poll.tripVersions.count == 2)
        #expect(poll.tripVersions[0] == TripPoll.computeTripHash(trip1))
        #expect(poll.tripVersions[1] == TripPoll.computeTripHash(trip2))
    }
}

// MARK: - PollVote Tests

struct PollVoteTests {

    // MARK: - Borda Count Tests

    @Test("Borda count scores 3 trips correctly")
    func bordaCount_Scores3TripsCorrectly() {
        // Rankings: [2, 0, 1] means trip 2 is #1, trip 0 is #2, trip 1 is #3
        let rankings = [2, 0, 1]
        let scores = PollVote.calculateScores(rankings: rankings, tripCount: 3)

        // Trip 2 is rank 0 (first place): 3 - 0 = 3 points
        // Trip 0 is rank 1 (second place): 3 - 1 = 2 points
        // Trip 1 is rank 2 (third place): 3 - 2 = 1 point
        #expect(scores[0] == 2, "Trip 0 should have 2 points")
        #expect(scores[1] == 1, "Trip 1 should have 1 point")
        #expect(scores[2] == 3, "Trip 2 should have 3 points")
    }

    @Test("Borda count scores 2 trips correctly")
    func bordaCount_Scores2TripsCorrectly() {
        let rankings = [1, 0]  // Trip 1 first, Trip 0 second
        let scores = PollVote.calculateScores(rankings: rankings, tripCount: 2)

        #expect(scores[0] == 1, "Trip 0 should have 1 point")
        #expect(scores[1] == 2, "Trip 1 should have 2 points")
    }

    @Test("Borda count handles invalid trip index")
    func bordaCount_HandlesInvalidTripIndex() {
        let rankings = [0, 5]  // 5 is out of bounds for tripCount 2
        let scores = PollVote.calculateScores(rankings: rankings, tripCount: 2)

        #expect(scores[0] == 2, "Trip 0 should have 2 points")
        #expect(scores[1] == 0, "Trip 1 should have 0 points (never ranked)")
    }

    @Test("Borda count with 5 trips")
    func bordaCount_With5Trips() {
        // Rankings: trip indices in preference order
        let rankings = [4, 2, 0, 3, 1]  // Trip 4 is best, trip 1 is worst
        let scores = PollVote.calculateScores(rankings: rankings, tripCount: 5)

        // Points: 5 for 1st, 4 for 2nd, 3 for 3rd, 2 for 4th, 1 for 5th
        #expect(scores[0] == 3, "Trip 0 (3rd place) should have 3 points")
        #expect(scores[1] == 1, "Trip 1 (5th place) should have 1 point")
        #expect(scores[2] == 4, "Trip 2 (2nd place) should have 4 points")
        #expect(scores[3] == 2, "Trip 3 (4th place) should have 2 points")
        #expect(scores[4] == 5, "Trip 4 (1st place) should have 5 points")
    }
}

// MARK: - PollResults Tests

struct PollResultsTests {

    @Test("Results with no votes returns zero scores")
    func results_NoVotesReturnsZeroScores() {
        let poll = makeTestPoll(tripCount: 3)
        let results = PollResults(poll: poll, votes: [])

        #expect(results.voterCount == 0)
        #expect(results.maxScore == 0)
        #expect(results.tripScores.count == 3)
        for item in results.tripScores {
            #expect(item.score == 0)
        }
    }

    @Test("Results with single vote")
    func results_SingleVote() {
        let poll = makeTestPoll(tripCount: 3)
        let vote = PollVote(pollId: poll.id, odg: "voter1", rankings: [2, 0, 1])
        let results = PollResults(poll: poll, votes: [vote])

        #expect(results.voterCount == 1)
        #expect(results.maxScore == 3)

        // Trip 2 should be first with 3 points
        #expect(results.tripScores[0].tripIndex == 2)
        #expect(results.tripScores[0].score == 3)

        // Trip 0 should be second with 2 points
        #expect(results.tripScores[1].tripIndex == 0)
        #expect(results.tripScores[1].score == 2)

        // Trip 1 should be third with 1 point
        #expect(results.tripScores[2].tripIndex == 1)
        #expect(results.tripScores[2].score == 1)
    }

    @Test("Results aggregates multiple votes")
    func results_AggregatesMultipleVotes() {
        let poll = makeTestPoll(tripCount: 3)
        let vote1 = PollVote(pollId: poll.id, odg: "voter1", rankings: [0, 1, 2])  // Trip 0 first
        let vote2 = PollVote(pollId: poll.id, odg: "voter2", rankings: [0, 2, 1])  // Trip 0 first
        let vote3 = PollVote(pollId: poll.id, odg: "voter3", rankings: [1, 0, 2])  // Trip 1 first
        let results = PollResults(poll: poll, votes: [vote1, vote2, vote3])

        #expect(results.voterCount == 3)

        // Trip 0: 3 + 3 + 2 = 8 points (first, first, second)
        // Trip 1: 2 + 1 + 3 = 6 points (second, third, first)
        // Trip 2: 1 + 2 + 1 = 4 points (third, second, third)

        #expect(results.tripScores[0].tripIndex == 0, "Trip 0 should be ranked first")
        #expect(results.tripScores[0].score == 8)
        #expect(results.tripScores[1].tripIndex == 1, "Trip 1 should be ranked second")
        #expect(results.tripScores[1].score == 6)
        #expect(results.tripScores[2].tripIndex == 2, "Trip 2 should be ranked third")
        #expect(results.tripScores[2].score == 4)
    }

    @Test("Score percentage calculation")
    func results_ScorePercentageCalculation() {
        let poll = makeTestPoll(tripCount: 2)
        let vote1 = PollVote(pollId: poll.id, odg: "voter1", rankings: [0, 1])
        let vote2 = PollVote(pollId: poll.id, odg: "voter2", rankings: [0, 1])
        let results = PollResults(poll: poll, votes: [vote1, vote2])

        // Trip 0: 2 + 2 = 4 points (max)
        // Trip 1: 1 + 1 = 2 points

        #expect(results.scorePercentage(for: 0) == 1.0, "Trip 0 should be 100%")
        #expect(results.scorePercentage(for: 1) == 0.5, "Trip 1 should be 50%")
    }

    @Test("Score percentage returns zero when no votes")
    func results_ScorePercentageReturnsZeroWhenNoVotes() {
        let poll = makeTestPoll(tripCount: 2)
        let results = PollResults(poll: poll, votes: [])

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

    @Test("Results handles tie correctly")
    func results_HandlesTieCorrectly() {
        let poll = makeTestPoll(tripCount: 2)
        let vote1 = PollVote(pollId: poll.id, odg: "voter1", rankings: [0, 1])
        let vote2 = PollVote(pollId: poll.id, odg: "voter2", rankings: [1, 0])
        let results = PollResults(poll: poll, votes: [vote1, vote2])

        // Trip 0: 2 + 1 = 3 points
        // Trip 1: 1 + 2 = 3 points
        // Both tied at 3

        #expect(results.tripScores[0].score == 3)
        #expect(results.tripScores[1].score == 3)
        #expect(results.maxScore == 3)
    }
}

// MARK: - Test Helpers

private func makeTestTrip(
    cities: [String],
    startDate: Date = Date(),
    games: [String] = []
) -> Trip {
    let stops = cities.enumerated().map { index, city in
        TripStop(
            stopNumber: index + 1,
            city: city,
            state: "XX",
            arrivalDate: startDate.addingTimeInterval(Double(index) * 86400),
            departureDate: startDate.addingTimeInterval(Double(index + 1) * 86400),
            games: games
        )
    }

    return Trip(
        name: "Test Trip",
        preferences: TripPreferences(
            planningMode: .dateRange,
            sports: [.mlb],
            startDate: startDate,
            endDate: startDate.addingTimeInterval(86400 * Double(cities.count))
        ),
        stops: stops
    )
}

private func makeTestPoll(tripCount: Int = 3, shareCode: String? = nil) -> TripPoll {
    let trips = (0..<tripCount).map { index in
        makeTestTrip(cities: ["City\(index)"])
    }

    return TripPoll(
        title: "Test Poll",
        ownerId: "testOwner",
        shareCode: shareCode ?? TripPoll.generateShareCode(),
        tripSnapshots: trips
    )
}
