//
//  ProgressTests.swift
//  SportsTimeTests
//
//  TDD specification tests for Progress models (LeagueProgress, DivisionProgress, etc.).
//

import Testing
import Foundation
@testable import SportsTime

@Suite("LeagueProgress")
struct LeagueProgressTests {

    // MARK: - Test Data

    private func makeLeagueProgress(visited: Int, total: Int) -> LeagueProgress {
        LeagueProgress(
            sport: .mlb,
            totalStadiums: total,
            visitedStadiums: visited,
            stadiumsVisited: [],
            stadiumsRemaining: []
        )
    }

    // MARK: - Specification Tests: completionPercentage

    @Test("completionPercentage: 50% when half visited")
    func completionPercentage_half() {
        let progress = makeLeagueProgress(visited: 15, total: 30)

        #expect(progress.completionPercentage == 50.0)
    }

    @Test("completionPercentage: 100% when all visited")
    func completionPercentage_complete() {
        let progress = makeLeagueProgress(visited: 30, total: 30)

        #expect(progress.completionPercentage == 100.0)
    }

    @Test("completionPercentage: 0% when none visited")
    func completionPercentage_zero() {
        let progress = makeLeagueProgress(visited: 0, total: 30)

        #expect(progress.completionPercentage == 0.0)
    }

    @Test("completionPercentage: 0 when total is 0")
    func completionPercentage_totalZero() {
        let progress = makeLeagueProgress(visited: 0, total: 0)

        #expect(progress.completionPercentage == 0)
    }

    // MARK: - Specification Tests: progressFraction

    @Test("progressFraction: 0.5 when half visited")
    func progressFraction_half() {
        let progress = makeLeagueProgress(visited: 15, total: 30)

        #expect(progress.progressFraction == 0.5)
    }

    @Test("progressFraction: 1.0 when complete")
    func progressFraction_complete() {
        let progress = makeLeagueProgress(visited: 30, total: 30)

        #expect(progress.progressFraction == 1.0)
    }

    @Test("progressFraction: 0 when total is 0")
    func progressFraction_totalZero() {
        let progress = makeLeagueProgress(visited: 5, total: 0)

        #expect(progress.progressFraction == 0)
    }

    // MARK: - Specification Tests: isComplete

    @Test("isComplete: true when visited >= total and total > 0")
    func isComplete_true() {
        let progress = makeLeagueProgress(visited: 30, total: 30)

        #expect(progress.isComplete == true)
    }

    @Test("isComplete: false when visited < total")
    func isComplete_false() {
        let progress = makeLeagueProgress(visited: 29, total: 30)

        #expect(progress.isComplete == false)
    }

    @Test("isComplete: false when total is 0")
    func isComplete_totalZero() {
        let progress = makeLeagueProgress(visited: 0, total: 0)

        #expect(progress.isComplete == false)
    }

    // MARK: - Specification Tests: progressDescription

    @Test("progressDescription: formats as visited/total")
    func progressDescription_format() {
        let progress = makeLeagueProgress(visited: 15, total: 30)

        #expect(progress.progressDescription == "15/30")
    }

    // MARK: - Invariant Tests

    @Test("Invariant: completionPercentage in range [0, 100]")
    func invariant_percentageRange() {
        let testCases = [
            (visited: 0, total: 30),
            (visited: 15, total: 30),
            (visited: 30, total: 30),
            (visited: 0, total: 0),
        ]

        for (visited, total) in testCases {
            let progress = makeLeagueProgress(visited: visited, total: total)
            #expect(progress.completionPercentage >= 0)
            #expect(progress.completionPercentage <= 100)
        }
    }

    @Test("Invariant: progressFraction in range [0, 1]")
    func invariant_fractionRange() {
        let testCases = [
            (visited: 0, total: 30),
            (visited: 15, total: 30),
            (visited: 30, total: 30),
            (visited: 0, total: 0),
        ]

        for (visited, total) in testCases {
            let progress = makeLeagueProgress(visited: visited, total: total)
            #expect(progress.progressFraction >= 0)
            #expect(progress.progressFraction <= 1)
        }
    }

    @Test("Invariant: isComplete requires total > 0")
    func invariant_isCompleteRequiresTotal() {
        // Can't be complete with 0 total
        let progress = makeLeagueProgress(visited: 0, total: 0)
        #expect(progress.isComplete == false)
    }
}

// MARK: - DivisionProgress Tests

@Suite("DivisionProgress")
struct DivisionProgressTests {

    private func makeDivisionProgress(visited: Int, total: Int) -> DivisionProgress {
        let division = Division(
            id: "test_div",
            name: "Test Division",
            conference: "Test Conference",
            conferenceId: "test_conf",
            sport: .mlb,
            teamCanonicalIds: []
        )

        return DivisionProgress(
            division: division,
            totalStadiums: total,
            visitedStadiums: visited,
            stadiumsVisited: [],
            stadiumsRemaining: []
        )
    }

    @Test("completionPercentage: calculates correctly")
    func completionPercentage() {
        let progress = makeDivisionProgress(visited: 3, total: 5)

        #expect(progress.completionPercentage == 60.0)
    }

