// // 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() 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..