//
//  AchievementEngineTests.swift
//  SportsTimeTests
//
//  TDD specification tests for AchievementEngine types.
//

import Testing
import Foundation
import SwiftUI
@testable import SportsTime

// MARK: - AchievementDelta Tests

@Suite("AchievementDelta")
struct AchievementDeltaTests {

    // MARK: - Test Data

    private func makeDefinition(id: String = "test_achievement") -> AchievementDefinition {
        AchievementDefinition(
            id: id,
            name: "Test Achievement",
            description: "Test description",
            category: .count,
            sport: nil,
            iconName: "star.fill",
            iconColor: .blue,
            requirement: .firstVisit
        )
    }

    private func makeDelta(
        newlyEarned: [AchievementDefinition] = [],
        revoked: [AchievementDefinition] = [],
        stillEarned: [AchievementDefinition] = []
    ) -> AchievementDelta {
        AchievementDelta(
            newlyEarned: newlyEarned,
            revoked: revoked,
            stillEarned: stillEarned
        )
    }

    // MARK: - Specification Tests: hasChanges

    /// - Expected Behavior: true when newlyEarned is not empty
    @Test("hasChanges: true when newlyEarned not empty")
    func hasChanges_newlyEarned() {
        let delta = makeDelta(newlyEarned: [makeDefinition()])
        #expect(delta.hasChanges == true)
    }

    /// - Expected Behavior: true when revoked is not empty
    @Test("hasChanges: true when revoked not empty")
    func hasChanges_revoked() {
        let delta = makeDelta(revoked: [makeDefinition()])
        #expect(delta.hasChanges == true)
    }

    /// - Expected Behavior: false when both are empty (stillEarned doesn't count)
    @Test("hasChanges: false when newlyEarned and revoked both empty")
    func hasChanges_bothEmpty() {
        let delta = makeDelta(newlyEarned: [], revoked: [], stillEarned: [makeDefinition()])
        #expect(delta.hasChanges == false)
    }

    /// - Expected Behavior: true when both newlyEarned and revoked have items
    @Test("hasChanges: true when both newlyEarned and revoked have items")
    func hasChanges_bothHaveItems() {
        let delta = makeDelta(
            newlyEarned: [makeDefinition(id: "new")],
            revoked: [makeDefinition(id: "old")]
        )
        #expect(delta.hasChanges == true)
    }

    // MARK: - Invariant Tests

    /// - Invariant: hasChanges == (!newlyEarned.isEmpty || !revoked.isEmpty)
    @Test("Invariant: hasChanges formula is correct")
    func invariant_hasChangesFormula() {
        let testCases: [(newlyEarned: [AchievementDefinition], revoked: [AchievementDefinition])] = [
            ([], []),
            ([makeDefinition()], []),
            ([], [makeDefinition()]),
            ([makeDefinition()], [makeDefinition()])
        ]

        for (newlyEarned, revoked) in testCases {
            let delta = makeDelta(newlyEarned: newlyEarned, revoked: revoked)
            let expected = !newlyEarned.isEmpty || !revoked.isEmpty
            #expect(delta.hasChanges == expected)
        }
    }
}

// MARK: - AchievementProgress Tests

@Suite("AchievementProgress")
struct AchievementProgressTests {

    // MARK: - Test Data

    private func makeDefinition() -> AchievementDefinition {
        AchievementDefinition(
            id: "test_progress",
            name: "Test Progress",
            description: "Test",
            category: .count,
            sport: nil,
            iconName: "star",
            iconColor: .blue,
            requirement: .visitCount(10)
        )
    }

    private func makeProgress(
        currentProgress: Int = 5,
        totalRequired: Int = 10,
        hasStoredAchievement: Bool = false,
        earnedAt: Date? = nil
    ) -> AchievementProgress {
        AchievementProgress(
            definition: makeDefinition(),
            currentProgress: currentProgress,
            totalRequired: totalRequired,
            hasStoredAchievement: hasStoredAchievement,
            earnedAt: earnedAt
        )
    }

    // MARK: - Specification Tests: isEarned