    @Test("progressFraction: calculates correctly")
    func progressFraction() {
        let progress = makeDivisionProgress(visited: 3, total: 5)

        #expect(progress.progressFraction == 0.6)
    }

    @Test("isComplete: true when all visited")
    func isComplete() {
        let complete = makeDivisionProgress(visited: 5, total: 5)
        let incomplete = makeDivisionProgress(visited: 4, total: 5)

        #expect(complete.isComplete == true)
        #expect(incomplete.isComplete == false)
    }
}

// MARK: - ConferenceProgress Tests

@Suite("ConferenceProgress")
struct ConferenceProgressTests {

    private func makeConferenceProgress(visited: Int, total: Int) -> ConferenceProgress {
        let conference = Conference(
            id: "test_conf",
            name: "Test Conference",
            abbreviation: "TC",
            sport: .mlb,
            divisionIds: []
        )

        return ConferenceProgress(
            conference: conference,
            totalStadiums: total,
            visitedStadiums: visited,
            divisionProgress: []
        )
    }

    @Test("completionPercentage: calculates correctly")
    func completionPercentage() {
        let progress = makeConferenceProgress(visited: 10, total: 15)

        #expect(abs(progress.completionPercentage - 66.666) < 0.01)
    }

    @Test("isComplete: true when all visited")
    func isComplete() {
        let complete = makeConferenceProgress(visited: 15, total: 15)
        let incomplete = makeConferenceProgress(visited: 14, total: 15)

        #expect(complete.isComplete == true)
        #expect(incomplete.isComplete == false)
    }
}

// MARK: - OverallProgress Tests

@Suite("OverallProgress")
struct OverallProgressTests {

    @Test("overallPercentage: calculates correctly")
    func overallPercentage() {
        let progress = OverallProgress(
            leagueProgress: [],
            totalVisits: 50,
            uniqueStadiumsVisited: 46,
            totalStadiumsAcrossLeagues: 92,
            achievementsEarned: 10,
            totalAchievements: 50
        )

        #expect(progress.overallPercentage == 50.0)
    }

    @Test("overallPercentage: 0 when no stadiums")
    func overallPercentage_zero() {
        let progress = OverallProgress(
            leagueProgress: [],
            totalVisits: 0,
            uniqueStadiumsVisited: 0,
            totalStadiumsAcrossLeagues: 0,
            achievementsEarned: 0,
            totalAchievements: 0
        )

        #expect(progress.overallPercentage == 0)
    }

    @Test("progress(for:): finds league progress")
    func progressForSport() {
        let mlbProgress = LeagueProgress(
            sport: .mlb,
            totalStadiums: 30,
            visitedStadiums: 15,
            stadiumsVisited: [],
            stadiumsRemaining: []
        )

        let overall = OverallProgress(
            leagueProgress: [mlbProgress],
            totalVisits: 15,
            uniqueStadiumsVisited: 15,
            totalStadiumsAcrossLeagues: 92,
            achievementsEarned: 5,
            totalAchievements: 50
        )

        let found = overall.progress(for: .mlb)

        #expect(found != nil)
        #expect(found?.visitedStadiums == 15)
    }

    @Test("progress(for:): returns nil for missing sport")
    func progressForSport_notFound() {
        let overall = OverallProgress(
            leagueProgress: [],
            totalVisits: 0,
            uniqueStadiumsVisited: 0,
            totalStadiumsAcrossLeagues: 92,
            achievementsEarned: 0,
            totalAchievements: 50
        )

        #expect(overall.progress(for: .mlb) == nil)
    }
}

// MARK: - StadiumVisitStatus Tests

@Suite("StadiumVisitStatus")
struct StadiumVisitStatusTests {

    private func makeVisitSummary(date: Date) -> VisitSummary {
        VisitSummary(
            id: UUID(),
            stadium: Stadium(
                id: "stadium1",
                name: "Test Stadium",
                city: "Test City",
                state: "TS",
                latitude: 40.0,
                longitude: -74.0,
                capacity: 40000,
                sport: .mlb
            ),
            visitDate: date,
            visitType: .game,
            sport: .mlb,
            homeTeamName: "Home Team",
            awayTeamName: "Away Team",
            score: "5-3",
            photoCount: 0,
            notes: nil
        )
    }

    // MARK: - Specification Tests: isVisited

    @Test("isVisited: true for visited status")
    func isVisited_true() {
        let visit = makeVisitSummary(date: Date())
        let status = StadiumVisitStatus.visited(visits: [visit])

        #expect(status.isVisited == true)
    }

    @Test("isVisited: false for notVisited status")
    func isVisited_false() {
        let status = StadiumVisitStatus.notVisited

        #expect(status.isVisited == false)
    }

    // MARK: - Specification Tests: visitCount

    @Test("visitCount: returns count of visits")
    func visitCount_multiple() {
        let visits = [
            makeVisitSummary(date: Date()),
            makeVisitSummary(date: Date()),
            makeVisitSummary(date: Date()),
        ]
        let status = StadiumVisitStatus.visited(visits: visits)

        #expect(status.visitCount == 3)
    }

