//
//  CloudTransferManagerTests.swift
//  suixinkanTests
//

import Foundation
import XCTest
@testable import suixinkan

@MainActor
/// 云盘传输管理测试。
final class CloudTransferManagerTests: XCTestCase {
    func testUploadRegistersFileAndRemovesCompletedTask() async throws {
        let store = MemoryTransferStore()
        let uploader = FakeCloudUploader(url: "https://cdn/a.jpg")
        let api = TransferMockCloudDriveAPI()
        let manager = CloudTransferManager(
            store: store,
            uploader: uploader,
            api: api,
            downloader: FakeDownloader(),
            photoSaver: FakePhotoSaver(),
            scenicIdProvider: { 10 }
        )
        let fileURL = try makeTempFile(name: "a.jpg")
        var completedFolder: Int?
        manager.onUploadCompleted = { completedFolder = $0 }

        manager.addUploadTask(localFileURL: fileURL, fileName: "a.jpg", fileType: 2, fileSize: 3, parentFolderId: 6)
        try await waitUntil { manager.uploadTasks.isEmpty }

        XCTAssertEqual(api.uploadCalls.first?.parentFolderId, 6)
        XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/a.jpg")
        XCTAssertEqual(completedFolder, 6)
        XCTAssertTrue(store.saved.last?.allSatisfy { $0.kind != .upload } == true)
    }

    func testUploadFailureKeepsFailedTask() async throws {
        let uploader = FakeCloudUploader(error: TestError.failed)
        let manager = CloudTransferManager(
            store: MemoryTransferStore(),
            uploader: uploader,
            api: TransferMockCloudDriveAPI(),
            downloader: FakeDownloader(),
            photoSaver: FakePhotoSaver(),
            scenicIdProvider: { 10 }
        )
        let fileURL = try makeTempFile(name: "a.jpg")

        manager.addUploadTask(localFileURL: fileURL, fileName: "a.jpg", fileType: 2, fileSize: 3, parentFolderId: 0)
        try await waitUntil { manager.uploadTasks.first?.status == .failed }

        XCTAssertEqual(manager.uploadTasks.first?.status, .failed)
    }

    func testDownloadCompletesAndCanCancel() async throws {
        let downloader = FakeDownloader()
        let saver = FakePhotoSaver()
        let manager = CloudTransferManager(
            store: MemoryTransferStore(),
            uploader: FakeCloudUploader(url: "unused"),
            api: TransferMockCloudDriveAPI(),
            downloader: downloader,
            photoSaver: saver,
            scenicIdProvider: { 10 }
        )

        manager.addDownloadTask(fileURL: "https://cdn/a.jpg", fileName: "a.jpg", fileType: 2)
        try await waitUntil { manager.downloadTasks.first?.status == .completed }

        XCTAssertEqual(downloader.downloadCalls.first?.remoteURL, "https://cdn/a.jpg")
        XCTAssertEqual(saver.saved.count, 1)
        XCTAssertEqual(manager.downloadTasks.first?.progress, 100)

        let id = try XCTUnwrap(manager.downloadTasks.first?.id)
        manager.cancelDownload(id: id)
        XCTAssertTrue(manager.downloadTasks.isEmpty)
    }

    func testUploadPauseThenResumeCompletes() async throws {
        let uploader = PausingThenSucceedCloudUploader(url: "https://cdn/resumed.jpg")
        let api = TransferMockCloudDriveAPI()
        let manager = CloudTransferManager(
            store: MemoryTransferStore(),
            uploader: uploader,
            api: api,
            downloader: FakeDownloader(),
            photoSaver: FakePhotoSaver(),
            scenicIdProvider: { 10 }
        )
        let fileURL = try makeTempFile(name: "resume.jpg")

        manager.addUploadTask(localFileURL: fileURL, fileName: "resume.jpg", fileType: 2, fileSize: 3, parentFolderId: 7)
        let id = try XCTUnwrap(manager.uploadTasks.first?.id)
        try await waitUntil { uploader.attempts == 1 }
        manager.pauseUpload(id: id)
        try await waitUntil { manager.uploadTasks.first?.status == .paused }

        manager.resumeUpload(id: id)
        try await waitUntil { manager.uploadTasks.isEmpty }

        XCTAssertEqual(uploader.attempts, 2)
        XCTAssertEqual(api.uploadCalls.first?.parentFolderId, 7)
        XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/resumed.jpg")
    }

