//
//  AssetsViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/23.
//

import XCTest
@testable import suixinkan

@MainActor
/// 资产 ViewModel 测试，覆盖云盘和素材管理核心业务逻辑。
final class AssetsViewModelTests: XCTestCase {
    /// 测试云盘无上传权限时不会继续上传 OSS 或写入云盘。
    func testCloudUploadStopsWhenPermissionDenied() async {
        let api = MockAssetsService()
        api.uploadPermission = false
        api.uploadReason = "空间不足"
        let uploader = MockAssetsUploader()
        let viewModel = CloudStorageViewModel()

        let success = await viewModel.upload(
            localFiles: [CloudLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)],
            scenicId: 9,
            api: api,
            uploadService: uploader,
            transferStore: CloudTransferStore()
        )

        XCTAssertFalse(success)
        XCTAssertEqual(viewModel.errorMessage, "空间不足")
        XCTAssertEqual(uploader.uploadedFiles.count, 0)
        XCTAssertEqual(api.cloudUploadRequests.count, 0)
    }

    /// 测试云盘上传会先上传 OSS，再登记云盘文件并刷新列表。
    func testCloudUploadUploadsOSSBeforeCloudRecord() async {
        let api = MockAssetsService()
        let uploader = MockAssetsUploader()
        let transferStore = CloudTransferStore()
        let viewModel = CloudStorageViewModel()

        let success = await viewModel.upload(
            localFiles: [CloudLocalUploadFile(data: Data([1, 2]), fileName: "a.jpg", fileType: 2)],
            scenicId: 9,
            api: api,
            uploadService: uploader,
            transferStore: transferStore
        )

        XCTAssertTrue(success)
        XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["a.jpg"])
        XCTAssertEqual(api.cloudUploadRequests.first?.fileUrl, "https://cdn.example.com/a.jpg")
        XCTAssertEqual(transferStore.records.first?.status, .success)
    }

    /// 测试云盘分页、进入文件夹和返回文件夹。
    func testCloudPaginationAndFolderNavigation() async {
        let folder = CloudDriveFile(id: 2, parentFolderId: 0, fileUrl: "", name: "子目录", type: 99)
        let api = MockAssetsService()
        api.cloudPages = [
            ListPayload(total: 2, list: [folder]),
            ListPayload(total: 2, list: [CloudDriveFile(id: 3, parentFolderId: 0, fileUrl: "https://cdn/a.jpg", name: "a.jpg", type: 2)])
        ]
        let viewModel = CloudStorageViewModel()

        await viewModel.reload(api: api)
        await viewModel.loadMore(api: api)
        await viewModel.enterFolder(folder, api: api)
        await viewModel.popToFolder(at: 0, api: api)

        XCTAssertEqual(api.cloudListRequests.first?.parentFolderId, 0)
        XCTAssertEqual(api.cloudListRequests[1].page, 2)
        XCTAssertEqual(api.cloudListRequests[2].parentFolderId, 2)
        XCTAssertEqual(viewModel.currentFolderId, 0)
    }

    /// 测试素材列表筛选和分页请求参数。
    func testMediaListFilterAndPagination() async {
        let api = MockAssetsService()
        api.mediaListResponses = [
            MediaLibraryListResponse.fixture(total: 2, ids: [1]),
            MediaLibraryListResponse.fixture(total: 2, ids: [2])
        ]
        let viewModel = MediaLibraryViewModel(kind: .material)
        viewModel.keyword = " 航拍 "
        viewModel.selectedAuditFilter = .approved

        await viewModel.reload(api: api)
        await viewModel.loadMore(api: api)

        XCTAssertEqual(api.mediaListRequests.first?.keyword, "航拍")
        XCTAssertEqual(api.mediaListRequests.first?.status, 1)
        XCTAssertEqual(api.mediaListRequests.last?.page, 2)
        XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
    }

    /// 测试样片列表请求使用样片类型。
    func testSampleMediaListUsesSampleKind() async {
        let api = MockAssetsService()
        let viewModel = MediaLibraryViewModel(kind: .sample)

        await viewModel.reload(api: api)

        XCTAssertEqual(api.mediaListRequests.first?.kind, .sample)
    }

    /// 测试审核未通过素材禁止上下架。
    func testRejectedMediaCannotToggleListing() async {
        let api = MockAssetsService()
        let viewModel = MediaLibraryViewModel()
        let item = MediaLibraryItem.fixture(id: 1, status: 2, listingStatus: 0)

        let success = await viewModel.toggleListing(item, api: api)

        XCTAssertFalse(success)
        XCTAssertEqual(api.mediaOperationRequests.count, 0)
        XCTAssertEqual(viewModel.errorMessage, "审核通过后才能上下架")
    }

    /// 测试样片上下架会携带样片类型。
    func testSampleToggleListingUsesSampleType() async {
        let api = MockAssetsService()
        let viewModel = MediaLibraryViewModel(kind: .sample)
        let item = MediaLibraryItem.fixture(id: 1, status: 1, listingStatus: 0)

        let success = await viewModel.toggleListing(item, api: api)

        XCTAssertTrue(success)
        XCTAssertEqual(api.mediaOperationRequests.first?.type, 2)
    }

    /// 测试上传素材会先上传封面和媒体文件，再提交最终 OSS URL。
    func testMediaUploadUploadsFilesBeforeSubmit() async {
        let api = MockAssetsService()
        let uploader = MockAssetsUploader()
        let viewModel = MediaLibraryEditorViewModel()
        viewModel.name = "航拍素材"
        viewModel.selectedSpotId = 6
        viewModel.tagsText = "航拍,夜景"
        viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
        viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])

        let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)

        XCTAssertTrue(success)
        XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["cover.jpg", "a.jpg"])
        XCTAssertEqual(api.mediaUploadRequests.first?.coverUrl, "https://cdn.example.com/cover.jpg")
        XCTAssertEqual(api.mediaUploadRequests.first?.mediaList.first?.ossUrl, "https://cdn.example.com/a.jpg")
        XCTAssertEqual(api.mediaUploadRequests.first?.materialTag, "航拍,夜景")
    }

    /// 测试素材上传失败时不会提交素材接口。
    func testMediaUploadFailureDoesNotSubmit() async {
        let api = MockAssetsService()
        let uploader = MockAssetsUploader()
        uploader.shouldFail = true
        let viewModel = MediaLibraryEditorViewModel()
        viewModel.name = "航拍素材"
        viewModel.selectedSpotId = 6
        viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
        viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])

        let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)

        XCTAssertFalse(success)
        XCTAssertEqual(api.mediaUploadRequests.count, 0)
    }

    /// 测试样片上传缺少关联项目时不提交。
    func testSampleUploadRequiresProject() async {
        let api = MockAssetsService()
        let uploader = MockAssetsUploader()
        let viewModel = MediaLibraryEditorViewModel()
        viewModel.name = "亲子样片"
        viewModel.selectedSpotId = 6
        viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
        viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])

        let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)

        XCTAssertFalse(success)
        XCTAssertEqual(viewModel.errorMessage, "请选择关联项目")
        XCTAssertEqual(api.mediaUploadRequests.count, 0)
    }

    /// 测试样片上传会先上传文件，再提交样片类型和关联项目。
    func testSampleUploadUploadsFilesAndSubmitsProject() async {
        let api = MockAssetsService()
        let uploader = MockAssetsUploader()
        let viewModel = MediaLibraryEditorViewModel()
        viewModel.name = "亲子样片"
        viewModel.selectedSpotId = 6
        viewModel.selectedProjectId = 88
        viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
        viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])

        let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)

        XCTAssertTrue(success)
        XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["cover.jpg", "a.jpg"])
        XCTAssertEqual(api.mediaUploadRequests.first?.type, 2)
        XCTAssertEqual(api.mediaUploadRequests.first?.projectId, 88)
    }

    /// 测试重复提交样片会被忽略。
    func testSampleDuplicateSubmitIsIgnored() async {
        let api = MockAssetsService()
        let uploader = MockAssetsUploader()
        let viewModel = MediaLibraryEditorViewModel()
        viewModel.name = "亲子样片"
        viewModel.selectedSpotId = 6
        viewModel.selectedProjectId = 88
        viewModel.isSubmitting = true
        viewModel.setCover(MediaLocalUploadFile(data: Data([1]), fileName: "cover.jpg", fileType: 2, width: 100, height: 80))
        viewModel.addMediaFiles([MediaLocalUploadFile(data: Data([2]), fileName: "a.jpg", fileType: 2, width: 120, height: 90)])

        let success = await viewModel.submit(kind: .sample, scenicId: 9, api: api, uploadService: uploader)

        XCTAssertFalse(success)
        XCTAssertEqual(api.mediaUploadRequests.count, 0)
        XCTAssertEqual(uploader.uploadedFiles.count, 0)
    }

    /// 测试相册列表无景区时清空数据且不请求接口。
    func testAlbumListClearsWithoutScenic() async {
        let api = MockAssetsService()
        let viewModel = AlbumListViewModel()

        await viewModel.reload(api: api, scenicId: nil)

        XCTAssertTrue(viewModel.folders.isEmpty)
        XCTAssertEqual(api.albumFolderListRequests.count, 0)
        XCTAssertEqual(viewModel.errorMessage, "缺少当前景区，无法加载相册")
    }

    /// 测试相册列表搜索、日期筛选和分页参数。
    func testAlbumListFilterAndPagination() async {
        let api = MockAssetsService()
        api.albumFolderPages = [
            ListPayload(total: 2, list: [AlbumFolderItem.fixture(id: 1, name: "旅拍")]),
            ListPayload(total: 2, list: [AlbumFolderItem.fixture(id: 2, name: "亲子")])
        ]
        let viewModel = AlbumListViewModel()
        viewModel.searchText = " 旅拍 "
        viewModel.startTime = "2026-06-01"
        viewModel.endTime = "2026-06-24"

        await viewModel.reload(api: api, scenicId: 9)
        await viewModel.loadMore(api: api, scenicId: 9)

        XCTAssertEqual(api.albumFolderListRequests.first?.name, "旅拍")
        XCTAssertEqual(api.albumFolderListRequests.first?.startTime, "2026-06-01")
        XCTAssertEqual(api.albumFolderListRequests.last?.page, 2)
        XCTAssertEqual(viewModel.folders.map(\.id), [1, 2])
    }

    /// 测试新建相册成功后刷新列表。
    func testAlbumCreateRefreshesList() async {
        let api = MockAssetsService()
        api.albumFolderPages = [ListPayload(total: 1, list: [AlbumFolderItem.fixture(id: 3, name: "新相册")])]
        let viewModel = AlbumListViewModel()

        let success = await viewModel.createFolder(name: " 新相册 ", remark: " 备注 ", scenicId: 9, api: api)

        XCTAssertTrue(success)
        XCTAssertEqual(api.albumAddRequests.first?.name, "新相册")
        XCTAssertEqual(api.albumAddRequests.first?.remark, "备注")
        XCTAssertEqual(viewModel.folders.first?.id, 3)
    }

    /// 测试相册详情加载信息和文件列表，切换 Tab 会重置分页。
    func testAlbumDetailLoadsInfoAndSwitchesTab() async {
        let api = MockAssetsService()
        api.albumFilePages = [
            ListPayload(total: 1, list: [AlbumFileItem.fixture(id: 11, fileType: 2)]),
            ListPayload(total: 1, list: [AlbumFileItem.fixture(id: 12, fileType: 1)])
        ]
        let viewModel = AlbumDetailViewModel(folderId: 3)

        await viewModel.reload(api: api, scenicId: 9)
        await viewModel.selectTab(.video, api: api, scenicId: 9)

        XCTAssertEqual(api.albumInfoRequests, [3, 3])
        XCTAssertEqual(api.albumFileListRequests.map { $0.fileType }, [2, 1])
        XCTAssertEqual(viewModel.files.first?.id, 12)
        XCTAssertEqual(viewModel.page, 1)
    }

    /// 测试相册详情删除、设置封面和编辑信息会调用正确接口。
    func testAlbumDetailMutationsUseExpectedRequests() async {
        let api = MockAssetsService()
        let viewModel = AlbumDetailViewModel(folderId: 3)
        let file = AlbumFileItem.fixture(id: 11, fileType: 2)

        _ = await viewModel.delete(file: file, scenicId: 9, api: api)
        _ = await viewModel.setCover(file: file, scenicId: 9, api: api)
        _ = await viewModel.rename(name: " 改名 ", scenicId: 9, api: api)
        _ = await viewModel.updateRemark(" 新备注 ", scenicId: 9, api: api)

        XCTAssertEqual(api.albumDeleteRequests.first?.idList, [11])
        XCTAssertEqual(api.albumEditRequests[0].coverFileId, 11)
        XCTAssertEqual(api.albumEditRequests[1].name, "改名")
        XCTAssertEqual(api.albumEditRequests[2].remark, "新备注")
    }

    /// 测试相册预览上传会先上传 OSS，再登记相册文件。
    func testAlbumTrailerUploadUploadsOSSBeforeRegisteringFile() async {
        let api = MockAssetsService()
        api.albumFolderPages = [ListPayload(total: 1, list: [AlbumFolderItem.fixture(id: 3, name: "旅拍")])]
        let uploader = MockAssetsUploader()
        let viewModel = AlbumTrailerViewModel()
        viewModel.addLocalFiles([AlbumLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)])
        await viewModel.loadFolders(api: api, scenicId: 9)

        let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)

        XCTAssertTrue(success)
        XCTAssertEqual(uploader.uploadedFiles.map(\.fileName), ["a.jpg"])
        XCTAssertEqual(api.albumUploadURLRequests.first?.fileURL, "https://cdn.example.com/a.jpg")
        XCTAssertTrue(viewModel.localFiles.isEmpty)
        XCTAssertEqual(viewModel.uploadProgress, 100)
    }

    /// 测试相册上传失败时不会登记文件入库。
    func testAlbumTrailerUploadFailureDoesNotRegisterFile() async {
        let api = MockAssetsService()
        let uploader = MockAssetsUploader()
        uploader.shouldFail = true
        let viewModel = AlbumTrailerViewModel()
        viewModel.selectedFolderId = 3
        viewModel.addLocalFiles([AlbumLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 2)])

        let success = await viewModel.submit(scenicId: 9, api: api, uploadService: uploader)

        XCTAssertFalse(success)
        XCTAssertEqual(api.albumUploadURLRequests.count, 0)
    }
}

