@testable import APIClient
import ComposableArchitecture
import Dependencies
@testable import FeatureHookDownload
import Foundation
import GenAPI
import Testing

@Suite("HookDownloadClient Tests", .serialized)
struct HookDownloadClientTests {
    // MARK: - Successful Download Flow Tests

    @Suite("Successful Download Flow", .serialized)
    struct SuccessfulDownloadFlow {
        @Test("Complete successful download flow with first poll success")
        func testSuccessfulDownloadFlowFirstPoll() async throws {
            let testHook = Hook.mock(id: "test-hook-123")
            let taskId = "task-001"
            let downloadURL = URL(string: "https://example.com/video.mp4")!
            let expectedData = Data("mock video data".utf8)
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: downloadURL,
                    pollsUntilReady: 1,
                    videoData: expectedData,
                    shouldSucceed: true
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            // Start collecting events
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            // Trigger download
            client.triggerHookDownload(testHook, .download, nil)
            // Wait for events to process
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Verify events emitted in correct order
            #expect(events.count >= 4)
            // First event: started fetching download URL
            guard case .startedFetchingDownloadURL(let hook1, let taskId1, _, _) = events[0] else {
                Issue.record("Expected startedFetchingDownloadURL event")
                return
            }
            #expect(hook1.id == testHook.id)
            // Second event: polled for download URL
            guard case .polledForDownloadURL(let hook2, let taskId2, let pollingCount, _, _) = events[1] else {
                Issue.record("Expected polledForDownloadURL event")
                return
            }
            #expect(hook2.id == testHook.id)
            #expect(pollingCount == 0)
            // Third event: download URL ready
            guard case .downloadURLReady(let hook3, let taskId3, let url, _, _) = events[2] else {
                Issue.record("Expected downloadURLReady event")
                return
            }
            #expect(hook3.id == testHook.id)
            #expect(url == downloadURL)
            // Fourth event: started downloading hook
            guard case .startedDownloadingHook(let hook4, let taskId4, let url4, _, _) = events[3] else {
                Issue.record("Expected startedDownloadingHook event")
                return
            }
            #expect(hook4.id == testHook.id)
            #expect(url4 == downloadURL)
        }

        @Test("Successful download after multiple polling attempts")
        func testSuccessfulDownloadAfterMultiplePolls() async throws {
            let testHook = Hook.mock(id: "test-hook-456")
            let taskId = "task-002"
            let downloadURL = URL(string: "https://example.com/video2.mp4")!
            let expectedData = Data("mock video data 2".utf8)
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: downloadURL,
                    pollsUntilReady: 3, // Success after 3 polls
                    videoData: expectedData,
                    shouldSucceed: true
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            client.triggerHookDownload(testHook, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Verify multiple poll events were emitted
            let pollEvents = events.compactMap { event -> Int? in
                if case .polledForDownloadURL(_, _, let count, _, _) = event {
                    return count
                }
                return nil
            }
            #expect(pollEvents.count == 3)
            #expect(pollEvents == [0, 1, 2])
            // Verify download eventually succeeded
            let downloadURLReadyEvents = events.filter { event in
                if case .downloadURLReady = event { return true }
                return false
            }
            #expect(downloadURLReadyEvents.count == 1)
        }
    }

    // MARK: - Timeout Tests

    @Suite("Timeout Scenarios", .serialized)
    struct TimeoutScenarios {
        @Test("Timeout when polling for download URL")
        func testTimeoutWhenPolling() async throws {
            let testHook = Hook.mock(id: "test-hook-timeout")
            let taskId = "task-timeout-001"
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: nil, // Never returns URL
                    pollsUntilReady: 100, // More than timeout allows
                    videoData: Data(),
                    shouldSucceed: false
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            client.triggerHookDownload(testHook, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Verify failure event was emitted
            let failureEvents = events.compactMap { event -> HookDownloadError? in
                if case .hookDownloadFailed(_, _, let error, _) = event {
                    return error
                }
                return nil
            }
            #expect(failureEvents.count == 1)
            if case .timedOut = failureEvents.first {
                // Success - got timeout error
            } else {
                Issue.record("Expected timedOut error")
            }
        }
    }

    // MARK: - Error Scenarios

