Files
Sportstime/SportsTimeTests/Services/GameMatcherTests.swift
2026-02-18 13:00:15 -06:00

316 lines
10 KiB
Swift

//
// 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: TestClock.now,
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: TestClock.now,
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)
}
}
}
}