/// 资产服务测试替身，记录请求并返回预设数据。
@MainActor
private final class MockAssetsService: AssetsServing {
    struct CloudListRequest: Equatable {
        let parentFolderId: Int
        let name: String
        let type: Int
        let orderBy: Int
        let page: Int
        let pageSize: Int
    }

    struct MediaListRequest: Equatable {
        let kind: MediaLibraryKind
        let keyword: String
        let status: Int?
        let page: Int
    }

    struct ProjectListRequest: Equatable {
        let scenicId: Int
        let name: String?
        let page: Int
        let pageSize: Int
    }

    struct AlbumFolderListRequest: Equatable {
        let scenicId: Int
        let page: Int
        let pageSize: Int
        let name: String
        let startTime: String?
        let endTime: String?
    }

    struct AlbumUploadURLRequest: Equatable {
        let scenicId: Int
        let fileURL: String
        let folderId: Int
    }

    var cloudPages: [ListPayload<CloudDriveFile>] = [ListPayload(total: 0, list: [])]
    var mediaListResponses: [MediaLibraryListResponse] = [MediaLibraryListResponse.fixture(total: 0, ids: [])]
    var projectPages: [ListPayload<PhotographerProjectItem>] = [ListPayload(total: 0, list: [])]
    var albumFolderPages: [ListPayload<AlbumFolderItem>] = [ListPayload(total: 0, list: [])]
    var albumFilePages: [ListPayload<AlbumFileItem>] = [ListPayload(total: 0, list: [])]
    var uploadPermission = true
    var uploadReason = ""
    var cloudListRequests: [CloudListRequest] = []
    var cloudUploadRequests: [CloudFileUploadRequest] = []
    var mediaListRequests: [MediaListRequest] = []
    var mediaOperationRequests: [MediaAlbumOperationRequest] = []
    var mediaUploadRequests: [MediaAlbumUploadRequest] = []
    var mediaEditRequests: [MediaAlbumEditRequest] = []
    var projectListRequests: [ProjectListRequest] = []
    var albumFolderListRequests: [AlbumFolderListRequest] = []
    var albumInfoRequests: [Int] = []
    var albumFileListRequests: [(scenicId: Int, folderId: Int, fileType: Int, page: Int)] = []
    var albumAddRequests: [(scenicId: Int, name: String, remark: String)] = []
    var albumEditRequests: [AlbumFolderEditRequest] = []
    var albumDeleteRequests: [AlbumFileDeleteRequest] = []
    var albumUploadURLRequests: [AlbumUploadURLRequest] = []

