@testable import AppStartUpClient
import OSLog
import Testing

@Suite("AppStartUpClient Tests")
struct AppStartUpClientTests {
    @Test("Manager throws error on critical failure events")
    func testCriticalFailureEvents() async throws {
        let failedSDKs: [SDKIdentifier] = [.braze, .clerk]
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.critical),
            .criticalFailure(failedSDKs),
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error to be thrown")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            #expect(failures == failedSDKs)
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    @Test("Manager completes successfully on canShowUI event")
    func testCanShowUIEvent() async throws {
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .phaseStarted(.critical),
            .canShowUI,
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        // Should complete successfully without throwing
        try await sut.startup()
    }

    @Test("Manager completes successfully with metrics")
    func testCompletionWithMetrics() async throws {
        let testMetrics = AppStartUpMetrics(
            startTime: Date(),
            phaseCompletionTimes: [.essential: Date(), .complete: Date()],
            totalDuration: 2.5
        )
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .canShowUI,
            .completed(testMetrics),
        ]
        let sut = makeSUT(events: events)

        // Should complete successfully without throwing
        try await sut.startup()
    }

    @Test("Manager throws error on first critical failure")
    func testFirstCriticalFailureThrows() async throws {
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .criticalFailure([.firebase]),
            .canShowUI,
            .phaseStarted(.important),
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error to be thrown")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            #expect(failures == [.firebase])
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    @Test("Manager completes successfully when no critical failures")
    func testSuccessfulCompletion() async throws {
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .phaseStarted(.critical),
            .phaseStarted(.important),
            .phaseStarted(.optional),
            .canShowUI,
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        // Should complete successfully without throwing
        try await sut.startup()
    }

    @Test("Manager throws error on first critical failure with multiple failures")
    func testMultipleCriticalFailuresThrowsFirst() async throws {
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.critical),
            .criticalFailure([.braze]),
            .criticalFailure([.clerk]),
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error to be thrown")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            // Should throw on first critical failure
            #expect(failures == [.braze])
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    // MARK: - Timeout Tests

    @Test("Manager handles SDK timeout as critical failure")
    func testSdkTimeoutCausesCriticalFailure() async throws {
        let timeoutSDKs: [SDKIdentifier] = [.clerk]
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .phaseStarted(.critical),
            .criticalFailure(timeoutSDKs), // Simulates timeout failure
            .canShowUI,
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error due to timeout")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            #expect(failures == timeoutSDKs)
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    @Test("Manager handles multiple SDK timeouts in same phase")
    func testMultipleConcurrentTimeouts() async throws {
        let timeoutSDKs: [SDKIdentifier] = [.braze, .clerk, .statsig]
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.critical),
            .criticalFailure(timeoutSDKs), // Multiple SDKs timeout together
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error for multiple timeouts")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            #expect(failures.hasSameElements(as: timeoutSDKs))
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    @Test("Manager handles timeout in essential phase")
    func testEssentialPhaseTimeout() async throws {
        let timeoutSDKs: [SDKIdentifier] = [.firebase]
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .criticalFailure(timeoutSDKs), // Firebase timeout in essential phase
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error for essential SDK timeout")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            #expect(failures == timeoutSDKs)
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    @Test("Manager continues after optional SDK timeout")
    func testOptionalSDKTimeoutDoesNotFailStartup() async throws {
        // Optional SDK timeouts should not cause critical failures
        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .phaseStarted(.critical),
            .canShowUI,
            .phaseStarted(.optional),
            // No critical failure for optional SDK timeouts
            .completed(.mock),
        ]
        let sut = makeSUT(events: events)

        // Should complete successfully without throwing
        try await sut.startup()
    }

    @Test("Manager tracks timeout metrics in completed event")
    func testTimeoutMetricsRecorded() async throws {
        let testMetrics = AppStartUpMetrics.makeTestMetrics(
            totalDuration: 5.5,
            successfulSDKs: [.firebase, .revenueCat, .statsig],
            failedSDKs: [
                .braze: "Timeout after 10.0 seconds",
                .clerk: "Timeout after 15.0 seconds",
            ],
            phaseCompletions: [.essential: 1.0, .critical: 5.0]
        )

        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .phaseStarted(.critical),
            .criticalFailure([.braze, .clerk]), // Timeouts
            .canShowUI,
            .completed(testMetrics),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            #expect(failures.hasSameElements(as: [.braze, .clerk]))
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    @Test("Manager completes without timeout when SDK finishes before timeout")
    func testSdkCompletesBeforeTimeout() async throws {
        // Simulates SDKs completing successfully before timeout
        let testMetrics = AppStartUpMetrics.makeTestMetrics(
            totalDuration: 3.0,
            successfulSDKs: [.firebase, .braze, .clerk, .revenueCat, .statsig],
            phaseCompletions: [
                .essential: 0.5,
                .critical: 2.0,
                .complete: 3.0,
            ]
        )

        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .phaseStarted(.critical),
            .canShowUI,
            .phaseStarted(.important),
            .phaseStarted(.optional),
            .completed(testMetrics), // All SDKs successful
        ]
        let sut = makeSUT(events: events)

        // Should complete successfully without any timeout errors
        try await sut.startup()
    }

    @Test("Manager handles mixed timeout and success scenarios")
    func testMixedTimeoutAndSuccessScenarios() async throws {
        // Some SDKs timeout, others succeed
        let testMetrics = AppStartUpMetrics.makeTestMetrics(
            totalDuration: 10.5,
            successfulSDKs: [.firebase, .clerk, .revenueCat],
            failedSDKs: [
                .braze: "Timeout after 10.0 seconds",
                .statsig: "Timeout after 10.0 seconds",
            ],
            phaseCompletions: [.essential: 1.0, .critical: 10.0]
        )

        let events = [
            AppStartUpOrchestrationEvent.phaseStarted(.essential),
            .phaseStarted(.critical),
            .criticalFailure([.braze, .statsig]), // Only the timed out SDKs
            .canShowUI,
            .completed(testMetrics),
        ]
        let sut = makeSUT(events: events)

        do {
            try await sut.startup()
            Issue.record("Expected criticalSdkInitFailures error")
        } catch AppStartUpError.criticalSdkInitFailures(let failures) {
            #expect(failures.hasSameElements(as: [.braze, .statsig]))
        } catch {
            Issue.record("Unexpected error: \(error)")
        }
    }

    // MARK: - Helper Methods

    private func makeSUT(
        events: [AppStartUpOrchestrationEvent]
    ) -> AppStartUpClient {
        let orchestrator = AppStartUpOrchestrator.Mock(events: events)
        return AppStartUpClient(
            logger: Logger(subsystem: "test", category: "AppStartUpClientTests"),
            orchestratorFactory: { orchestrator }
        )
    }
}

// MARK: - Test Helpers

private extension AppStartUpMetrics {
    static func makeTestMetrics(
        startTime: Date = Date(),
        totalDuration: TimeInterval,
        successfulSDKs: Set<SDKIdentifier> = [],
        failedSDKs: [SDKIdentifier: String] = [:],
        phaseCompletions: [AppStartUpPhase: TimeInterval] = [:]
    ) -> AppStartUpMetrics {
        var sdkMetrics: [SDKIdentifier: SDKMetric] = [:]
        let errors: [SDKIdentifier: String] = failedSDKs

        // Add successful SDKs
        for sdk in successfulSDKs {
            let sdkStartOffset = sdk == .firebase ? 0.0 : 1.0
            let sdkDuration = sdk == .firebase ? 0.5 : 1.0

            sdkMetrics[sdk] = SDKMetric(
                identifier: sdk,
                startTime: startTime.addingTimeInterval(sdkStartOffset),
                endTime: startTime.addingTimeInterval(sdkStartOffset + sdkDuration),
                success: true,
                retryCount: 0,
                threadName: sdk.requiresMainThread ? "main" : "background"
            )
        }

        // Add failed SDKs
        for (sdk, _) in failedSDKs {
            let sdkStartOffset = sdk == .firebase ? 0.0 : 1.0

            sdkMetrics[sdk] = SDKMetric(
                identifier: sdk,
                startTime: startTime.addingTimeInterval(sdkStartOffset),
                endTime: nil,
                success: false,
                retryCount: 0,
                threadName: sdk.requiresMainThread ? "main" : "background"
            )
        }

        // Build phase completion times
        var phaseCompletionTimes: [AppStartUpPhase: Date] = [:]
        for (phase, offset) in phaseCompletions {
            phaseCompletionTimes[phase] = startTime.addingTimeInterval(offset)
        }

        return AppStartUpMetrics(
            startTime: startTime,
            phaseCompletionTimes: phaseCompletionTimes,
            sdkMetrics: sdkMetrics,
            totalDuration: totalDuration,
            errors: errors
        )
    }
}

// Helper to compare SDK arrays regardless of order
private extension Array where Element == SDKIdentifier {
    func hasSameElements(as other: [SDKIdentifier]) -> Bool {
        self.sorted { $0.rawValue < $1.rawValue } == other.sorted { $0.rawValue < $1.rawValue }
    }
}
