//
//  WiredCameraTransferViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/30.
//

import XCTest
import UIKit
@testable import suixinkan

@MainActor
/// 有线传图 ViewModel 测试。
final class WiredCameraTransferViewModelTests: XCTestCase {
    /// 测试相机管道回调的首张新照片应出现在列表中。
    func testFirstNewPipelineTaskAppearsInPhotos() async throws {
        let camera = MockWiredCameraService()
        let context = WiredTransferContext(
            albumId: 6603,
            albumName: "测试相册",
            phone: "",
            orderNumber: ""
        )
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))

        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )

        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("DSC_NEW.JPG")
        try Data(repeating: 0xFF, count: 16).write(to: tempURL)
        defer { try? FileManager.default.removeItem(at: tempURL) }

        let asset = CameraAsset(id: "ptp_new_001", filename: "DSC_NEW.JPG", fileSize: 16)
        camera.downloadURL = tempURL
        camera.onNewAsset?(asset)
        try await Task.sleep(nanoseconds: 500_000_000)

        XCTAssertEqual(viewModel.photos.count, 1)
        XCTAssertEqual(viewModel.photos.first?.id, "ptp_new_001")
        XCTAssertEqual(viewModel.photos.first?.fileName, "DSC_NEW.JPG")
    }

    /// 测试删除待上传照片后，边拍边传新片不会把已删照片带回列表。
    func testDeletedPendingPhotoDoesNotReappearAfterLiveCapture() async throws {
        let camera = MockWiredCameraService()
        let context = WiredTransferContext(
            albumId: 7701,
            albumName: "删除回归测试",
            phone: "",
            orderNumber: ""
        )
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))

        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )

        let oldURL = FileManager.default.temporaryDirectory.appendingPathComponent("OLD_DELETE.JPG")
        try Data(repeating: 0x11, count: 16).write(to: oldURL)
        defer { try? FileManager.default.removeItem(at: oldURL) }

        camera.downloadURL = oldURL
        camera.onNewAsset?(CameraAsset(id: "ptp_delete_me", filename: "OLD_DELETE.JPG", fileSize: 16))
        try await Task.sleep(nanoseconds: 500_000_000)
        XCTAssertEqual(viewModel.photos.count, 1)

        viewModel.deletePhoto(id: "ptp_delete_me")
        XCTAssertTrue(viewModel.photos.isEmpty)

        viewModel.selectTransferMode(
            WiredCameraTransferViewModel.modeLiveCapture,
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1
        )

        let newURL = FileManager.default.temporaryDirectory.appendingPathComponent("NEW_LIVE.JPG")
        try Data(repeating: 0x22, count: 16).write(to: newURL)
        defer { try? FileManager.default.removeItem(at: newURL) }

        camera.downloadURL = newURL
        camera.onNewAsset?(CameraAsset(id: "ptp_live_new", filename: "NEW_LIVE.JPG", fileSize: 16))
        try await Task.sleep(nanoseconds: 500_000_000)

        XCTAssertEqual(viewModel.photos.count, 1)
        XCTAssertEqual(viewModel.photos.first?.id, "ptp_live_new")
        XCTAssertFalse(viewModel.photos.contains { $0.id == "ptp_delete_me" })
    }

    /// 测试 start 被取消时 detach UI 回调，但不 shutdown 相机连接。
    func testStartCancellationDetachesWithoutShutdown() async {
        let camera = DisconnectTrackingWiredCameraService()
        let context = WiredTransferContext(
            albumId: 6603,
            albumName: "测试相册",
            phone: "",
            orderNumber: ""
        )
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))

        let startTask = Task {
            await viewModel.start(
                api: MockTravelAlbumAPIForWiredTransfer(),
                ossService: WiredTransferMockOSSUploadService(),
                scenicID: 1,
                accountContext: account
            )
        }

        for _ in 0 ..< 100 {
            if camera.connectCount > 0 { break }
            try? await Task.sleep(nanoseconds: 10_000_000)
        }

        startTask.cancel()
        try? await Task.sleep(nanoseconds: 300_000_000)

        XCTAssertGreaterThanOrEqual(camera.detachCount, 1)
        XCTAssertEqual(camera.shutdownCount, 0)
    }

    /// 测试绑定到其他相册的照片不会出现在当前相册列表。
    func testPhotoBoundToOtherAlbumIsFilteredOut() async throws {
        let bindingStore = WiredTransferPhotoStore(
            accountPrefixProvider: { "user_101" },
            userIDProvider: { "101" }
        )
        bindingStore.bindPhotoToAlbum(photoID: "ptp_other_album", albumID: 99)
        defer { bindingStore.bindPhotoToAlbum(photoID: "ptp_other_album", albumID: 1) }

        let camera = MockWiredCameraService()
        let context = WiredTransferContext(
            albumId: 1,
            albumName: "当前相册",
            phone: "",
            orderNumber: ""
        )
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"), accountType: "user")

        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )

        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("DSC_OTHER.JPG")
        try Data(repeating: 0xAA, count: 8).write(to: tempURL)
        defer { try? FileManager.default.removeItem(at: tempURL) }

        camera.downloadURL = tempURL
        camera.onNewAsset?(CameraAsset(id: "ptp_other_album", filename: "DSC_OTHER.JPG", fileSize: 8))
        try await Task.sleep(nanoseconds: 500_000_000)

        XCTAssertTrue(viewModel.photos.isEmpty)
    }

    /// 测试批量上传按钮三态流转。
    func testBatchUploadButtonThreeStateFlow() async {
        let viewModel = makeViewModelWithPendingPhotos()
        let api = MockTravelAlbumAPIForWiredTransfer()
        let oss = WiredTransferMockOSSUploadService()

        XCTAssertFalse(viewModel.selectUploadMode)
        await viewModel.onBatchUploadButtonClick(api: api, ossService: oss, scenicID: 1)
        XCTAssertTrue(viewModel.selectUploadMode)
        XCTAssertTrue(viewModel.selectedPhotoIDs.isEmpty)

        viewModel.togglePhotoSelection(id: "pending-1")
        XCTAssertEqual(viewModel.selectedPhotoIDs, ["pending-1"])

        await viewModel.onBatchUploadButtonClick(api: api, ossService: oss, scenicID: 1)
        XCTAssertFalse(viewModel.selectUploadMode)
        XCTAssertTrue(viewModel.selectedPhotoIDs.isEmpty)
    }

    /// 测试全选 pending 与取消全选。
    func testToggleSelectAllPendingInVisible() {
        let viewModel = makeViewModelWithPendingPhotos()
        viewModel.selectUploadMode = true

        viewModel.toggleSelectAllPendingInVisible()
        XCTAssertEqual(viewModel.selectedPhotoIDs, ["pending-1", "pending-2"])

        viewModel.toggleSelectAllPendingInVisible()
        XCTAssertTrue(viewModel.selectedPhotoIDs.isEmpty)
    }

    /// 测试指定上传今日拍摄筛选。
    func testUploadAllTodayCapturedFiltersByDate() async {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        formatter.locale = Locale(identifier: "zh_CN")
        let today = formatter.string(from: Date())

        let viewModel = WiredCameraTransferViewModel(
            context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: ""),
            cameraService: MockWiredCameraService()
        )
        viewModel.replacePhotos([
            makePendingPhoto(id: "today", capturedAt: today),
            makePendingPhoto(id: "old", capturedAt: "2020-01-01 09:00:00"),
        ])

        await viewModel.onSpecifyUploadOptionSelected(
            WiredCameraTransferViewModel.specifyUploadTodayCaptured,
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1
        )
        XCTAssertNil(viewModel.errorMessage)
    }

    /// 测试上传进度更新时保留 capturedAt，避免列表分组抖动。
    func testPipelineProgressUpdatePreservesCapturedAt() async throws {
        let camera = MockWiredCameraService()
        let context = WiredTransferContext(
            albumId: 1,
            albumName: "测试相册",
            phone: "",
            orderNumber: ""
        )
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))

        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )
        let originalCapturedAt = "2026-05-20 12:05:18"
        viewModel.replacePhotos([
            WiredTransferPhotoItem(
                id: "ptp_progress",
                fileName: "DSC_PROGRESS.JPG",
                thumbnailURL: "",
                previewURL: "",
                capturedAt: originalCapturedAt,
                fileSizeText: "2 MB",
                status: .uploading,
                progress: 10
            ),
        ])

        let scope = downloadScope(account: account, albumID: context.albumId)
        let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "DSC_PROGRESS.JPG", scope: scope)
        try Data(repeating: 0xAB, count: 16).write(to: fileURL)
        defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }

        camera.downloadURL = fileURL
        camera.onNewAsset?(CameraAsset(id: "ptp_progress", filename: "DSC_PROGRESS.JPG", fileSize: 16))
        try await Task.sleep(nanoseconds: 300_000_000)

        XCTAssertEqual(viewModel.photos.first?.capturedAt, originalCapturedAt)
    }

    /// 测试较新拍摄时间的照片排在列表前面。
    func testNewerCapturedPhotoAppearsFirst() async throws {
        let camera = MockWiredCameraService()
        let context = WiredTransferContext(
            albumId: 6604,
            albumName: "测试相册",
            phone: "",
            orderNumber: ""
        )
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))

        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )

        let fileURL = FileManager.default.temporaryDirectory
        let olderURL = fileURL.appendingPathComponent("OLDER.JPG")
        let newerURL = fileURL.appendingPathComponent("NEWER.JPG")
        try Data(repeating: 0xAA, count: 8).write(to: olderURL)
        try Data(repeating: 0xBB, count: 8).write(to: newerURL)
        defer {
            try? FileManager.default.removeItem(at: olderURL)
            try? FileManager.default.removeItem(at: newerURL)
        }

        camera.downloadURL = olderURL
        camera.onNewAsset?(
            CameraAsset(
                id: "ptp_old",
                filename: "OLDER.JPG",
                fileSize: 8,
                creationDate: Date(timeIntervalSince1970: 1_700_000_000)
            )
        )
        try await Task.sleep(nanoseconds: 400_000_000)

        camera.downloadURL = newerURL
        camera.onNewAsset?(
            CameraAsset(
                id: "ptp_new",
                filename: "NEWER.JPG",
                fileSize: 8,
                creationDate: Date(timeIntervalSince1970: 1_800_000_000)
            )
        )
        try await Task.sleep(nanoseconds: 400_000_000)

        XCTAssertEqual(viewModel.photos.count, 2)
        XCTAssertEqual(viewModel.photos[0].id, "ptp_new")
        XCTAssertEqual(viewModel.photos[1].id, "ptp_old")
    }

    /// 测试拍后传输模式下重传会从持久化记录恢复上传。
    func testRetryPhotoInPostShootModeUploadsPersistedFile() async throws {
        let camera = MockWiredCameraService()
        let context = WiredTransferContext(
            albumId: 1,
            albumName: "测试相册",
            phone: "",
            orderNumber: ""
        )
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "101", displayName: "测试"))

        let scope = downloadScope(account: account, albumID: context.albumId)
        let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "retry_failed.JPG", scope: scope)
        try Data(repeating: 0xCD, count: 32).write(to: fileURL)
        defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }

        let photoID = "persist_failed_001"
        let record = WiredTransferPhotoRecord(
            id: photoID,
            sourceId: photoID,
            fileName: "retry_failed.JPG",
            localPath: CameraDownloadStorage.relativePath(for: fileURL),
            thumbnailPath: "",
            capturedAt: "2026-05-20 12:05:18",
            fileSizeBytes: 32,
            status: WiredTransferUploadStatus.failed.rawValue,
            progress: 0,
            errorMessage: "upload failed",
            albumId: 1,
            userId: "101",
            remoteURL: "",
            updatedAt: Date().timeIntervalSince1970
        )
        let store = WiredTransferPhotoStore(
            accountPrefixProvider: { account.accountCachePrefix ?? "guest_" },
            userIDProvider: { "101" }
        )
        store.save(albumID: 1, records: [record])

        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        viewModel.transferModeOption = WiredCameraTransferViewModel.modePostShootTransfer

        let api = MockTravelAlbumAPIForWiredTransfer()
        let oss = WiredTransferMockOSSUploadService()
        await viewModel.start(api: api, ossService: oss, scenicID: 1, accountContext: account)

        XCTAssertEqual(viewModel.photos.first?.id, photoID)
        XCTAssertEqual(viewModel.photos.first?.status, .failed)

        await viewModel.retryPhoto(id: photoID, api: api, ossService: oss, scenicID: 1)
        try await Task.sleep(nanoseconds: 500_000_000)

        XCTAssertEqual(viewModel.photos.first?.status, .uploaded)
    }

    /// 测试指定上传“所有未上传”会恢复本地持久化照片并全部上传。
    func testBatchUploadAllUploadsPersistedPendingFiles() async throws {
        let albumID = 7711
        let userID = "batch-persist-user"
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: userID, displayName: "测试"))

        let fileURLs = try (1 ... 5).map { index in
            let scope = downloadScope(account: account, albumID: albumID)
            let url = CameraDownloadStorage.uniqueLocalURL(for: "batch_persist_\(index).JPG", scope: scope)
            try Data(repeating: UInt8(0xA0 + index), count: 32).write(to: url)
            return url
        }
        defer {
            CameraDownloadStorage.removeScopeDirectory(for: downloadScope(account: account, albumID: albumID))
        }

        let records = fileURLs.enumerated().map { offset, url in
            let index = offset + 1
            return makePhotoRecord(
                id: "batch_persist_\(index)",
                fileName: "batch_persist_\(index).JPG",
                localPath: CameraDownloadStorage.relativePath(for: url),
                albumID: albumID,
                userID: userID
            )
        }
        let store = WiredTransferPhotoStore(
            accountPrefixProvider: { account.accountCachePrefix ?? "guest_" },
            userIDProvider: { userID }
        )
        store.save(albumID: albumID, records: records)

        let viewModel = WiredCameraTransferViewModel(
            context: WiredTransferContext(albumId: albumID, albumName: "批量上传", phone: "", orderNumber: ""),
            cameraService: MockWiredCameraService()
        )
        let oss = WiredTransferMockOSSUploadService()
        oss.uploadDelayNanoseconds = 150_000_000
        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: oss,
            scenicID: 1,
            accountContext: account
        )

        XCTAssertEqual(viewModel.photos.filter(\.isNotUploaded).count, 5)

        await viewModel.batchUploadAll(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: oss,
            scenicID: 1
        )

        XCTAssertEqual(Set(oss.uploadedFileNames), Set((1 ... 5).map { "batch_persist_\($0).JPG" }))
        XCTAssertGreaterThan(oss.maxActiveUploads, 1)
        XCTAssertLessThanOrEqual(oss.maxActiveUploads, 3)
        XCTAssertTrue(viewModel.photos.allSatisfy { $0.status == .uploaded })
    }

    /// 测试切换传输模式后会按账号缓存选项。
    func testSelectTransferModePersistsOption() async {
        let account = makeTransferModeTestAccount()
        let context = WiredTransferContext(albumId: 7701, albumName: "模式缓存", phone: "", orderNumber: "")
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: MockWiredCameraService())
        let api = MockTravelAlbumAPIForWiredTransfer()
        let oss = WiredTransferMockOSSUploadService()

        await viewModel.start(api: api, ossService: oss, scenicID: 1, accountContext: account)
        viewModel.selectTransferMode(
            WiredCameraTransferViewModel.modePostShootTransfer,
            api: api,
            ossService: oss,
            scenicID: 1
        )

        let store = makeTransferModeTestStore(account: account)
        XCTAssertEqual(store.loadTransferModeOption(), WiredCameraTransferViewModel.modePostShootTransfer)
    }

    /// 测试再次进入有线传图页时会恢复上次选择的传输模式。
    func testStartRestoresCachedTransferModeOption() async {
        let account = makeTransferModeTestAccount()
        let store = makeTransferModeTestStore(account: account)
        store.saveTransferModeOption(WiredCameraTransferViewModel.modePostShootTransfer)

        let context = WiredTransferContext(albumId: 7702, albumName: "模式恢复", phone: "", orderNumber: "")
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: MockWiredCameraService())

        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )

        XCTAssertEqual(viewModel.transferModeOption, WiredCameraTransferViewModel.modePostShootTransfer)
    }

    /// 测试下载完成后生成本地缩略图，并优先用于列表展示。
    func testDownloadedPhotoGeneratesThumbnailForList() async throws {
        let camera = MockWiredCameraService()
        let albumID = 6601
        let context = WiredTransferContext(albumId: albumID, albumName: "缩略图相册", phone: "", orderNumber: "")
        let viewModel = WiredCameraTransferViewModel(context: context, cameraService: camera)
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "thumb-user", displayName: "测试"))

        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )

        let scope = downloadScope(account: account, albumID: albumID)
        let imageURL = CameraDownloadStorage.uniqueLocalURL(for: "thumb_source.JPG", scope: scope)
        try makeJPEGData().write(to: imageURL)
        defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }

        camera.downloadURL = imageURL
        camera.onNewAsset?(CameraAsset(id: "thumb_asset_001", filename: "thumb_source.JPG", fileSize: 128))
        try await Task.sleep(nanoseconds: 900_000_000)

        let photo = try XCTUnwrap(viewModel.photos.first { $0.id == "thumb_asset_001" })
        XCTAssertTrue(photo.thumbnailURL.contains("/CameraDownloads/\(scope.accountDirectoryName)/\(albumID)/thumbnails/"))
        XCTAssertTrue(photo.previewURL.contains("/CameraDownloads/"))
        XCTAssertFalse(photo.previewURL.contains("/thumbnails/"))
    }

    /// 测试旧记录没有缩略图时列表先占位，但仍保留原图预览能力。
    func testLegacyRecordWithoutThumbnailKeepsPreviewURL() async throws {
        let albumID = 6602
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: "legacy-user", displayName: "测试"))
        let scope = downloadScope(account: account, albumID: albumID)
        let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "legacy_only.JPG", scope: scope)
        try makeJPEGData().write(to: fileURL)
        defer { CameraDownloadStorage.removeScopeDirectory(for: scope) }

        let record = WiredTransferPhotoRecord(
            id: "legacy_photo_001",
            sourceId: "legacy_photo_001",
            fileName: "legacy_only.JPG",
            localPath: CameraDownloadStorage.relativePath(for: fileURL),
            thumbnailPath: "",
            capturedAt: "2026-05-20 12:05:18",
            fileSizeBytes: 128,
            status: WiredTransferUploadStatus.pending.rawValue,
            progress: 0,
            errorMessage: nil,
            albumId: albumID,
            userId: "legacy-user",
            remoteURL: "",
            updatedAt: Date().timeIntervalSince1970
        )
        let store = WiredTransferPhotoStore(
            accountPrefixProvider: { account.accountCachePrefix ?? "guest_" },
            userIDProvider: { "legacy-user" }
        )
        store.save(albumID: albumID, records: [record])

        let viewModel = WiredCameraTransferViewModel(
            context: WiredTransferContext(albumId: albumID, albumName: "旧记录", phone: "", orderNumber: ""),
            cameraService: MockWiredCameraService()
        )
        await viewModel.start(
            api: MockTravelAlbumAPIForWiredTransfer(),
            ossService: WiredTransferMockOSSUploadService(),
            scenicID: 1,
            accountContext: account
        )

        let photo = try XCTUnwrap(viewModel.photos.first)
        XCTAssertEqual(photo.thumbnailURL, "")
        XCTAssertFalse(photo.previewURL.isEmpty)
    }

    /// 测试清空相册时只删除当前账号当前相册的物理目录。
    func testClearAlbumRemovesOnlyCurrentAlbumScopedDirectory() throws {
        let userID = "clear-scope-user"
        let account = AccountContext()
        account.applyLogin(profile: AccountProfile(userId: userID, displayName: "测试"), accountType: "store")
        let albumID = 8801
        let otherAlbumID = 8802
        let scope = downloadScope(account: account, albumID: albumID)
        let otherScope = downloadScope(account: account, albumID: otherAlbumID)
        let scopeDirectoryPath = CameraDownloadStorage.scopeDirectory(for: scope).path
        let otherScopeDirectoryPath = CameraDownloadStorage.scopeDirectory(for: otherScope).path
        defer {
            CameraDownloadStorage.removeScopeDirectory(for: scope)
            CameraDownloadStorage.removeScopeDirectory(for: otherScope)
        }

        let fileURL = CameraDownloadStorage.uniqueLocalURL(for: "clear_target.JPG", scope: scope)
        let otherFileURL = CameraDownloadStorage.uniqueLocalURL(for: "clear_other.JPG", scope: otherScope)
        try Data(repeating: 0x01, count: 8).write(to: fileURL)
        try Data(repeating: 0x02, count: 8).write(to: otherFileURL)

        let store = WiredTransferPhotoStore(
            accountPrefixProvider: { account.accountCachePrefix ?? "guest_" },
            userIDProvider: { userID }
        )
        store.save(albumID: albumID, records: [
            makePhotoRecord(
                id: "clear_target",
                fileName: "clear_target.JPG",
                localPath: CameraDownloadStorage.relativePath(for: fileURL),
                albumID: albumID,
                userID: userID
            )
        ])
        store.save(albumID: otherAlbumID, records: [
            makePhotoRecord(
                id: "clear_other",
                fileName: "clear_other.JPG",
                localPath: CameraDownloadStorage.relativePath(for: otherFileURL),
                albumID: otherAlbumID,
                userID: userID
            )
        ])

        let deletedIDs = store.clearAlbum(albumID: albumID)

        XCTAssertEqual(deletedIDs, Set(["clear_target"]))
        XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
        XCTAssertFalse(FileManager.default.fileExists(atPath: scopeDirectoryPath))
        XCTAssertTrue(FileManager.default.fileExists(atPath: otherFileURL.path))
        XCTAssertTrue(FileManager.default.fileExists(atPath: otherScopeDirectoryPath))
    }

    /// 测试同一用户 ID 在不同账号类型下的有线传图记录与物理目录隔离。
    func testSameUserIDDifferentAccountTypesAreIsolated() throws {
        let userID = "multi-account-user"
        let albumID = 8803
        let storeAccount = AccountContext()
        storeAccount.applyLogin(profile: AccountProfile(userId: userID, displayName: "门店"), accountType: "store")
        let photographerAccount = AccountContext()
        photographerAccount.applyLogin(profile: AccountProfile(userId: userID, displayName: "摄影师"), accountType: "photographer")
        let storeScope = downloadScope(account: storeAccount, albumID: albumID)
        let photographerScope = downloadScope(account: photographerAccount, albumID: albumID)
        defer {
            CameraDownloadStorage.removeScopeDirectory(for: storeScope)
            CameraDownloadStorage.removeScopeDirectory(for: photographerScope)
        }

        let storeURL = CameraDownloadStorage.uniqueLocalURL(for: "store_photo.JPG", scope: storeScope)
        let photographerURL = CameraDownloadStorage.uniqueLocalURL(for: "photographer_photo.JPG", scope: photographerScope)
        try Data(repeating: 0x03, count: 8).write(to: storeURL)
        try Data(repeating: 0x04, count: 8).write(to: photographerURL)

        let store = WiredTransferPhotoStore(
            accountPrefixProvider: { storeAccount.accountCachePrefix ?? "guest_" },
            userIDProvider: { userID }
        )
        let photographerStore = WiredTransferPhotoStore(
            accountPrefixProvider: { photographerAccount.accountCachePrefix ?? "guest_" },
            userIDProvider: { userID }
        )
        store.save(albumID: albumID, records: [
            makePhotoRecord(
                id: "store_photo",
                fileName: "store_photo.JPG",
                localPath: CameraDownloadStorage.relativePath(for: storeURL),
                albumID: albumID,
                userID: userID
            )
        ])
        photographerStore.save(albumID: albumID, records: [
            makePhotoRecord(
                id: "photographer_photo",
                fileName: "photographer_photo.JPG",
                localPath: CameraDownloadStorage.relativePath(for: photographerURL),
                albumID: albumID,
                userID: userID
            )
        ])

        XCTAssertEqual(store.load(albumID: albumID).map(\.id), ["store_photo"])
        XCTAssertEqual(photographerStore.load(albumID: albumID).map(\.id), ["photographer_photo"])
        XCTAssertNotEqual(
            CameraDownloadStorage.scopeDirectory(for: storeScope).path,
            CameraDownloadStorage.scopeDirectory(for: photographerScope).path
        )
    }

    private func makeViewModelWithPendingPhotos() -> WiredCameraTransferViewModel {
        let viewModel = WiredCameraTransferViewModel(
            context: WiredTransferContext(albumId: 1, albumName: "相册", phone: "", orderNumber: ""),
            cameraService: MockWiredCameraService()
        )
        viewModel.replacePhotos([
            makePendingPhoto(id: "pending-1", capturedAt: "2026-05-20 12:05:18"),
            makePendingPhoto(id: "pending-2", capturedAt: "2026-05-20 12:18:06"),
            WiredTransferPhotoItem(
                id: "uploaded-1",
                fileName: "uploaded.JPG",
                thumbnailURL: "",
                previewURL: "",
                capturedAt: "2026-05-20 12:20:00",
                fileSizeText: "1 MB",
                status: .uploaded
            ),
        ])
        return viewModel
    }

    private func makePendingPhoto(id: String, capturedAt: String) -> WiredTransferPhotoItem {
        WiredTransferPhotoItem(
            id: id,
            fileName: "\(id).JPG",
            thumbnailURL: "",
            capturedAt: capturedAt,
            fileSizeText: "1 MB",
            status: .pending
        )
    }

    private func makePhotoRecord(
        id: String,
        fileName: String,
        localPath: String,
        albumID: Int,
        userID: String,
        status: WiredTransferUploadStatus = .pending
    ) -> WiredTransferPhotoRecord {
        WiredTransferPhotoRecord(
            id: id,
            sourceId: id,
            fileName: fileName,
            localPath: localPath,
            thumbnailPath: "",
            capturedAt: "2026-05-20 12:05:18",
            fileSizeBytes: 32,
            status: status.rawValue,
            progress: 0,
            errorMessage: nil,
            albumId: albumID,
            userId: userID,
            remoteURL: "",
            updatedAt: Date().timeIntervalSince1970
        )
    }

    private func makeJPEGData() -> Data {
        let renderer = UIGraphicsImageRenderer(size: CGSize(width: 8, height: 8))
        return renderer.jpegData(withCompressionQuality: 0.9) { context in
            UIColor.systemBlue.setFill()
            context.fill(CGRect(x: 0, y: 0, width: 8, height: 8))
        }
    }

    private func downloadScope(account: AccountContext, albumID: Int) -> CameraDownloadStorage.Scope {
        CameraDownloadStorage.Scope(accountKey: account.accountCachePrefix ?? "guest_", albumID: albumID)
    }
}

