//
//  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 = Calendar.current
        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 = Calendar.current
        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: Date())
        #expect(query.sport == .nba)
    }

    @Test("init: stores team abbreviations")
    func init_storesTeams() {
        let query = HistoricalGameQuery(
            sport: .mlb,
            date: Date(),
            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: Date())

        #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: Date(),
            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: Date(),
            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)
        }
    }
}