    func testFailedUploadCanRetry() async throws {
        let uploader = FailOnceCloudUploader(url: "https://cdn/retried.jpg")
        let api = TransferMockCloudDriveAPI()
        let manager = CloudTransferManager(
            store: MemoryTransferStore(),
            uploader: uploader,
            api: api,
            downloader: FakeDownloader(),
            photoSaver: FakePhotoSaver(),
            scenicIdProvider: { 10 }
        )
        let fileURL = try makeTempFile(name: "retry.jpg")

        manager.addUploadTask(localFileURL: fileURL, fileName: "retry.jpg", fileType: 2, fileSize: 3, parentFolderId: 0)
        try await waitUntil { manager.uploadTasks.first?.status == .failed }
        let id = try XCTUnwrap(manager.uploadTasks.first?.id)

        manager.resumeUpload(id: id)
        try await waitUntil { manager.uploadTasks.isEmpty }

        XCTAssertEqual(uploader.attempts, 2)
        XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/retried.jpg")
    }

    func testUploadCancelRemovesTaskAndStagedFile() async throws {
        let uploader = PausingThenSucceedCloudUploader(url: "unused")
        let manager = CloudTransferManager(
            store: MemoryTransferStore(),
            uploader: uploader,
            api: TransferMockCloudDriveAPI(),
            downloader: FakeDownloader(),
            photoSaver: FakePhotoSaver(),
            scenicIdProvider: { 10 }
        )
        let fileURL = try makeTempFile(name: "cancel.jpg")

        manager.addUploadTask(localFileURL: fileURL, fileName: "cancel.jpg", fileType: 2, fileSize: 3, parentFolderId: 0)
        let id = try XCTUnwrap(manager.uploadTasks.first?.id)
        manager.cancelUpload(id: id)

        XCTAssertTrue(manager.uploadTasks.isEmpty)
        XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
    }

    func testDownloadPauseThenResumeCompletes() async throws {
        let downloader = PausingThenSucceedDownloader()
        let saver = FakePhotoSaver()
        let manager = CloudTransferManager(
            store: MemoryTransferStore(),
            uploader: FakeCloudUploader(url: "unused"),
            api: TransferMockCloudDriveAPI(),
            downloader: downloader,
            photoSaver: saver,
            scenicIdProvider: { 10 }
        )

        manager.addDownloadTask(fileURL: "https://cdn/resume.jpg", fileName: "resume.jpg", fileType: 2)
        let id = try XCTUnwrap(manager.downloadTasks.first?.id)
        try await waitUntil { await downloader.attemptCount() == 1 }
        manager.pauseDownload(id: id)
        try await waitUntil { manager.downloadTasks.first?.status == .paused }

        manager.resumeDownload(id: id)
        try await waitUntil { manager.downloadTasks.first?.status == .completed }

        let attemptCount = await downloader.attemptCount()
        XCTAssertEqual(attemptCount, 2)
        XCTAssertEqual(saver.saved.count, 1)
        XCTAssertEqual(manager.downloadTasks.first?.progress, 100)
    }

    private func makeTempFile(name: String) throws -> URL {
        let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + name)
        try Data([1, 2, 3]).write(to: url)
        return url
    }

    private func waitUntil(
        timeout: TimeInterval = 2,
        condition: @escaping () -> Bool
    ) async throws {
        let deadline = Date().addingTimeInterval(timeout)
        while !condition() {
            if Date() > deadline {
                XCTFail("Timed out waiting for condition")
                return
            }
            try await Task.sleep(nanoseconds: 20_000_000)
        }
    }

    private func waitUntil(
        timeout: TimeInterval = 2,
        condition: @escaping () async -> Bool
    ) async throws {
        let deadline = Date().addingTimeInterval(timeout)
        while !(await condition()) {
            if Date() > deadline {
                XCTFail("Timed out waiting for condition")
                return
            }
            try await Task.sleep(nanoseconds: 20_000_000)
        }
    }
}