@MainActor
private final class DisconnectTrackingWiredCameraService: CameraServiceProtocol {
    let brand: CameraBrand = .sony
    var connectionState: CameraConnectionState = .disconnected
    var onConnectionStateChange: ((CameraConnectionState) -> Void)?
    var onNewAsset: ((CameraAsset) -> Void)?
    private(set) var connectCount = 0
    private(set) var detachCount = 0
    private(set) var shutdownCount = 0

    func connect() async {
        connectCount += 1
        connectionState = .connected(deviceName: "Mock")
        onConnectionStateChange?(.connected(deviceName: "Mock"))
        while !Task.isCancelled {
            try? await Task.sleep(nanoseconds: 50_000_000)
        }
    }

    func detachFromUI() { detachCount += 1 }
    func shutdown() async { shutdownCount += 1 }

    func listAssets() async throws -> [CameraAsset] { [] }

    func downloadAsset(_ asset: CameraAsset) async throws -> URL {
        FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename)
    }
}

@MainActor
private final class MockWiredCameraService: CameraServiceProtocol {
    let brand: CameraBrand = .sony
    var connectionState: CameraConnectionState = .connected(deviceName: "Mock")
    var onConnectionStateChange: ((CameraConnectionState) -> Void)?
    var onNewAsset: ((CameraAsset) -> Void)?
    var downloadURL: URL?

