//
//  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)
            }
        }
    }
}
