// // GameMatcherTests.swift // SportsTimeTests // // TDD specification tests for GameMatcher types. // import Testing import Foundation import CoreLocation @testable import SportsTime // MARK: - NoMatchReason Tests @Suite("NoMatchReason") struct NoMatchReasonTests { // MARK: - Specification Tests: description /// - Expected Behavior: Each reason has a user-friendly description @Test("description: noStadiumNearby has description") func description_noStadiumNearby() { let reason = NoMatchReason.noStadiumNearby #expect(!reason.description.isEmpty) #expect(reason.description.lowercased().contains("stadium") || reason.description.lowercased().contains("nearby")) } @Test("description: noGamesOnDate has description") func description_noGamesOnDate() { let reason = NoMatchReason.noGamesOnDate #expect(!reason.description.isEmpty) #expect(reason.description.lowercased().contains("game") || reason.description.lowercased().contains("date")) } @Test("description: metadataMissing noLocation has description") func description_metadataMissing_noLocation() { let reason = NoMatchReason.metadataMissing(.noLocation) #expect(!reason.description.isEmpty) #expect(reason.description.lowercased().contains("location")) } @Test("description: metadataMissing noDate has description") func description_metadataMissing_noDate() { let reason = NoMatchReason.metadataMissing(.noDate) #expect(!reason.description.isEmpty) #expect(reason.description.lowercased().contains("date")) } @Test("description: metadataMissing noBoth has description") func description_metadataMissing_noBoth() { let reason = NoMatchReason.metadataMissing(.noBoth) #expect(!reason.description.isEmpty) #expect(reason.description.lowercased().contains("location") || reason.description.lowercased().contains("date")) } // MARK: - Invariant Tests /// - Invariant: All reasons have non-empty descriptions @Test("Invariant: all reasons have non-empty descriptions") func invariant_allHaveDescriptions() { let allReasons: [NoMatchReason] = [ .noStadiumNearby, .noGamesOnDate, .metadataMissing(.noLocation), .metadataMissing(.noDate), .metadataMissing(.noBoth) ] for reason in allReasons { #expect(!reason.description.isEmpty, "Reason should have description") } } } // MARK: - GameMatchResult Tests @Suite("GameMatchResult") struct GameMatchResultTests { private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855) // MARK: - Test Data private func makeGame(id: String = "game_1") -> Game { Game( id: id, homeTeamId: "home_team", awayTeamId: "away_team", stadiumId: "stadium_1", dateTime: Date(), sport: .mlb, season: "2026" ) } private func makeStadium() -> Stadium { Stadium( id: "stadium_1", name: "Test Stadium", city: "Test City", state: "TS", latitude: nycCoord.latitude, longitude: nycCoord.longitude, capacity: 40000, sport: .mlb ) } private func makeTeam(id: String = "team_1") -> Team { Team( id: id, name: "Test Team", abbreviation: "TST", sport: .mlb, city: "Test City", stadiumId: "stadium_1" ) } private func makeCandidate(gameId: String = "game_1") -> GameMatchCandidate { GameMatchCandidate( game: makeGame(id: gameId), stadium: makeStadium(), homeTeam: makeTeam(id: "home"), awayTeam: makeTeam(id: "away"), confidence: PhotoMatchConfidence(spatial: .high, temporal: .exactDay) ) } // MARK: - Specification Tests: hasMatch /// - Expected Behavior: singleMatch returns true for hasMatch @Test("hasMatch: true for singleMatch case") func hasMatch_singleMatch() { let result = GameMatchResult.singleMatch(makeCandidate()) #expect(result.hasMatch == true) } /// - Expected Behavior: multipleMatches returns true for hasMatch @Test("hasMatch: true for multipleMatches case") func hasMatch_multipleMatches() { let candidates = [makeCandidate(gameId: "game_1"), makeCandidate(gameId: "game_2")] let result = GameMatchResult.multipleMatches(candidates) #expect(result.hasMatch == true) } /// - Expected Behavior: noMatches returns false for hasMatch @Test("hasMatch: false for noMatches case") func hasMatch_noMatches() { let result = GameMatchResult.noMatches(.noGamesOnDate) #expect(result.hasMatch == false) } // MARK: - Invariant Tests /// - Invariant: singleMatch and multipleMatches always hasMatch @Test("Invariant: match cases always hasMatch") func invariant_matchCasesHaveMatch() { let single = GameMatchResult.singleMatch(makeCandidate()) let multiple = GameMatchResult.multipleMatches([makeCandidate()]) #expect(single.hasMatch == true) #expect(multiple.hasMatch == true) } /// - Invariant: noMatches never hasMatch @Test("Invariant: noMatches never hasMatch") func invariant_noMatchesNeverHasMatch() { let reasons: [NoMatchReason] = [ .noStadiumNearby, .noGamesOnDate, .metadataMissing(.noLocation) ] for reason in reasons { let result = GameMatchResult.noMatches(reason) #expect(result.hasMatch == false) } } } // MARK: - GameMatchCandidate Tests @Suite("GameMatchCandidate") struct GameMatchCandidateTests { private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855) // MARK: - Test Data private func makeGame() -> Game { Game( id: "game_test", homeTeamId: "home_team", awayTeamId: "away_team", stadiumId: "stadium_1", dateTime: Date(), sport: .mlb, season: "2026" ) } private func makeStadium() -> Stadium { Stadium( id: "stadium_1", name: "Test Stadium", city: "Test City", state: "TS", latitude: nycCoord.latitude, longitude: nycCoord.longitude, capacity: 40000, sport: .mlb ) } private func makeTeam(id: String, name: String, abbreviation: String) -> Team { Team( id: id, name: name, abbreviation: abbreviation, sport: .mlb, city: "Test", stadiumId: "stadium_1" ) } // MARK: - Specification Tests @Test("id: matches game id") func id_matchesGameId() { let game = makeGame() let candidate = GameMatchCandidate( game: game, stadium: makeStadium(), homeTeam: makeTeam(id: "home", name: "Home Team", abbreviation: "HOM"), awayTeam: makeTeam(id: "away", name: "Away Team", abbreviation: "AWY"), confidence: PhotoMatchConfidence(spatial: .high, temporal: .exactDay) ) #expect(candidate.id == game.id) } @Test("matchupDescription: returns abbreviations format") func matchupDescription_format() { let candidate = GameMatchCandidate( game: makeGame(), stadium: makeStadium(), homeTeam: makeTeam(id: "home", name: "Home Team", abbreviation: "HOM"), awayTeam: makeTeam(id: "away", name: "Away Team", abbreviation: "AWY"), confidence: PhotoMatchConfidence(spatial: .high, temporal: .exactDay) ) #expect(candidate.matchupDescription == "AWY @ HOM") } // MARK: - Invariant Tests /// - Invariant: id is always equal to game.id @Test("Invariant: id equals game.id") func invariant_idEqualsGameId() { let game = makeGame() let candidate = GameMatchCandidate( game: game, stadium: makeStadium(), homeTeam: makeTeam(id: "home", name: "Home", abbreviation: "HOM"), awayTeam: makeTeam(id: "away", name: "Away", abbreviation: "AWY"), confidence: PhotoMatchConfidence(spatial: .medium, temporal: .adjacentDay) ) #expect(candidate.id == candidate.game.id) } } // MARK: - PhotoMatchConfidence Tests @Suite("PhotoMatchConfidence") struct PhotoMatchConfidenceTests { // MARK: - Specification Tests: combined @Test("combined: high spatial + exactDay = autoSelect") func combined_highAndExact() { let confidence = PhotoMatchConfidence(spatial: .high, temporal: .exactDay) #expect(confidence.combined == .autoSelect) } @Test("combined: medium spatial + adjacentDay = userConfirm") func combined_mediumAndAdjacentDay() { let confidence = PhotoMatchConfidence(spatial: .medium, temporal: .adjacentDay) #expect(confidence.combined == .userConfirm) } @Test("combined: low spatial = manualOnly") func combined_lowSpatial() { let confidence = PhotoMatchConfidence(spatial: .low, temporal: .exactDay) #expect(confidence.combined == .manualOnly) } // MARK: - Invariant Tests /// - Invariant: combined is always derived from spatial and temporal @Test("Invariant: combined is deterministic from spatial and temporal") func invariant_combinedDeterministic() { let spatials: [MatchConfidence] = [.high, .medium, .low] let temporals: [TemporalConfidence] = [.exactDay, .adjacentDay, .outOfRange] for spatial in spatials { for temporal in temporals { let confidence = PhotoMatchConfidence(spatial: spatial, temporal: temporal) let expected = CombinedConfidence.combine(spatial: spatial, temporal: temporal) #expect(confidence.combined == expected) } } } }