    @Test("visitCount: 0 for notVisited")
    func visitCount_notVisited() {
        let status = StadiumVisitStatus.notVisited

        #expect(status.visitCount == 0)
    }

    // MARK: - Specification Tests: latestVisit

    @Test("latestVisit: returns visit with max date")
    func latestVisit_maxDate() {
        let calendar = Calendar.current
        let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))!
        let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))!
        let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))!

        let visits = [
            makeVisitSummary(date: date1),
            makeVisitSummary(date: date2),
            makeVisitSummary(date: date3),
        ]
        let status = StadiumVisitStatus.visited(visits: visits)

        #expect(status.latestVisit?.visitDate == date2)
    }

    @Test("latestVisit: nil for notVisited")
    func latestVisit_notVisited() {
        let status = StadiumVisitStatus.notVisited

        #expect(status.latestVisit == nil)
    }

    // MARK: - Specification Tests: firstVisit

    @Test("firstVisit: returns visit with min date")
    func firstVisit_minDate() {
        let calendar = Calendar.current
        let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))!
        let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))!
        let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))!

        let visits = [
            makeVisitSummary(date: date1),
            makeVisitSummary(date: date2),
            makeVisitSummary(date: date3),
        ]
        let status = StadiumVisitStatus.visited(visits: visits)

        #expect(status.firstVisit?.visitDate == date1)
    }

    @Test("firstVisit: nil for notVisited")
    func firstVisit_notVisited() {
        let status = StadiumVisitStatus.notVisited

        #expect(status.firstVisit == nil)
    }

    // MARK: - Invariant Tests

    @Test("Invariant: visitCount == 0 for notVisited")
    func invariant_visitCountZeroForNotVisited() {
        let status = StadiumVisitStatus.notVisited

        #expect(status.visitCount == 0)
    }

    @Test("Invariant: latestVisit and firstVisit are nil for notVisited")
    func invariant_visitsNilForNotVisited() {
        let status = StadiumVisitStatus.notVisited

        #expect(status.latestVisit == nil)
        #expect(status.firstVisit == nil)
    }
}

// MARK: - VisitSummary Tests

@Suite("VisitSummary")
struct VisitSummaryTests {

    private func makeVisitSummary(
        homeTeam: String? = "Home",
        awayTeam: String? = "Away"
    ) -> VisitSummary {
        VisitSummary(
            id: UUID(),
            stadium: Stadium(
                id: "stadium1",
                name: "Test Stadium",
                city: "Test City",
                state: "TS",
                latitude: 40.0,
                longitude: -74.0,
                capacity: 40000,
                sport: .mlb
            ),
            visitDate: Date(),
            visitType: .game,
            sport: .mlb,
            homeTeamName: homeTeam,
            awayTeamName: awayTeam,
            score: "5-3",
            photoCount: 0,
            notes: nil
        )
    }

    @Test("matchup: returns 'away @ home' format")
    func matchup_format() {
        let summary = makeVisitSummary(homeTeam: "Red Sox", awayTeam: "Yankees")

        #expect(summary.matchup == "Yankees @ Red Sox")
    }

    @Test("matchup: nil when home team is nil")
    func matchup_nilHome() {
        let summary = makeVisitSummary(homeTeam: nil, awayTeam: "Yankees")

        #expect(summary.matchup == nil)
    }

    @Test("matchup: nil when away team is nil")
    func matchup_nilAway() {
        let summary = makeVisitSummary(homeTeam: "Red Sox", awayTeam: nil)

        #expect(summary.matchup == nil)
    }
}

// MARK: - ProgressCardData Tests

@Suite("ProgressCardData")
struct ProgressCardDataTests {

    @Test("title: includes sport display name")
    func title_includesSport() {
        let progress = LeagueProgress(
            sport: .mlb,
            totalStadiums: 30,
            visitedStadiums: 15,
            stadiumsVisited: [],
            stadiumsRemaining: []
        )

        let cardData = ProgressCardData(
            sport: .mlb,
            progress: progress,
            username: "TestUser",
            includeMap: true,
            showDetailedStats: true
        )

        #expect(cardData.title == "Major League Baseball Stadium Quest")
    }

    @Test("subtitle: shows visited of total")
    func subtitle_format() {
        let progress = LeagueProgress(
            sport: .mlb,
            totalStadiums: 30,
            visitedStadiums: 15,
            stadiumsVisited: [],
            stadiumsRemaining: []
        )

        let cardData = ProgressCardData(
            sport: .mlb,
            progress: progress,
            username: nil,
            includeMap: false,
            showDetailedStats: false
        )

        #expect(cardData.subtitle == "15 of 30 Stadiums")
    }

    @Test("percentageText: formats without decimals")
    func percentageText_format() {
        let progress = LeagueProgress(
            sport: .mlb,
            totalStadiums: 30,
            visitedStadiums: 15,
            stadiumsVisited: [],
            stadiumsRemaining: []
        )

        let cardData = ProgressCardData(
            sport: .mlb,
            progress: progress,
            username: nil,
            includeMap: false,
            showDetailedStats: false
        )

        #expect(cardData.percentageText == "50%")
    }
}
