// // ProgressTests.swift // SportsTimeTests // // TDD specification tests for Progress models (LeagueProgress, DivisionProgress, etc.). // import Testing import Foundation @testable import SportsTime @Suite("LeagueProgress") struct LeagueProgressTests { // MARK: - Test Data private func makeLeagueProgress(visited: Int, total: Int) -> LeagueProgress { LeagueProgress( sport: .mlb, totalStadiums: total, visitedStadiums: visited, stadiumsVisited: [], stadiumsRemaining: [] ) } // MARK: - Specification Tests: completionPercentage @Test("completionPercentage: 50% when half visited") func completionPercentage_half() { let progress = makeLeagueProgress(visited: 15, total: 30) #expect(progress.completionPercentage == 50.0) } @Test("completionPercentage: 100% when all visited") func completionPercentage_complete() { let progress = makeLeagueProgress(visited: 30, total: 30) #expect(progress.completionPercentage == 100.0) } @Test("completionPercentage: 0% when none visited") func completionPercentage_zero() { let progress = makeLeagueProgress(visited: 0, total: 30) #expect(progress.completionPercentage == 0.0) } @Test("completionPercentage: 0 when total is 0") func completionPercentage_totalZero() { let progress = makeLeagueProgress(visited: 0, total: 0) #expect(progress.completionPercentage == 0) } // MARK: - Specification Tests: progressFraction @Test("progressFraction: 0.5 when half visited") func progressFraction_half() { let progress = makeLeagueProgress(visited: 15, total: 30) #expect(progress.progressFraction == 0.5) } @Test("progressFraction: 1.0 when complete") func progressFraction_complete() { let progress = makeLeagueProgress(visited: 30, total: 30) #expect(progress.progressFraction == 1.0) } @Test("progressFraction: 0 when total is 0") func progressFraction_totalZero() { let progress = makeLeagueProgress(visited: 5, total: 0) #expect(progress.progressFraction == 0) } // MARK: - Specification Tests: isComplete @Test("isComplete: true when visited >= total and total > 0") func isComplete_true() { let progress = makeLeagueProgress(visited: 30, total: 30) #expect(progress.isComplete == true) } @Test("isComplete: false when visited < total") func isComplete_false() { let progress = makeLeagueProgress(visited: 29, total: 30) #expect(progress.isComplete == false) } @Test("isComplete: false when total is 0") func isComplete_totalZero() { let progress = makeLeagueProgress(visited: 0, total: 0) #expect(progress.isComplete == false) } // MARK: - Specification Tests: progressDescription @Test("progressDescription: formats as visited/total") func progressDescription_format() { let progress = makeLeagueProgress(visited: 15, total: 30) #expect(progress.progressDescription == "15/30") } // MARK: - Invariant Tests @Test("Invariant: completionPercentage in range [0, 100]") func invariant_percentageRange() { let testCases = [ (visited: 0, total: 30), (visited: 15, total: 30), (visited: 30, total: 30), (visited: 0, total: 0), ] for (visited, total) in testCases { let progress = makeLeagueProgress(visited: visited, total: total) #expect(progress.completionPercentage >= 0) #expect(progress.completionPercentage <= 100) } } @Test("Invariant: progressFraction in range [0, 1]") func invariant_fractionRange() { let testCases = [ (visited: 0, total: 30), (visited: 15, total: 30), (visited: 30, total: 30), (visited: 0, total: 0), ] for (visited, total) in testCases { let progress = makeLeagueProgress(visited: visited, total: total) #expect(progress.progressFraction >= 0) #expect(progress.progressFraction <= 1) } } @Test("Invariant: isComplete requires total > 0") func invariant_isCompleteRequiresTotal() { // Can't be complete with 0 total let progress = makeLeagueProgress(visited: 0, total: 0) #expect(progress.isComplete == false) } } // MARK: - DivisionProgress Tests @Suite("DivisionProgress") struct DivisionProgressTests { private func makeDivisionProgress(visited: Int, total: Int) -> DivisionProgress { let division = Division( id: "test_div", name: "Test Division", conference: "Test Conference", conferenceId: "test_conf", sport: .mlb, teamCanonicalIds: [] ) return DivisionProgress( division: division, totalStadiums: total, visitedStadiums: visited, stadiumsVisited: [], stadiumsRemaining: [] ) } @Test("completionPercentage: calculates correctly") func completionPercentage() { let progress = makeDivisionProgress(visited: 3, total: 5) #expect(progress.completionPercentage == 60.0) } @Test("progressFraction: calculates correctly") func progressFraction() { let progress = makeDivisionProgress(visited: 3, total: 5) #expect(progress.progressFraction == 0.6) } @Test("isComplete: true when all visited") func isComplete() { let complete = makeDivisionProgress(visited: 5, total: 5) let incomplete = makeDivisionProgress(visited: 4, total: 5) #expect(complete.isComplete == true) #expect(incomplete.isComplete == false) } } // MARK: - ConferenceProgress Tests @Suite("ConferenceProgress") struct ConferenceProgressTests { private func makeConferenceProgress(visited: Int, total: Int) -> ConferenceProgress { let conference = Conference( id: "test_conf", name: "Test Conference", abbreviation: "TC", sport: .mlb, divisionIds: [] ) return ConferenceProgress( conference: conference, totalStadiums: total, visitedStadiums: visited, divisionProgress: [] ) } @Test("completionPercentage: calculates correctly") func completionPercentage() { let progress = makeConferenceProgress(visited: 10, total: 15) #expect(abs(progress.completionPercentage - 66.666) < 0.01) } @Test("isComplete: true when all visited") func isComplete() { let complete = makeConferenceProgress(visited: 15, total: 15) let incomplete = makeConferenceProgress(visited: 14, total: 15) #expect(complete.isComplete == true) #expect(incomplete.isComplete == false) } } // MARK: - OverallProgress Tests @Suite("OverallProgress") struct OverallProgressTests { @Test("overallPercentage: calculates correctly") func overallPercentage() { let progress = OverallProgress( leagueProgress: [], totalVisits: 50, uniqueStadiumsVisited: 46, totalStadiumsAcrossLeagues: 92, achievementsEarned: 10, totalAchievements: 50 ) #expect(progress.overallPercentage == 50.0) } @Test("overallPercentage: 0 when no stadiums") func overallPercentage_zero() { let progress = OverallProgress( leagueProgress: [], totalVisits: 0, uniqueStadiumsVisited: 0, totalStadiumsAcrossLeagues: 0, achievementsEarned: 0, totalAchievements: 0 ) #expect(progress.overallPercentage == 0) } @Test("progress(for:): finds league progress") func progressForSport() { let mlbProgress = LeagueProgress( sport: .mlb, totalStadiums: 30, visitedStadiums: 15, stadiumsVisited: [], stadiumsRemaining: [] ) let overall = OverallProgress( leagueProgress: [mlbProgress], totalVisits: 15, uniqueStadiumsVisited: 15, totalStadiumsAcrossLeagues: 92, achievementsEarned: 5, totalAchievements: 50 ) let found = overall.progress(for: .mlb) #expect(found != nil) #expect(found?.visitedStadiums == 15) } @Test("progress(for:): returns nil for missing sport") func progressForSport_notFound() { let overall = OverallProgress( leagueProgress: [], totalVisits: 0, uniqueStadiumsVisited: 0, totalStadiumsAcrossLeagues: 92, achievementsEarned: 0, totalAchievements: 50 ) #expect(overall.progress(for: .mlb) == nil) } } // MARK: - StadiumVisitStatus Tests @Suite("StadiumVisitStatus") struct StadiumVisitStatusTests { private func makeVisitSummary(date: Date) -> VisitSummary { VisitSummary( id: UUID(), stadium: Stadium( id: "stadium1", name: "Test Stadium", city: "Test City", state: "TS", latitude: 40.0, longitude: -74.0, capacity: 40000, sport: .mlb ), visitDate: date, visitType: .game, sport: .mlb, homeTeamName: "Home Team", awayTeamName: "Away Team", score: "5-3", photoCount: 0, notes: nil ) } // MARK: - Specification Tests: isVisited @Test("isVisited: true for visited status") func isVisited_true() { let visit = makeVisitSummary(date: TestClock.now) let status = StadiumVisitStatus.visited(visits: [visit]) #expect(status.isVisited == true) } @Test("isVisited: false for notVisited status") func isVisited_false() { let status = StadiumVisitStatus.notVisited #expect(status.isVisited == false) } // MARK: - Specification Tests: visitCount @Test("visitCount: returns count of visits") func visitCount_multiple() { let visits = [ makeVisitSummary(date: TestClock.now), makeVisitSummary(date: TestClock.now), makeVisitSummary(date: TestClock.now), ] let status = StadiumVisitStatus.visited(visits: visits) #expect(status.visitCount == 3) } @Test("visitCount: 0 for notVisited") func visitCount_notVisited() { let status = StadiumVisitStatus.notVisited #expect(status.visitCount == 0) } // MARK: - Specification Tests: latestVisit @Test("latestVisit: returns visit with max date") func latestVisit_maxDate() { let calendar = TestClock.calendar let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))! let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))! let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))! let visits = [ makeVisitSummary(date: date1), makeVisitSummary(date: date2), makeVisitSummary(date: date3), ] let status = StadiumVisitStatus.visited(visits: visits) #expect(status.latestVisit?.visitDate == date2) } @Test("latestVisit: nil for notVisited") func latestVisit_notVisited() { let status = StadiumVisitStatus.notVisited #expect(status.latestVisit == nil) } // MARK: - Specification Tests: firstVisit @Test("firstVisit: returns visit with min date") func firstVisit_minDate() { let calendar = TestClock.calendar let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))! let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))! let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))! let visits = [ makeVisitSummary(date: date1), makeVisitSummary(date: date2), makeVisitSummary(date: date3), ] let status = StadiumVisitStatus.visited(visits: visits) #expect(status.firstVisit?.visitDate == date1) } @Test("firstVisit: nil for notVisited") func firstVisit_notVisited() { let status = StadiumVisitStatus.notVisited #expect(status.firstVisit == nil) } // MARK: - Invariant Tests @Test("Invariant: visitCount == 0 for notVisited") func invariant_visitCountZeroForNotVisited() { let status = StadiumVisitStatus.notVisited #expect(status.visitCount == 0) } @Test("Invariant: latestVisit and firstVisit are nil for notVisited") func invariant_visitsNilForNotVisited() { let status = StadiumVisitStatus.notVisited #expect(status.latestVisit == nil) #expect(status.firstVisit == nil) } } // MARK: - VisitSummary Tests @Suite("VisitSummary") struct VisitSummaryTests { private func makeVisitSummary( homeTeam: String? = "Home", awayTeam: String? = "Away" ) -> VisitSummary { VisitSummary( id: UUID(), stadium: Stadium( id: "stadium1", name: "Test Stadium", city: "Test City", state: "TS", latitude: 40.0, longitude: -74.0, capacity: 40000, sport: .mlb ), visitDate: TestClock.now, visitType: .game, sport: .mlb, homeTeamName: homeTeam, awayTeamName: awayTeam, score: "5-3", photoCount: 0, notes: nil ) } @Test("matchup: returns 'away @ home' format") func matchup_format() { let summary = makeVisitSummary(homeTeam: "Red Sox", awayTeam: "Yankees") #expect(summary.matchup == "Yankees @ Red Sox") } @Test("matchup: nil when home team is nil") func matchup_nilHome() { let summary = makeVisitSummary(homeTeam: nil, awayTeam: "Yankees") #expect(summary.matchup == nil) } @Test("matchup: nil when away team is nil") func matchup_nilAway() { let summary = makeVisitSummary(homeTeam: "Red Sox", awayTeam: nil) #expect(summary.matchup == nil) } } // MARK: - ProgressCardData Tests @Suite("ProgressCardData") struct ProgressCardDataTests { @Test("title: includes sport display name") func title_includesSport() { let progress = LeagueProgress( sport: .mlb, totalStadiums: 30, visitedStadiums: 15, stadiumsVisited: [], stadiumsRemaining: [] ) let cardData = ProgressCardData( sport: .mlb, progress: progress, username: "TestUser", includeMap: true, showDetailedStats: true ) #expect(cardData.title == "Major League Baseball Stadium Quest") } @Test("subtitle: shows visited of total") func subtitle_format() { let progress = LeagueProgress( sport: .mlb, totalStadiums: 30, visitedStadiums: 15, stadiumsVisited: [], stadiumsRemaining: [] ) let cardData = ProgressCardData( sport: .mlb, progress: progress, username: nil, includeMap: false, showDetailedStats: false ) #expect(cardData.subtitle == "15 of 30 Stadiums") } @Test("percentageText: formats without decimals") func percentageText_format() { let progress = LeagueProgress( sport: .mlb, totalStadiums: 30, visitedStadiums: 15, stadiumsVisited: [], stadiumsRemaining: [] ) let cardData = ProgressCardData( sport: .mlb, progress: progress, username: nil, includeMap: false, showDetailedStats: false ) #expect(cardData.percentageText == "50%") } }