    @Suite("Error Handling", .serialized)
    struct ErrorHandling {
        @Test("API error when fetching download URL")
        func testAPIError() async throws {
            let testHook = Hook.mock(id: "test-hook-api-error")
            let taskId = "task-api-error-001"
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: nil,
                    pollsUntilReady: 1,
                    videoData: Data(),
                    shouldSucceed: false,
                    throwAPIError: true
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            client.triggerHookDownload(testHook, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Verify started fetching but eventually timed out
            let startedEvents = events.filter { event in
                if case .startedFetchingDownloadURL = event { return true }
                return false
            }
            #expect(startedEvents.count == 1)
            let failureEvents = events.compactMap { event -> HookDownloadError? in
                if case .hookDownloadFailed(_, _, let error, _) = event {
                    return error
                }
                return nil
            }
            #expect(failureEvents.count == 1)
        }

        @Test("Empty download URL from API")
        func testEmptyDownloadURL() async throws {
            let testHook = Hook.mock(id: "test-hook-empty-url")
            let taskId = "task-empty-url-001"
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: URL(string: ""),
                    pollsUntilReady: 1,
                    videoData: Data(),
                    shouldSucceed: false
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            client.triggerHookDownload(testHook, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Should timeout since empty URL is treated as no URL
            let failureEvents = events.compactMap { event -> HookDownloadError? in
                if case .hookDownloadFailed(_, _, let error, _) = event {
                    return error
                }
                return nil
            }
            #expect(failureEvents.count == 1)
            if case .timedOut = failureEvents.first {
                // Success - got timeout error
            } else {
                Issue.record("Expected timedOut error")
            }
        }

        @Test("HTTP error when downloading video")
        func testHTTPDownloadError() async throws {
            let testHook = Hook.mock(id: "test-hook-http-error")
            let taskId = "task-http-error-001"
            let downloadURL = URL(string: "https://example.com/error-video.mp4")!
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: downloadURL,
                    pollsUntilReady: 1,
                    videoData: Data(),
                    shouldSucceed: false,
                    httpStatusCode: 500
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            client.triggerHookDownload(testHook, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Should get download URL ready but then fail on download
            let downloadURLReadyEvents = events.filter { event in
                if case .downloadURLReady = event { return true }
                return false
            }
            #expect(downloadURLReadyEvents.count == 1)
            let failureEvents = events.compactMap { event -> HookDownloadError? in
                if case .hookDownloadFailed(_, _, let error, _) = event {
                    return error
                }
                return nil
            }
            #expect(failureEvents.count == 1)
            if case .httpStatusError = failureEvents.first {
                // Success - got HTTP status error
            } else {
                Issue.record("Expected httpStatusError")
            }
        }
    }

    // MARK: - Event Stream Tests

    @Suite("Event Stream", .serialized)
    struct EventStream {
        @Test("Events contain correct hook and task ID")
        func testEventDataCorrectness() async throws {
            let testHook = Hook.mock(id: "test-hook-event-data")
            let taskId = "task-event-data-001"
            let downloadURL = URL(string: "https://example.com/video.mp4")!
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: downloadURL,
                    pollsUntilReady: 2,
                    videoData: Data("test".utf8),
                    shouldSucceed: true
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            client.triggerHookDownload(testHook, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Verify all events have correct hook and task ID
            for event in events {
                switch event {
                case .startedFetchingDownloadURL(let hook, let tid, _, _),
                     .polledForDownloadURL(let hook, let tid, _, _, _),
                     .downloadURLReady(let hook, let tid, _, _, _),
                     .startedDownloadingHook(let hook, let tid, _, _, _),
                     .hookDownloadComplete(let hook, let tid, _, _, _),
                     .hookDownloadFailed(let hook, let tid, _, _):
                    #expect(hook.id == testHook.id)
                }
            }
        }

        @Test("Event order is deterministic")
        func testEventOrder() async throws {
            let testHook = Hook.mock(id: "test-hook-order")
            let taskId = "task-order-001"
            let downloadURL = URL(string: "https://example.com/video.mp4")!
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: downloadURL,
                    pollsUntilReady: 1,
                    videoData: Data("test".utf8),
                    shouldSucceed: true
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            client.triggerHookDownload(testHook, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Verify events are in correct order
            #expect(events.count >= 4)
            // Event 0: startedFetchingDownloadURL
            guard case .startedFetchingDownloadURL = events[0] else {
                Issue.record("Expected startedFetchingDownloadURL as first event")
                return
            }
            // Event 1: polledForDownloadURL
            guard case .polledForDownloadURL = events[1] else {
                Issue.record("Expected polledForDownloadURL as second event")
                return
            }
            // Event 2: downloadURLReady
            guard case .downloadURLReady = events[2] else {
                Issue.record("Expected downloadURLReady as third event")
                return
            }
            // Event 3: startedDownloadingHook
            guard case .startedDownloadingHook = events[3] else {
                Issue.record("Expected startedDownloadingHook as fourth event")
                return
            }
        }
    }

    // MARK: - Edge Cases

    @Suite("Edge Cases", .serialized)
    struct EdgeCases {
        @Test("Multiple concurrent downloads")
        func testMultipleConcurrentDownloads() async throws {
            let hook1 = Hook.mock(id: "hook-1")
            let hook2 = Hook.mock(id: "hook-2")
            let hook3 = Hook.mock(id: "hook-3")
            let taskId1 = "task-1"
            let taskId2 = "task-2"
            let taskId3 = "task-3"
            let downloadURL = URL(string: "https://example.com/video.mp4")!
            var events: [HookDownloadClient.HookDownloadClientEvent] = []
            let client = withDependencies {
                $0.hookDownloadClient = .testValue(
                    downloadURL: downloadURL,
                    pollsUntilReady: 1,
                    videoData: Data("test".utf8),
                    shouldSucceed: true
                )
            } operation: {
                @Dependency(\.hookDownloadClient) var client
                return client
            }
            let eventsTask = Task {
                for await event in client.stream() {
                    events.append(event)
                }
            }
            // Trigger multiple downloads concurrently
            client.triggerHookDownload(hook1, .download, nil)
            client.triggerHookDownload(hook2, .download, nil)
            client.triggerHookDownload(hook3, .download, nil)
            try await Task.sleep(for: .milliseconds(100))
            eventsTask.cancel()
            // Verify we received events for all three hooks
            let hook1Events = events.filter { event in
                switch event {
                case .startedFetchingDownloadURL(let hook, _, _, _),
                     .polledForDownloadURL(let hook, _, _, _, _),
                     .downloadURLReady(let hook, _, _, _, _),
                     .startedDownloadingHook(let hook, _, _, _, _),
                     .hookDownloadComplete(let hook, _, _, _, _),
                     .hookDownloadFailed(let hook, _, _, _):
                    return hook.id == hook1.id
                }
            }
            let hook2Events = events.filter { event in
                switch event {
                case .startedFetchingDownloadURL(let hook, _, _, _),
                     .polledForDownloadURL(let hook, _, _, _, _),
                     .downloadURLReady(let hook, _, _, _, _),
                     .startedDownloadingHook(let hook, _, _, _, _),
                     .hookDownloadComplete(let hook, _, _, _, _),
                     .hookDownloadFailed(let hook, _, _, _):
                    return hook.id == hook2.id
                }
            }
            let hook3Events = events.filter { event in
                switch event {
                case .startedFetchingDownloadURL(let hook, _, _, _),
                     .polledForDownloadURL(let hook, _, _, _, _),
                     .downloadURLReady(let hook, _, _, _, _),
                     .startedDownloadingHook(let hook, _, _, _, _),
                     .hookDownloadComplete(let hook, _, _, _, _),
                     .hookDownloadFailed(let hook, _, _, _):
                    return hook.id == hook3.id
                }
            }
            #expect(!hook1Events.isEmpty)
            #expect(!hook2Events.isEmpty)
            #expect(!hook3Events.isEmpty)
        }
    }

    // MARK: - Model Tests

    @Suite("Model Tests")
    struct ModelTests {
        @Test("HookDownloadError descriptions")
        func testErrorDescriptions() {
            #expect(HookDownloadError.noData.errorDescription == "No data received")
            #expect(HookDownloadError.timedOut(hookId: "test-123", pollingAttempts: 5).errorDescription?.contains("Timed out") == true)
            #expect(HookDownloadError.pollingFailed(hookId: "test-123", underlyingError: "Error").errorDescription?.contains("Failed to poll") == true)
            #expect(HookDownloadError.invalidDownloadURL(hookId: "test-123", urlString: "bad-url").errorDescription?.contains("Invalid download URL") == true)
            #expect(HookDownloadError.invalidHTTPResponse(hookId: "test-123", url: "http://test.com").errorDescription?.contains("Invalid HTTP response") == true)
            #expect(HookDownloadError.httpStatusError(hookId: "test-123", statusCode: 500, url: "http://test.com").errorDescription?.contains("HTTP error 500") == true)
            #expect(HookDownloadError.networkError(hookId: "test-123", underlyingError: "Network failure").errorDescription?.contains("Network error") == true)
            #expect(HookDownloadError.fileSystemError(hookId: "test-123", operation: "write", underlyingError: "Disk full").errorDescription?.contains("File system error") == true)
            #expect(HookDownloadError.photoLibraryError(hookId: "test-123", underlyingError: "Permission denied").errorDescription?.contains("Photo library error") == true)
            #expect(HookDownloadError.cancelled(hookId: "test-123").errorDescription?.contains("cancelled") == true)
            #expect(HookDownloadError.unknown(hookId: "test-123", underlyingError: "Mystery error").errorDescription?.contains("Unknown error") == true)
        }

        @Test("HookDownloadClientStatus equatability")
        func testStatusEquality() {
            let url1 = URL(string: "https://example.com/video1.mp4")!
            let url2 = URL(string: "https://example.com/video2.mp4")!
            #expect(HookDownloadClientStatus.fetchingDownloadURL == HookDownloadClientStatus.fetchingDownloadURL)
            #expect(HookDownloadClientStatus.downloadingHook == HookDownloadClientStatus.downloadingHook)
            #expect(HookDownloadClientStatus.successfullyDownloadedHook == HookDownloadClientStatus.successfullyDownloadedHook)
            #expect(HookDownloadClientStatus.successfullyFetchedDownloadURL(url1) == HookDownloadClientStatus.successfullyFetchedDownloadURL(url1))
            #expect(HookDownloadClientStatus.successfullyFetchedDownloadURL(url1) != HookDownloadClientStatus.successfullyFetchedDownloadURL(url2))
            #expect(HookDownloadClientStatus.failure(.noData) == HookDownloadClientStatus.failure(.noData))
            let timeout1 = HookDownloadError.timedOut(hookId: "hook-1", pollingAttempts: 5)
            let timeout2 = HookDownloadError.timedOut(hookId: "hook-1", pollingAttempts: 5)
            #expect(HookDownloadClientStatus.failure(timeout1) == HookDownloadClientStatus.failure(timeout2))
            #expect(HookDownloadClientStatus.failure(.noData) != HookDownloadClientStatus.failure(timeout1))
        }

        @Test("HookDownloadClientTask initialization and properties")
        func testTaskProperties() {
            let taskId = "test-task-123"
            let hookId = "hook-456"
            let url = URL(string: "https://example.com/video.mp4")!
            let task1 = HookDownloadClientTask(
                id: taskId,
                type: .fetchDownloadURL(hookId: hookId),
                status: .fetchingDownloadURL
            )
            #expect(task1.id == taskId)
            #expect(task1.status == .fetchingDownloadURL)
            if case .fetchDownloadURL(let id) = task1.type {
                #expect(id == hookId)
            } else {
                Issue.record("Expected fetchDownloadURL task type")
            }
            let task2 = HookDownloadClientTask(
                type: .downloadHook(hookId: hookId, url: url),
                status: .downloadingHook
            )
            #expect(task2.id.isEmpty == false) // Has auto-generated ID
            #expect(task2.status == .downloadingHook)
            if case .downloadHook(let id, let taskUrl) = task2.type {
                #expect(id == hookId)
                #expect(taskUrl == url)
            } else {
                Issue.record("Expected downloadHook task type")
            }
        }

        @Test("TaskType processing status")
        func testTaskTypeProcessingStatus() {
            let hookId = "hook-123"
            let url = URL(string: "https://example.com/video.mp4")!
            let fetchType = HookDownloadClientTask.TaskType.fetchDownloadURL(hookId: hookId)
            #expect(fetchType.processingStatus == .fetchingDownloadURL)
            let downloadType = HookDownloadClientTask.TaskType.downloadHook(hookId: hookId, url: url)
            #expect(downloadType.processingStatus == .downloadingHook)
        }

        @Test("HookDownloadClientEvent equatability")
        func testEventEquality() {
            let hook1 = Hook.mock(id: "hook-1")
            let hook2 = Hook.mock(id: "hook-2")
            let taskId1 = "task-1"
            let taskId2 = "task-2"
            let url1 = URL(string: "https://example.com/video1.mp4")!
            let url2 = URL(string: "https://example.com/video2.mp4")!
            let event1 = HookDownloadClient.HookDownloadClientEvent.startedFetchingDownloadURL(hook1, taskId1, .download, nil)
            let event2 = HookDownloadClient.HookDownloadClientEvent.startedFetchingDownloadURL(hook1, taskId1, .download, nil)
            let event3 = HookDownloadClient.HookDownloadClientEvent.startedFetchingDownloadURL(hook2, taskId2, .download, nil)
            #expect(event1 == event2)
            #expect(event1 != event3)
            let pollEvent1 = HookDownloadClient.HookDownloadClientEvent.polledForDownloadURL(hook1, taskId1, 0, .download, nil)
            let pollEvent2 = HookDownloadClient.HookDownloadClientEvent.polledForDownloadURL(hook1, taskId1, 0, .download, nil)
            let pollEvent3 = HookDownloadClient.HookDownloadClientEvent.polledForDownloadURL(hook1, taskId1, 1, .download, nil)
            #expect(pollEvent1 == pollEvent2)
            #expect(pollEvent1 != pollEvent3)
            let urlReadyEvent1 = HookDownloadClient.HookDownloadClientEvent.downloadURLReady(hook1, taskId1, url1, .download, nil)
            let urlReadyEvent2 = HookDownloadClient.HookDownloadClientEvent.downloadURLReady(hook1, taskId1, url1, .download, nil)
            let urlReadyEvent3 = HookDownloadClient.HookDownloadClientEvent.downloadURLReady(hook1, taskId1, url2, .download, nil)
            #expect(urlReadyEvent1 == urlReadyEvent2)
            #expect(urlReadyEvent1 != urlReadyEvent3)
        }
    }
}

// MARK: - Test Helpers

extension HookDownloadClient {
    static func testValue(
        downloadURL: URL?,
        pollsUntilReady: Int,
        videoData _: Data,
        shouldSucceed: Bool,
        throwAPIError: Bool = false,
        httpStatusCode: Int = 200
    ) -> Self {
        let subject = AsyncStream<HookDownloadClientEvent>.makeStream()
        return Self(
            stream: {
                subject.stream
            },
            triggerHookDownload: { hook, target, attributionURL in
                let taskId = UUID().uuidString
                Task { @MainActor in
                    subject.continuation.yield(.startedFetchingDownloadURL(hook, taskId, target, attributionURL))
                    // Simulate polling - immediate execution for tests
                    var currentPoll = 0
                    var foundURL: URL?
                    while currentPoll < pollsUntilReady && foundURL == nil {
                        subject.continuation.yield(.polledForDownloadURL(hook, taskId, currentPoll, target, attributionURL))
                        currentPoll += 1
                        if throwAPIError {
                            // Simulate API error - continue polling but don't return URL
                            continue
                        }
                        if currentPoll >= pollsUntilReady, let downloadURL = downloadURL, !downloadURL.absoluteString.isEmpty {
                            foundURL = downloadURL
                            break
                        }
                    }
                    // Timeout if no URL found
                    guard let foundURL = foundURL else {
                        let error = HookDownloadError.timedOut(hookId: hook.id, pollingAttempts: currentPoll)
                        subject.continuation.yield(.hookDownloadFailed(hook, taskId, error, target))
                        return
                    }
                    subject.continuation.yield(.downloadURLReady(hook, taskId, foundURL, target, attributionURL))
                    subject.continuation.yield(.startedDownloadingHook(hook, taskId, foundURL, target, attributionURL))
                    // Simulate download
                    if !shouldSucceed || httpStatusCode != 200 {
                        let error = HookDownloadError.httpStatusError(hookId: hook.id, statusCode: httpStatusCode, url: foundURL.absoluteString)
                        subject.continuation.yield(.hookDownloadFailed(hook, taskId, error, target))
                        return
                    }
                    subject.continuation.yield(.hookDownloadComplete(hook, taskId, foundURL, target, attributionURL))
                }
            }
        )
    }
}