    func connect() async {}
    func detachFromUI() {}
    func shutdown() async {}

    func listAssets() async throws -> [CameraAsset] {
        []
    }

    func downloadAsset(_ asset: CameraAsset) async throws -> URL {
        if let downloadURL {
            return downloadURL
        }
        let url = FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename)
        if !FileManager.default.fileExists(atPath: url.path) {
            try Data(repeating: 0xFF, count: 8).write(to: url)
        }
        return url
    }
}

@MainActor
private final class MockTravelAlbumAPIForWiredTransfer: TravelAlbumServing {
    func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] }
    func createAlbum(_ request: TravelAlbumCreateRequest) async throws -> TravelAlbumCreateResponse {
        TravelAlbumCreateResponse(id: 1)
    }
    func albumList(page: Int, pageSize: Int) async throws -> ListPayload<TravelAlbumItem> {
        ListPayload(total: 0, list: [])
    }
    func albumInfo(id: Int) async throws -> TravelAlbumItem { throw APIError.emptyData }
    func editAlbum(_ request: TravelAlbumEditRequest) async throws {}
    func deleteAlbum(_ request: TravelAlbumDeleteRequest) async throws {}
    func materialList(
        userEquityTravelId: Int,
        page: Int,
        pageSize: Int,
        orderBy: Int,
        isPurchased: Int?
    ) async throws -> ListPayload<TravelAlbumMaterial> {
        ListPayload(total: 0, list: [])
    }
    func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial {
        let data = Data(
            #"{"code":100000,"msg":"success","data":{"id":"11","user_equity_travel_id":"8","status":"1","order_number":"","user_id":"100","file_name":"DSC_001.JPG","file_type":"2","file_url":"https://cdn/a.jpg","file_size":"2048","cover_url":"","is_purchased":"0","created_at":"2026","updated_at":"2026"}}"#.utf8
        )
        let envelope = try JSONDecoder().decode(APIEnvelope<TravelAlbumMaterial>.self, from: data)
        return try XCTUnwrap(envelope.data)
    }
    func deleteMaterial(_ request: TravelAlbumDeleteMaterialRequest) async throws {}
    func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse {
        TravelAlbumMpCodeResponse(mpCodeOssURL: "")
    }
}

@MainActor
private final class WiredTransferMockOSSUploadService: OSSUploadServing {
    private(set) var uploadedFileNames: [String] = []
    var uploadDelayNanoseconds: UInt64 = 0
    private(set) var activeUploads = 0
    private(set) var maxActiveUploads = 0

    func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        activeUploads += 1
        maxActiveUploads = max(maxActiveUploads, activeUploads)
        if uploadDelayNanoseconds > 0 {
            try await Task.sleep(nanoseconds: uploadDelayNanoseconds)
        }
        activeUploads -= 1
        uploadedFileNames.append(fileName)
        onProgress(100)
        return "https://cdn/mock/\(fileName)"
    }
    func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
}

@MainActor
private func makeTransferModeTestAccount() -> AccountContext {
    let account = AccountContext()
    account.applyLogin(profile: AccountProfile(userId: "transfer-mode-user", displayName: "测试"))
    return account
}

@MainActor
private func makeTransferModeTestStore(account: AccountContext) -> WiredTransferPhotoStore {
    WiredTransferPhotoStore(
        accountPrefixProvider: { account.accountCachePrefix ?? "guest_" },
        userIDProvider: { account.profile?.userId ?? "" }
    )
}