    /// - Expected Behavior: true when hasStoredAchievement is true
    @Test("isEarned: true when hasStoredAchievement")
    func isEarned_hasStoredAchievement() {
        let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: true)
        #expect(progress.isEarned == true)
    }

    /// - Expected Behavior: true when progress >= total (and total > 0)
    @Test("isEarned: true when progress equals total")
    func isEarned_progressEqualsTotal() {
        let progress = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: false)
        #expect(progress.isEarned == true)
    }

    /// - Expected Behavior: true when progress > total
    @Test("isEarned: true when progress exceeds total")
    func isEarned_progressExceedsTotal() {
        let progress = makeProgress(currentProgress: 15, totalRequired: 10, hasStoredAchievement: false)
        #expect(progress.isEarned == true)
    }

    /// - Expected Behavior: false when progress < total and no stored achievement
    @Test("isEarned: false when progress less than total")
    func isEarned_progressLessThanTotal() {
        let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: false)
        #expect(progress.isEarned == false)
    }

    /// - Expected Behavior: false when total is 0 (edge case)
    @Test("isEarned: false when total is 0")
    func isEarned_totalZero() {
        let progress = makeProgress(currentProgress: 5, totalRequired: 0, hasStoredAchievement: false)
        #expect(progress.isEarned == false)
    }

    // MARK: - Specification Tests: progressPercentage

    /// - Expected Behavior: Returns current/total as Double
    @Test("progressPercentage: returns correct ratio")
    func progressPercentage_correctRatio() {
        let progress = makeProgress(currentProgress: 5, totalRequired: 10)
        #expect(progress.progressPercentage == 0.5)
    }

    @Test("progressPercentage: 100% when complete")
    func progressPercentage_complete() {
        let progress = makeProgress(currentProgress: 10, totalRequired: 10)
        #expect(progress.progressPercentage == 1.0)
    }

    @Test("progressPercentage: 0% when no progress")
    func progressPercentage_noProgress() {
        let progress = makeProgress(currentProgress: 0, totalRequired: 10)
        #expect(progress.progressPercentage == 0.0)
    }

    @Test("progressPercentage: 0 when total is 0")
    func progressPercentage_totalZero() {
        let progress = makeProgress(currentProgress: 5, totalRequired: 0)
        #expect(progress.progressPercentage == 0.0)
    }

    @Test("progressPercentage: can exceed 100%")
    func progressPercentage_exceedsHundred() {
        let progress = makeProgress(currentProgress: 15, totalRequired: 10)
        #expect(progress.progressPercentage == 1.5)
    }

    // MARK: - Specification Tests: progressText

    /// - Expected Behavior: "Completed" when earned
    @Test("progressText: Completed when earned via stored")
    func progressText_completedViaStored() {
        let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: true)
        #expect(progress.progressText == "Completed")
    }

    @Test("progressText: Completed when earned via progress")
    func progressText_completedViaProgress() {
        let progress = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: false)
        #expect(progress.progressText == "Completed")
    }

    /// - Expected Behavior: "current/total" when not earned
    @Test("progressText: shows fraction when not earned")
    func progressText_showsFraction() {
        let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: false)
        #expect(progress.progressText == "5/10")
    }

    @Test("progressText: shows 0/total when no progress")
    func progressText_zeroProgress() {
        let progress = makeProgress(currentProgress: 0, totalRequired: 10, hasStoredAchievement: false)
        #expect(progress.progressText == "0/10")
    }

    // MARK: - Specification Tests: id

    /// - Expected Behavior: id returns definition.id
    @Test("id: returns definition id")
    func id_returnsDefinitionId() {
        let progress = makeProgress()
        #expect(progress.id == "test_progress")
    }

    // MARK: - Invariant Tests

    /// - Invariant: progressPercentage == Double(current) / Double(total) when total > 0
    @Test("Invariant: progressPercentage calculation correct")
    func invariant_progressPercentageFormula() {
        let testCases: [(current: Int, total: Int)] = [
            (0, 10),
            (5, 10),
            (10, 10),
            (15, 10),
            (1, 3)
        ]

        for (current, total) in testCases {
            let progress = makeProgress(currentProgress: current, totalRequired: total)
            let expected = Double(current) / Double(total)
            #expect(abs(progress.progressPercentage - expected) < 0.0001)
        }
    }

    /// - Invariant: isEarned implies progressText == "Completed"
    @Test("Invariant: isEarned implies Completed text")
    func invariant_earnedImpliesCompletedText() {
        let earned = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: true)
        if earned.isEarned {
            #expect(earned.progressText == "Completed")
        }
    }
}