    /// 返回云盘文件列表。
    func cloudFileList(parentFolderId: Int, name: String, type: Int, orderBy: Int, page: Int, pageSize: Int) async throws -> ListPayload<CloudDriveFile> {
        cloudListRequests.append(CloudListRequest(parentFolderId: parentFolderId, name: name, type: type, orderBy: orderBy, page: page, pageSize: pageSize))
        return cloudPages.isEmpty ? ListPayload(total: 0, list: []) : cloudPages.removeFirst()
    }

    /// 创建文件夹。
    func cloudFolderCreate(_ request: CloudFolderCreateRequest) async throws {}

    /// 登记云盘文件。
    func cloudFileUpload(_ request: CloudFileUploadRequest) async throws {
        cloudUploadRequests.append(request)
    }

    /// 删除云盘文件。
    func cloudFileDelete(_ request: CloudFileDeleteRequest) async throws {}

    /// 移动云盘文件。
    func cloudFileMove(_ request: CloudFileMoveRequest) async throws {}

    /// 重命名文件夹。
    func cloudFolderEdit(_ request: CloudFolderModifyRequest) async throws {}

    /// 重命名文件。
    func cloudFileEdit(_ request: CloudFileModifyRequest) async throws {}

    /// 检查上传权限。
    func checkCloudUploadPermission() async throws -> CheckCloudUploadResponse {
        try JSONDecoder().decode(CheckCloudUploadResponse.self, from: #"{"can_upload":\#(uploadPermission ? 1 : 0),"reason":"\#(uploadReason)"}"#.data(using: .utf8)!)
    }

    /// 返回素材列表。
    func mediaAlbumList(kind: MediaLibraryKind, keyword: String, status: Int?, page: Int, pageSize: Int) async throws -> MediaLibraryListResponse {
        mediaListRequests.append(MediaListRequest(kind: kind, keyword: keyword, status: status, page: page))
        return mediaListResponses.isEmpty ? MediaLibraryListResponse.fixture(total: 0, ids: []) : mediaListResponses.removeFirst()
    }

    /// 返回素材详情。
    func mediaAlbumDetail(id: Int, kind: MediaLibraryKind) async throws -> MediaLibraryDetail {
        MediaLibraryDetail.fixture(id: id)
    }

    /// 更新上下架。
    func mediaAlbumOperation(_ request: MediaAlbumOperationRequest) async throws {
        mediaOperationRequests.append(request)
    }

    /// 删除素材。
    func mediaAlbumDelete(_ request: MediaAlbumDeleteRequest) async throws {}

    /// 新增标签。
    func mediaAlbumAddTag(_ request: MediaAlbumAddTagRequest) async throws {}

    /// 上传素材。
    func mediaAlbumUpload(_ request: MediaAlbumUploadRequest) async throws {
        mediaUploadRequests.append(request)
    }

    /// 编辑素材。
    func mediaAlbumEdit(_ request: MediaAlbumEditRequest) async throws {
        mediaEditRequests.append(request)
    }

    /// 返回样片可关联项目列表。
    func projectList(scenicId: Int, name: String?, page: Int, pageSize: Int) async throws -> ListPayload<PhotographerProjectItem> {
        projectListRequests.append(ProjectListRequest(scenicId: scenicId, name: name, page: page, pageSize: pageSize))
        return projectPages.isEmpty ? ListPayload(total: 0, list: []) : projectPages.removeFirst()
    }

    /// 返回相册文件夹列表。
    func albumFolderList(scenicId: Int, page: Int, pageSize: Int, name: String, startTime: String?, endTime: String?) async throws -> ListPayload<AlbumFolderItem> {
        albumFolderListRequests.append(AlbumFolderListRequest(scenicId: scenicId, page: page, pageSize: pageSize, name: name, startTime: startTime, endTime: endTime))
        return albumFolderPages.isEmpty ? ListPayload(total: 0, list: []) : albumFolderPages.removeFirst()
    }

    /// 返回相册文件夹详情。
    func albumFolderInfo(id: Int) async throws -> AlbumFolderItem {
        albumInfoRequests.append(id)
        return AlbumFolderItem.fixture(id: id, name: "相册\(id)")
    }

    /// 返回相册文件列表。
    func albumFileList(scenicId: Int, folderId: Int, fileType: Int, page: Int, pageSize: Int) async throws -> ListPayload<AlbumFileItem> {
        albumFileListRequests.append((scenicId: scenicId, folderId: folderId, fileType: fileType, page: page))
        return albumFilePages.isEmpty ? ListPayload(total: 0, list: []) : albumFilePages.removeFirst()
    }

    /// 新建相册。
    func addAlbumFolder(scenicId: Int, name: String, remark: String) async throws {
        albumAddRequests.append((scenicId: scenicId, name: name, remark: remark))
    }

    /// 编辑相册。
    func editAlbumFolder(folderId: Int, coverFileId: Int?, name: String?, remark: String?) async throws {
        albumEditRequests.append(AlbumFolderEditRequest(folderId: folderId, coverFileId: coverFileId, name: name, remark: remark))
    }

    /// 删除相册文件。
    func deleteAlbumFiles(folderId: Int, idList: [Int]) async throws {
        albumDeleteRequests.append(AlbumFileDeleteRequest(idList: idList, folderId: folderId))
    }

    /// 登记相册上传文件。
    func albumFileUploadURL(scenicId: Int, fileURL: String, folderId: Int) async throws {
        albumUploadURLRequests.append(AlbumUploadURLRequest(scenicId: scenicId, fileURL: fileURL, folderId: folderId))
    }
}

/// OSS 上传测试替身，记录上传文件并返回固定 CDN URL。
@MainActor
private final class MockAssetsUploader: OSSUploadServing {
    struct UploadedFile: Equatable {
        let fileName: String
        let fileType: Int
        let scenicId: Int
    }