private final class MemoryTransferStore: CloudTransferTaskStoring {
    var tasks: [CloudTransferTask] = []
    var saved: [[CloudTransferTask]] = []

    func loadTasks() -> [CloudTransferTask] {
        tasks
    }

    func saveTasks(_ tasks: [CloudTransferTask]) {
        self.tasks = tasks
        saved.append(tasks)
    }
}

@MainActor
private final class FakeCloudUploader: CloudDriveUploading {
    let url: String
    let error: Error?

    init(url: String = "", error: Error? = nil) {
        self.url = url
        self.error = error
    }

    func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if let error { throw error }
        onProgress(50)
        onProgress(100)
        return url
    }
}

@MainActor
private final class PausingThenSucceedCloudUploader: CloudDriveUploading {
    let url: String
    private(set) var attempts = 0

    init(url: String) {
        self.url = url
    }

    func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        attempts += 1
        if attempts == 1 {
            try await Task.sleep(nanoseconds: 5_000_000_000)
        }
        onProgress(100)
        return url
    }
}

@MainActor
private final class FailOnceCloudUploader: CloudDriveUploading {
    let url: String
    private(set) var attempts = 0

    init(url: String) {
        self.url = url
    }

    func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        attempts += 1
        if attempts == 1 {
            throw TestError.failed
        }
        onProgress(100)
        return url
    }
}

private final class FakeDownloader: CloudFileDownloading {
    var downloadCalls: [(remoteURL: String, fileName: String)] = []

    func downloadFile(remoteURL: String, fileName: String, onProgress: @escaping @Sendable (Int) -> Void) async throws -> URL {
        downloadCalls.append((remoteURL, fileName))
        onProgress(40)
        let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName)
        try Data([1]).write(to: url)
        onProgress(100)
        return url
    }
}

private final class PausingThenSucceedDownloader: CloudFileDownloading {
    private let counter = DownloadAttemptCounter()

    func downloadFile(remoteURL: String, fileName: String, onProgress: @escaping @Sendable (Int) -> Void) async throws -> URL {
        let attempt = await counter.next()
        if attempt == 1 {
            try await Task.sleep(nanoseconds: 5_000_000_000)
        }
        onProgress(100)
        let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName)
        try Data([1]).write(to: url)
        return url
    }

    func attemptCount() async -> Int {
        await counter.current()
    }
}

private actor DownloadAttemptCounter {
    private var value = 0

    func next() -> Int {
        value += 1
        return value
    }

    func current() -> Int {
        value
    }
}

private final class FakePhotoSaver: CloudPhotoSaving {
    var saved: [(fileURL: URL, fileType: Int)] = []

    func saveToPhotoLibrary(fileURL: URL, fileType: Int) async throws {
        saved.append((fileURL, fileType))
    }
}

@MainActor
private final class TransferMockCloudDriveAPI: CloudDriveServing {
    var uploadCalls: [(parentFolderId: Int, fileUrl: String, fileName: String)] = []

    func cloudFileList(parentFolderId: Int, name: String, type: Int, orderBy: Int, page: Int, pageSize: Int) async throws -> CloudFileListResponse {
        CloudFileListResponse()
    }

    func createCloudFolder(parentFolderId: Int, name: String) async throws {}

    func uploadCloudFile(parentFolderId: Int, fileUrl: String, fileName: String) async throws {
        uploadCalls.append((parentFolderId, fileUrl, fileName))
    }

    func deleteCloudFiles(_ request: CloudFileDeleteRequest) async throws {}

    func moveCloudFiles(_ request: CloudFileMoveRequest) async throws {}

    func checkUploadPermission() async throws -> CloudUploadPermissionResponse {
        CloudUploadPermissionResponse(canUpload: true, reason: "")
    }

    func modifyCloudFolderName(fileId: Int, name: String) async throws {}

    func modifyCloudFileName(fileId: Int, fileName: String) async throws {}
}

private enum TestError: LocalizedError {
    case failed

    var errorDescription: String? { "failed" }
}
