// // FreeScoreAPITests.swift // SportsTimeTests // // TDD specification tests for FreeScoreAPI types. // import Testing import Foundation @testable import SportsTime // MARK: - ProviderReliability Tests @Suite("ProviderReliability") struct ProviderReliabilityTests { // MARK: - Specification Tests: Values @Test("official: raw value is 'official'") func official_rawValue() { #expect(ProviderReliability.official.rawValue == "official") } @Test("unofficial: raw value is 'unofficial'") func unofficial_rawValue() { #expect(ProviderReliability.unofficial.rawValue == "unofficial") } @Test("scraped: raw value is 'scraped'") func scraped_rawValue() { #expect(ProviderReliability.scraped.rawValue == "scraped") } // MARK: - Invariant Tests /// - Invariant: All reliability levels have distinct raw values @Test("Invariant: all raw values are distinct") func invariant_distinctRawValues() { let all: [ProviderReliability] = [.official, .unofficial, .scraped] let rawValues = all.map { $0.rawValue } let uniqueValues = Set(rawValues) #expect(rawValues.count == uniqueValues.count) } } // MARK: - HistoricalGameQuery Tests @Suite("HistoricalGameQuery") struct HistoricalGameQueryTests { // MARK: - Specification Tests: normalizedDateString /// - Expected Behavior: Returns date in yyyy-MM-dd format (Eastern time) @Test("normalizedDateString: formats as yyyy-MM-dd") func normalizedDateString_format() { var calendar = TestClock.calendar calendar.timeZone = TimeZone(identifier: "America/New_York")! let date = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))! let query = HistoricalGameQuery(sport: .mlb, date: date) #expect(query.normalizedDateString == "2026-06-15") } @Test("normalizedDateString: pads single-digit months") func normalizedDateString_padMonth() { var calendar = TestClock.calendar calendar.timeZone = TimeZone(identifier: "America/New_York")! let date = calendar.date(from: DateComponents(year: 2026, month: 3, day: 5))! let query = HistoricalGameQuery(sport: .mlb, date: date) #expect(query.normalizedDateString == "2026-03-05") } // MARK: - Specification Tests: Initialization @Test("init: stores sport correctly") func init_storesSport() { let query = HistoricalGameQuery(sport: .nba, date: TestClock.now) #expect(query.sport == .nba) } @Test("init: stores team abbreviations") func init_storesTeams() { let query = HistoricalGameQuery( sport: .mlb, date: TestClock.now, homeTeamAbbrev: "NYY", awayTeamAbbrev: "BOS" ) #expect(query.homeTeamAbbrev == "NYY") #expect(query.awayTeamAbbrev == "BOS") } @Test("init: team abbreviations default to nil") func init_defaultNilTeams() { let query = HistoricalGameQuery(sport: .mlb, date: TestClock.now) #expect(query.homeTeamAbbrev == nil) #expect(query.awayTeamAbbrev == nil) #expect(query.stadiumCanonicalId == nil) } } // MARK: - HistoricalGameResult Tests @Suite("HistoricalGameResult") struct HistoricalGameResultTests { // MARK: - Test Data private func makeResult( homeScore: Int? = 5, awayScore: Int? = 3 ) -> HistoricalGameResult { HistoricalGameResult( sport: .mlb, gameDate: TestClock.now, homeTeamAbbrev: "NYY", awayTeamAbbrev: "BOS", homeTeamName: "Yankees", awayTeamName: "Red Sox", homeScore: homeScore, awayScore: awayScore, source: .api, providerName: "Test Provider" ) } // MARK: - Specification Tests: scoreString /// - Expected Behavior: Format is "away-home" (e.g., "3-5") @Test("scoreString: formats as away-home") func scoreString_format() { let result = makeResult(homeScore: 5, awayScore: 3) #expect(result.scoreString == "3-5") } @Test("scoreString: nil when homeScore is nil") func scoreString_nilHomeScore() { let result = makeResult(homeScore: nil, awayScore: 3) #expect(result.scoreString == nil) } @Test("scoreString: nil when awayScore is nil") func scoreString_nilAwayScore() { let result = makeResult(homeScore: 5, awayScore: nil) #expect(result.scoreString == nil) } @Test("scoreString: nil when both scores are nil") func scoreString_bothNil() { let result = makeResult(homeScore: nil, awayScore: nil) #expect(result.scoreString == nil) } // MARK: - Specification Tests: hasScore /// - Expected Behavior: true only when both scores are present @Test("hasScore: true when both scores present") func hasScore_bothPresent() { let result = makeResult(homeScore: 5, awayScore: 3) #expect(result.hasScore == true) } @Test("hasScore: false when homeScore is nil") func hasScore_nilHomeScore() { let result = makeResult(homeScore: nil, awayScore: 3) #expect(result.hasScore == false) } @Test("hasScore: false when awayScore is nil") func hasScore_nilAwayScore() { let result = makeResult(homeScore: 5, awayScore: nil) #expect(result.hasScore == false) } @Test("hasScore: false when both are nil") func hasScore_bothNil() { let result = makeResult(homeScore: nil, awayScore: nil) #expect(result.hasScore == false) } // MARK: - Invariant Tests /// - Invariant: hasScore == true implies scoreString != nil @Test("Invariant: hasScore implies scoreString exists") func invariant_hasScoreImpliesScoreString() { let withScore = makeResult(homeScore: 5, awayScore: 3) if withScore.hasScore { #expect(withScore.scoreString != nil) } } /// - Invariant: scoreString != nil implies hasScore @Test("Invariant: scoreString exists implies hasScore") func invariant_scoreStringImpliesHasScore() { let withScore = makeResult(homeScore: 5, awayScore: 3) if withScore.scoreString != nil { #expect(withScore.hasScore) } } } // MARK: - ScoreResolutionResult Tests @Suite("ScoreResolutionResult") struct ScoreResolutionResultTests { // MARK: - Test Data private func makeHistoricalResult() -> HistoricalGameResult { HistoricalGameResult( sport: .mlb, gameDate: TestClock.now, homeTeamAbbrev: "NYY", awayTeamAbbrev: "BOS", homeTeamName: "Yankees", awayTeamName: "Red Sox", homeScore: 5, awayScore: 3, source: .api, providerName: "Test" ) } // MARK: - Specification Tests: isResolved @Test("isResolved: true for resolved case") func isResolved_resolved() { let result = ScoreResolutionResult.resolved(makeHistoricalResult()) #expect(result.isResolved == true) } @Test("isResolved: false for pending case") func isResolved_pending() { let result = ScoreResolutionResult.pending #expect(result.isResolved == false) } @Test("isResolved: false for requiresUserInput case") func isResolved_requiresUserInput() { let result = ScoreResolutionResult.requiresUserInput(reason: "Test reason") #expect(result.isResolved == false) } @Test("isResolved: false for notFound case") func isResolved_notFound() { let result = ScoreResolutionResult.notFound(reason: "No game found") #expect(result.isResolved == false) } // MARK: - Specification Tests: result @Test("result: returns HistoricalGameResult for resolved case") func result_resolved() { let historical = makeHistoricalResult() let result = ScoreResolutionResult.resolved(historical) #expect(result.result != nil) #expect(result.result?.homeTeamAbbrev == "NYY") } @Test("result: returns nil for pending case") func result_pending() { let result = ScoreResolutionResult.pending #expect(result.result == nil) } @Test("result: returns nil for requiresUserInput case") func result_requiresUserInput() { let result = ScoreResolutionResult.requiresUserInput(reason: "Test") #expect(result.result == nil) } @Test("result: returns nil for notFound case") func result_notFound() { let result = ScoreResolutionResult.notFound(reason: "Not found") #expect(result.result == nil) } // MARK: - Invariant Tests /// - Invariant: isResolved == true implies result != nil @Test("Invariant: isResolved implies result exists") func invariant_isResolvedImpliesResult() { let resolved = ScoreResolutionResult.resolved(makeHistoricalResult()) if resolved.isResolved { #expect(resolved.result != nil) } } /// - Invariant: isResolved == false implies result == nil @Test("Invariant: not resolved implies result nil") func invariant_notResolvedImpliesNoResult() { let cases: [ScoreResolutionResult] = [ .pending, .requiresUserInput(reason: "Test"), .notFound(reason: "Test") ] for resolution in cases { if !resolution.isResolved { #expect(resolution.result == nil) } } } } // MARK: - ScoreProviderError Tests @Suite("ScoreProviderError") struct ScoreProviderErrorTests { // MARK: - Specification Tests: errorDescription @Test("errorDescription: networkError includes underlying message") func errorDescription_networkError() { let error = ScoreProviderError.networkError(underlying: "Connection timeout") #expect(error.errorDescription?.contains("timeout") == true) } @Test("errorDescription: rateLimited has description") func errorDescription_rateLimited() { let error = ScoreProviderError.rateLimited #expect(error.errorDescription != nil) #expect(!error.errorDescription!.isEmpty) } @Test("errorDescription: parseError includes message") func errorDescription_parseError() { let error = ScoreProviderError.parseError(message: "Invalid JSON") #expect(error.errorDescription?.contains("Invalid JSON") == true) } @Test("errorDescription: gameNotFound has description") func errorDescription_gameNotFound() { let error = ScoreProviderError.gameNotFound #expect(error.errorDescription != nil) } @Test("errorDescription: unsupportedSport includes sport") func errorDescription_unsupportedSport() { let error = ScoreProviderError.unsupportedSport(.nfl) #expect(error.errorDescription?.contains("NFL") == true) // rawValue is uppercase } @Test("errorDescription: providerUnavailable includes reason") func errorDescription_providerUnavailable() { let error = ScoreProviderError.providerUnavailable(reason: "Maintenance") #expect(error.errorDescription?.contains("Maintenance") == true) } // MARK: - Invariant Tests /// - Invariant: All errors have non-empty descriptions @Test("Invariant: all errors have descriptions") func invariant_allHaveDescriptions() { let errors: [ScoreProviderError] = [ .networkError(underlying: "test"), .rateLimited, .parseError(message: "test"), .gameNotFound, .unsupportedSport(.mlb), .providerUnavailable(reason: "test") ] for error in errors { #expect(error.errorDescription != nil) #expect(!error.errorDescription!.isEmpty) } } }