    var uploadedFiles: [UploadedFile] = []
    var shouldFail = false

    /// 上传用户头像。
    func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传实名认证图片。
    func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传云盘文件。
    func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: fileType, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传相册文件。
    func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: fileType, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传任务附件。
    func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: fileType, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传项目图片。
    func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传打卡点图片。
    func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传景区申请图片。
    func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
    }

    /// 上传银行卡图片。
    func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        try await upload(fileName: fileName, fileType: 2, scenicId: scenicId, onProgress: onProgress)
    }

    /// 通用测试上传流程。
    private func upload(fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if shouldFail {
            throw APIError.networkFailed("上传失败")
        }
        uploadedFiles.append(UploadedFile(fileName: fileName, fileType: fileType, scenicId: scenicId))
        onProgress(100)
        return "https://cdn.example.com/\(fileName)"
    }
}

private extension MediaLibraryListResponse {
    /// 创建素材列表测试数据。
    static func fixture(total: Int, ids: [Int]) -> MediaLibraryListResponse {
        let list = ids.map { MediaLibraryItem.fixture(id: $0, status: 1, listingStatus: 0) }
        let payload = [
            "total": total,
            "list": list.map { ["id": $0.id, "name": $0.name, "status": $0.status, "listing_status": $0.listingStatus] },
            "order": ["total_num": 0, "avg_order_amount": 0, "refund_total": 0, "avg_change": 0]
        ] as [String: Any]
        let data = try! JSONSerialization.data(withJSONObject: payload)
        return try! JSONDecoder().decode(MediaLibraryListResponse.self, from: data)
    }
}

private extension MediaLibraryItem {
    /// 创建素材列表项测试数据。
    static func fixture(id: Int, status: Int, listingStatus: Int) -> MediaLibraryItem {
        let payload: [String: Any] = [
            "id": id,
            "name": "素材\(id)",
            "cover_url": "",
            "created_at": "",
            "status": status,
            "download_count": 0,
            "likes_count": 0,
            "collect_count": 0,
            "share_count": 0,
            "scenic_spot_name": "",
            "project_name": "",
            "listing_status": listingStatus
        ]
        let data = try! JSONSerialization.data(withJSONObject: payload)
        return try! JSONDecoder().decode(MediaLibraryItem.self, from: data)
    }
}

private extension MediaLibraryDetail {
    /// 创建素材详情测试数据。
    static func fixture(id: Int) -> MediaLibraryDetail {
        let payload: [String: Any] = [
            "id": id,
            "name": "素材\(id)",
            "cover_url": "",
            "description": "",
            "status": 1,
            "listing_status": 0,
            "likes_count": 0,
            "download_count": 0,
            "collect_count": 0,
            "share_count": 0,
            "uploader_name": "",
            "created_at": "",
            "media_list": [],
            "scenic_name": "",
            "project_name": ""
        ]
        let data = try! JSONSerialization.data(withJSONObject: payload)
        return try! JSONDecoder().decode(MediaLibraryDetail.self, from: data)
    }
}

private extension AlbumFolderItem {
    /// 创建相册文件夹测试数据。
    static func fixture(id: Int, name: String) -> AlbumFolderItem {
        let payload: [String: Any] = [
            "id": id,
            "name": name,
            "cover_file_id": 0,
            "count_video": 1,
            "count_image": 2,
            "created_at": "2026",
            "remark": "备注"
        ]
        let data = try! JSONSerialization.data(withJSONObject: payload)
        return try! JSONDecoder().decode(AlbumFolderItem.self, from: data)
    }
}

private extension AlbumFileItem {
    /// 创建相册文件测试数据。
    static func fixture(id: Int, fileType: Int) -> AlbumFileItem {
        let fileName = fileType == 1 ? "v.mp4" : "a.jpg"
        let payload: [String: Any] = [
            "id": id,
            "file_name": fileName,
            "file_type": fileType,
            "file_url": "https://cdn.example.com/\(fileName)",
            "cover_url": "",
            "file_size": "2048",
            "file_size_human": "2KB",
            "remark": ""
        ]
        let data = try! JSONSerialization.data(withJSONObject: payload)
        return try! JSONDecoder().decode(AlbumFileItem.self, from: data)
    }
}
