//
//  LiveViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/25.
//

import XCTest
@testable import suixinkan

@MainActor
/// 直播 ViewModel 测试，覆盖列表、详情、相册和上传流程。
final class LiveViewModelTests: XCTestCase {
    func testLiveManagementReloadLoadMoreAndMissingScenicReset() async {
        let api = LiveMock()
        api.liveListResponses = [
            LiveListResponse(items: [live(id: 1, status: 2), live(id: 2, status: 3)], total: 3, page: 1, pageSize: 2),
            LiveListResponse(items: [live(id: 3, status: 1)], total: 3, page: 2, pageSize: 2)
        ]
        let viewModel = LiveManagementViewModel()

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

        XCTAssertEqual(api.liveListRequests.map(\.page), [1, 2])
        XCTAssertEqual(viewModel.items.map(\.id), [1, 2, 3])
        XCTAssertEqual(viewModel.liveRunningCount, 1)
        XCTAssertEqual(viewModel.liveFinishedCount, 1)
        XCTAssertFalse(viewModel.hasMore)

        await viewModel.reload(api: api, scenicId: nil)
        XCTAssertTrue(viewModel.items.isEmpty)
        XCTAssertNil(viewModel.detail)
        XCTAssertFalse(viewModel.hasMore)
    }

    func testLiveManagementReloadFailureAndDetailFailureClearStaleState() async {
        let api = LiveMock()
        api.liveListResponses = [LiveListResponse(items: [live(id: 1)], total: 1)]
        api.liveDetailResponses = [live(id: 1, title: "旧详情")]
        let viewModel = LiveManagementViewModel()

        await viewModel.reload(api: api, scenicId: 88)
        await viewModel.loadDetail(api: api, liveId: 1)

        XCTAssertEqual(viewModel.items.map(\.id), [1])
        XCTAssertEqual(viewModel.detail?.title, "旧详情")

        api.liveListError = LiveTestError.sample
        await viewModel.reload(api: api, scenicId: 88)

        XCTAssertTrue(viewModel.items.isEmpty)
        XCTAssertNil(viewModel.detail)
        XCTAssertFalse(viewModel.hasMore)
        XCTAssertEqual(viewModel.errorMessage, "测试错误")

        api.liveListError = nil
        api.liveDetailError = LiveTestError.sample
        await viewModel.loadDetail(api: api, liveId: 1)
        XCTAssertNil(viewModel.detail)
    }

    func testLiveManagementActionsRefreshAfterSuccess() async throws {
        let api = LiveMock()
        api.liveListResponses = [
            LiveListResponse(items: [live(id: 1, status: 1)], total: 1),
            LiveListResponse(items: [live(id: 1, status: 2)], total: 1),
            LiveListResponse(items: [live(id: 1, status: 3)], total: 1),
            LiveListResponse(items: [live(id: 4, status: 1)], total: 1)
        ]
        let viewModel = LiveManagementViewModel()
        await viewModel.reload(api: api, scenicId: 88)

        try await viewModel.control(api: api, item: live(id: 1, status: 1), scenicId: 88)
        XCTAssertEqual(api.startIds, [1])
        XCTAssertEqual(viewModel.items.first?.status, 2)

        try await viewModel.finish(api: api, item: live(id: 1, status: 2), scenicId: 88)
        XCTAssertEqual(api.finishIds, [1])
        XCTAssertEqual(viewModel.items.first?.status, 3)

        try await viewModel.create(api: api, scenicId: 88, title: "  新直播  ", coverURL: " https://cdn.example.com/c.jpg ")
        XCTAssertEqual(api.createRequests.first?.title, "新直播")
        XCTAssertEqual(api.createRequests.first?.coverImg, "https://cdn.example.com/c.jpg")
        XCTAssertEqual(viewModel.items.first?.id, 4)
    }

    func testLiveDetailActionsRefreshDetail() async throws {
        let api = LiveMock()
        api.liveDetailResponses = [
            live(id: 1, status: 2, manualPushMode: 1),
            live(id: 1, status: 3, manualPushMode: 1),
            live(id: 1, status: 3, manualPushMode: 2)
        ]
        let viewModel = LiveDetailViewModel(detail: live(id: 1, status: 1, manualPushMode: 1))

        try await viewModel.control(api: api)
        XCTAssertEqual(api.startIds, [1])
        XCTAssertEqual(viewModel.detail.status, 2)

        try await viewModel.finish(api: api)
        XCTAssertEqual(api.finishIds, [1])
        XCTAssertEqual(viewModel.detail.status, 3)

        try await viewModel.setPushMode(api: api, mode: 2)
        XCTAssertEqual(api.pushModeRequests.map(\.mode), [2])
        XCTAssertEqual(viewModel.detail.manualPushMode, 2)
    }

    func testLiveAlbumDateValidationReloadLoadMoreAndFailureBehavior() async {
        let api = LiveMock()
        api.albumListResponses = [
            LiveAlbumFolderListResponse(items: [folder(id: 1)], page: 1, pageSize: 1, total: 2),
            LiveAlbumFolderListResponse(items: [folder(id: 2)], page: 2, pageSize: 1, total: 2)
        ]
        let viewModel = LiveAlbumViewModel()

        XCTAssertNoThrow(try viewModel.setStartDate(date("2026-05-01")))
        XCTAssertNoThrow(try viewModel.setEndDate(date("2026-05-23")))
        await viewModel.reload(api: api, scenicId: 88)
        await viewModel.loadMore(api: api, scenicId: 88)

        XCTAssertEqual(api.albumListRequests.map(\.page), [1, 2])
        XCTAssertEqual(api.albumListRequests.first?.startTime, "2026-05-01")
        XCTAssertEqual(api.albumListRequests.first?.endTime, "2026-05-23")
        XCTAssertEqual(viewModel.folders.map(\.id), [1, 2])
        XCTAssertFalse(viewModel.hasMore)
        XCTAssertThrowsError(try viewModel.setStartDate(date("2026-06-01")))

        api.albumListError = LiveTestError.sample
        await viewModel.reload(api: api, scenicId: 88)
        XCTAssertTrue(viewModel.folders.isEmpty)
        XCTAssertFalse(viewModel.hasMore)

        await viewModel.reload(api: api, scenicId: nil)
        XCTAssertTrue(viewModel.folders.isEmpty)
    }

    func testLiveAlbumCreateUploadsFilesAndClearsForm() async throws {
        let api = LiveMock()
        let uploader = LiveUploadMock()
        let uploadedURLs = ["https://cdn.example.com/live_albums/a.jpg", "https://cdn.example.com/live_albums/b.mp4"]
        uploader.urls = uploadedURLs
        let viewModel = LiveAlbumCreateViewModel()
        viewModel.name = " 五一相册 "
        viewModel.addLocalFiles([
            LiveAlbumLocalUploadFile(data: Data([1, 2, 3]), fileName: "a.jpg", fileType: 1),
            LiveAlbumLocalUploadFile(data: Data([4, 5, 6]), fileName: "b.mp4", fileType: 2)
        ])

        try await viewModel.submit(scenicId: 88, api: api, uploader: uploader)

        XCTAssertEqual(uploader.uploads.map(\.fileName), ["a.jpg", "b.mp4"])
        let request = try XCTUnwrap(api.albumCreateRequests.first)
        XCTAssertEqual(request.scenicId, "88")
        XCTAssertEqual(request.name, "五一相册")
        XCTAssertEqual(request.items.map(\.url), uploadedURLs)
        XCTAssertEqual(request.items.map(\.type), [1, 2])
        XCTAssertTrue(viewModel.name.isEmpty)
        XCTAssertTrue(viewModel.localFiles.isEmpty)
        XCTAssertEqual(viewModel.uploadProgress, 100)
    }

    func testLiveAlbumCreateFailureKeepsForm() async {
        let api = LiveMock()
        let uploader = LiveUploadMock()
        uploader.error = LiveTestError.sample
        let viewModel = LiveAlbumCreateViewModel()
        viewModel.name = "相册"
        viewModel.addLocalFiles([LiveAlbumLocalUploadFile(data: Data([1]), fileName: "a.jpg", fileType: 1)])

        await XCTAssertThrowsErrorAsync(try await viewModel.submit(scenicId: 88, api: api, uploader: uploader))

        XCTAssertEqual(viewModel.name, "相册")
        XCTAssertEqual(viewModel.localFiles.count, 1)
        XCTAssertTrue(api.albumCreateRequests.isEmpty)
    }

    func testLiveAlbumPreviewLoadAndDeleteRefresh() async throws {
        let api = LiveMock()
        api.albumDetailResponses = [
            folder(id: 1, items: [albumFile(id: 11), albumFile(id: 12)]),
            folder(id: 1, items: [albumFile(id: 12)])
        ]
        let viewModel = LiveAlbumPreviewViewModel(folderId: 1, startIndex: 0, summary: nil)

        await viewModel.load(api: api)
        XCTAssertEqual(viewModel.files.map(\.id), [11, 12])

        try await viewModel.deleteCurrentFile(api: api)
        XCTAssertEqual(api.albumDeleteFileRequests.first?.fileIds, [11])
        XCTAssertEqual(viewModel.files.map(\.id), [12])
    }

    func testLiveEntityDecodesPlaybackURLsAndResolverExcludesRTMP() throws {
        let json = """
        {
          "id": "1",
          "title": "播放测试",
          "cover_img": "https://cdn.example.com/c.jpg",
          "push_url": "rtmp://push.example.com/live/1",
          "play_url": "rtmp://pull.example.com/live/1",
          "pull_url": "https://cdn.example.com/live/1.flv",
          "hls_url": "https://cdn.example.com/live/1.m3u8",
          "live_url": "https://cdn.example.com/live/1.mp4"
        }
        """.data(using: .utf8)!

        let live = try JSONDecoder().decode(LiveEntity.self, from: json)

        XCTAssertEqual(live.playUrl, "rtmp://pull.example.com/live/1")
        XCTAssertEqual(live.pullUrl, "https://cdn.example.com/live/1.flv")
        XCTAssertEqual(live.hlsUrl, "https://cdn.example.com/live/1.m3u8")
        XCTAssertNil(LivePlaybackURLResolver.playableURL(from: live.pushUrl))
        XCTAssertNil(LivePlaybackURLResolver.playableURL(from: live.playUrl))
        XCTAssertNil(LivePlaybackURLResolver.playableURL(from: live.pullUrl))
        XCTAssertEqual(LivePlaybackURLResolver.playableURL(from: live)?.absoluteString, "https://cdn.example.com/live/1.m3u8")
    }

    func testLivePlaybackViewModelStateTransitions() {
        let viewModel = LivePlaybackViewModel(urlString: "https://cdn.example.com/live/1.m3u8")

        XCTAssertEqual(viewModel.playableURL?.absoluteString, "https://cdn.example.com/live/1.m3u8")
        XCTAssertFalse(viewModel.isPlaying)

        viewModel.play()
        XCTAssertTrue(viewModel.isPlaying)
        XCTAssertNotNil(viewModel.player)

        viewModel.pause()
        XCTAssertFalse(viewModel.isPlaying)

        viewModel.release()
        XCTAssertNil(viewModel.player)
        XCTAssertEqual(viewModel.playableURL?.absoluteString, "https://cdn.example.com/live/1.m3u8")

        viewModel.load(urlString: "rtmp://push.example.com/live/1")
        XCTAssertNil(viewModel.playableURL)
        XCTAssertEqual(viewModel.errorMessage, "暂无可播放地址")
    }

    func testLivePushReadinessPermissionsNetworkAndUnsupportedAdapter() async {
        let permissions = LivePermissionMock(camera: .granted, microphone: .granted)
        let network = LiveNetworkMock(state: .wifi)
        let adapter = LivePushAdapterMock(available: false)
        let viewModel = LivePushReadinessViewModel(permissionProvider: permissions, networkMonitor: network, adapter: adapter)

        viewModel.configure(pushURL: "rtmp://push.example.com/live/1")
        viewModel.startMonitoring()
        await viewModel.refreshPermissions()

        XCTAssertEqual(viewModel.cameraPermission, .granted)
        XCTAssertEqual(viewModel.microphonePermission, .granted)
        XCTAssertEqual(viewModel.networkState, .wifi)
        XCTAssertEqual(viewModel.sdkStatusText, "未接入真推流 SDK")

        await XCTAssertThrowsErrorAsync(try await viewModel.prepare())
        XCTAssertEqual(viewModel.errorMessage, LivePushReadinessError.sdkUnavailable.localizedDescription)
    }

    func testLivePushReadinessDiagnosticsDoNotPrepareUnsupportedAdapter() async {
        let permissions = LivePermissionMock(camera: .granted, microphone: .granted)
        let network = LiveNetworkMock(state: .wifi)
        let adapter = LivePushAdapterMock(available: false)
        let viewModel = LivePushReadinessViewModel(permissionProvider: permissions, networkMonitor: network, adapter: adapter)

        viewModel.configure(pushURL: "rtmp://push.example.com/live/1")
        viewModel.startMonitoring()
        await viewModel.refreshPermissions()

        XCTAssertThrowsError(try viewModel.runDiagnostics())
        XCTAssertFalse(adapter.prepareCalled)
        XCTAssertFalse(adapter.startCalled)
        XCTAssertEqual(viewModel.errorMessage, LivePushReadinessError.sdkUnavailable.localizedDescription)
    }

    func testLivePushReadinessBlocksDeniedPermissionAndUnavailableNetwork() async {
        let permissions = LivePermissionMock(camera: .denied, microphone: .granted)
        let network = LiveNetworkMock(state: .unavailable)
        let adapter = LivePushAdapterMock(available: true)
        let viewModel = LivePushReadinessViewModel(permissionProvider: permissions, networkMonitor: network, adapter: adapter)

        viewModel.configure(pushURL: "rtmp://push.example.com/live/1")
        viewModel.startMonitoring()
        await viewModel.refreshPermissions()

        await XCTAssertThrowsErrorAsync(try await viewModel.prepare())
        XCTAssertFalse(adapter.prepareCalled)

        let missingURLViewModel = LivePushReadinessViewModel(permissionProvider: permissions, networkMonitor: network, adapter: adapter)
        missingURLViewModel.configure(pushURL: "")
        await XCTAssertThrowsErrorAsync(try await missingURLViewModel.prepare())
        XCTAssertEqual(missingURLViewModel.errorMessage, LivePushReadinessError.missingPushURL.localizedDescription)
    }

    /// 测试播放地址解析会跳过 RTMP/FLV 并优先选择可用 HTTP 地址。
    func testLivePlaybackResolverCandidatePriority() {
        let live = LiveEntity(
            playUrl: "rtmp://pull.example.com/live/1",
            pullUrl: "https://cdn.example.com/live/1.flv",
            hlsUrl: "https://cdn.example.com/live/1.m3u8",
            liveUrl: "https://cdn.example.com/live/1.mp4"
        )

        XCTAssertEqual(
            LivePlaybackURLResolver.playableURL(from: live)?.absoluteString,
            "https://cdn.example.com/live/1.m3u8"
        )
        XCTAssertEqual(
            LivePlaybackURLResolver.playableURL(from: ["rtmp://a", "https://cdn.example.com/fallback.mp4"])?.absoluteString,
            "https://cdn.example.com/fallback.mp4"
        )
    }

    /// 测试播放器可从 LiveEntity 初始化并支持 reload。
    func testLivePlaybackViewModelLoadLiveEntityAndReload() {
        let live = LiveEntity(hlsUrl: "https://cdn.example.com/live/1.m3u8")
        let viewModel = LivePlaybackViewModel(live: live)

        XCTAssertEqual(viewModel.playableURL?.absoluteString, "https://cdn.example.com/live/1.m3u8")
        XCTAssertNotNil(viewModel.player)

        viewModel.play()
        XCTAssertTrue(viewModel.isPlaying)

        viewModel.reload()
        XCTAssertFalse(viewModel.isPlaying)
        XCTAssertNotNil(viewModel.player)
        XCTAssertEqual(viewModel.playableURL?.absoluteString, "https://cdn.example.com/live/1.m3u8")
    }

    /// 测试无效推流地址配置会提示格式错误。
    func testLivePushReadinessInvalidPushURL() {
        let viewModel = LivePushReadinessViewModel(
            permissionProvider: LivePermissionMock(camera: .granted, microphone: .granted),
            networkMonitor: LiveNetworkMock(state: .wifi),
            adapter: LivePushAdapterMock(available: true)
        )

        viewModel.configure(pushURL: "not-a-valid-url")

        XCTAssertEqual(viewModel.errorMessage, LivePushReadinessError.invalidPushURL.localizedDescription)
    }

    /// 测试可用 SDK 时 prepare、start、stop 和 dispose 的完整流程。
    func testLivePushReadinessStartStopAndDispose() async throws {
        let permissions = LivePermissionMock(camera: .granted, microphone: .granted)
        let network = LiveNetworkMock(state: .wifi)
        let adapter = LivePushAdapterMock(available: true)
        let viewModel = LivePushReadinessViewModel(permissionProvider: permissions, networkMonitor: network, adapter: adapter)

        viewModel.configure(pushURL: "rtmp://push.example.com/live/1")
        viewModel.startMonitoring()
        await viewModel.refreshPermissions()

        try viewModel.runDiagnostics()
        try await viewModel.prepare()
        try await viewModel.startPush()

        XCTAssertTrue(adapter.prepareCalled)
        XCTAssertTrue(adapter.startCalled)
        XCTAssertTrue(viewModel.running)
        XCTAssertTrue(viewModel.prepared)

        await viewModel.stopPush()
        XCTAssertTrue(adapter.stopCalled)
        XCTAssertFalse(viewModel.running)

        await viewModel.dispose()
        XCTAssertFalse(viewModel.prepared)
    }

    /// 测试权限和网络状态展示文案。
    func testLivePushStateDisplayText() {
        XCTAssertEqual(LivePushPermissionState.granted.displayText, "已授权")
        XCTAssertEqual(LivePushNetworkState.wifi.displayText, "Wi-Fi")
        XCTAssertEqual(LivePushNetworkState.unavailable.displayText, "网络不可用")
    }
}

@MainActor
private final class LiveMock: LiveServing {
    var liveListResponses: [LiveListResponse] = []
    var liveDetailResponses: [LiveEntity] = []
    var albumListResponses: [LiveAlbumFolderListResponse] = []
    var albumDetailResponses: [LiveAlbumFolderItem] = []
    var liveListError: Error?
    var liveDetailError: Error?
    var albumListError: Error?
    var albumDetailError: Error?
    var createRequests: [LiveCreateRequest] = []
    var startIds: [Int] = []
    var stopIds: [Int] = []
    var finishIds: [Int] = []
    var pushModeRequests: [(liveId: Int, mode: Int)] = []
    var albumCreateRequests: [LiveAlbumCreateFolderRequest] = []
    var albumDeleteFolderIds: [Int] = []
    var albumDeleteFileRequests: [(folderId: Int, fileIds: [Int])] = []
    private(set) var liveListRequests: [(scenicId: Int, page: Int, pageSize: Int)] = []
    private(set) var albumListRequests: [(scenicId: Int, startTime: String?, endTime: String?, page: Int, pageSize: Int)] = []

    func liveList(scenicId: Int, page: Int, pageSize: Int) async throws -> LiveListResponse {
        liveListRequests.append((scenicId, page, pageSize))
        if let liveListError { throw liveListError }
        return liveListResponses.isEmpty ? LiveListResponse() : liveListResponses.removeFirst()
    }

    func liveDetail(liveId: Int) async throws -> LiveEntity {
        if let liveDetailError { throw liveDetailError }
        return liveDetailResponses.isEmpty ? live(id: liveId) : liveDetailResponses.removeFirst()
    }

    func liveCreate(_ request: LiveCreateRequest) async throws {
        createRequests.append(request)
    }

    func liveStart(liveId: Int) async throws {
        startIds.append(liveId)
    }

    func liveStop(liveId: Int) async throws {
        stopIds.append(liveId)
    }

    func liveFinish(liveId: Int) async throws {
        finishIds.append(liveId)
    }

    func liveSetPushMode(liveId: Int, mode: Int) async throws {
        pushModeRequests.append((liveId, mode))
    }

    func liveAlbumList(scenicId: Int, startTime: String?, endTime: String?, page: Int, pageSize: Int) async throws -> LiveAlbumFolderListResponse {
        albumListRequests.append((scenicId, startTime, endTime, page, pageSize))
        if let albumListError { throw albumListError }
        return albumListResponses.isEmpty ? LiveAlbumFolderListResponse() : albumListResponses.removeFirst()
    }

    func liveAlbumCreateFolder(_ request: LiveAlbumCreateFolderRequest) async throws {
        albumCreateRequests.append(request)
    }

    func liveAlbumDeleteFolder(folderId: Int) async throws {
        albumDeleteFolderIds.append(folderId)
    }

    func liveAlbumFolderDetail(folderId: Int) async throws -> LiveAlbumFolderItem {
        if let albumDetailError { throw albumDetailError }
        return albumDetailResponses.isEmpty ? folder(id: folderId) : albumDetailResponses.removeFirst()
    }

    func liveAlbumDeleteFiles(folderId: Int, fileIds: [Int]) async throws {
        albumDeleteFileRequests.append((folderId, fileIds))
    }
}

@MainActor
private final class LiveUploadMock: OSSUploadServing {
    var urls: [String] = []
    var error: Error?
    private(set) var uploads: [(data: Data, fileName: String, fileType: Int, scenicId: Int)] = []

    func uploadAliveAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if let error { throw error }
        uploads.append((data, fileName, fileType, scenicId))
        onProgress(100)
        return urls.isEmpty ? "https://cdn.example.com/\(fileName)" : urls.removeFirst()
    }

    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 { "" }
    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 { "" }
}

private struct LivePermissionMock: LivePermissionProviding {
    let camera: LivePushPermissionState
    let microphone: LivePushPermissionState

    func cameraPermission() async -> LivePushPermissionState {
        camera
    }

    func microphonePermission() async -> LivePushPermissionState {
        microphone
    }
}

private final class LiveNetworkMock: LiveNetworkMonitoring {
    private(set) var currentState: LivePushNetworkState
    private var onChange: (@Sendable (LivePushNetworkState) -> Void)?

    init(state: LivePushNetworkState) {
        currentState = state
    }

    func start(_ onChange: @escaping @Sendable (LivePushNetworkState) -> Void) {
        self.onChange = onChange
        onChange(currentState)
    }

    func stop() {
        onChange = nil
    }
}

private final class LivePushAdapterMock: LivePushAdapter {
    let name = "Mock SDK"
    let isAvailable: Bool
    var prepareCalled = false
    var startCalled = false
    var stopCalled = false

    init(available: Bool) {
        isAvailable = available
    }

    func prepare(pushURL: URL) async throws {
        prepareCalled = true
        if !isAvailable {
            throw LivePushReadinessError.sdkUnavailable
        }
    }

    func start() async throws {
        startCalled = true
        if !isAvailable {
            throw LivePushReadinessError.sdkUnavailable
        }
    }

    func stop() async throws {
        stopCalled = true
    }

    func dispose() async {}
}

private func live(id: Int, title: String = "直播", status: Int = 1, manualPushMode: Int = 1) -> LiveEntity {
    LiveEntity(
        id: id,
        title: title,
        coverImg: "https://cdn.example.com/\(id).jpg",
        pushUrl: "rtmp://push.example.com/live/\(id)",
        duration: 3600,
        status: status,
        statusLabel: status == 2 ? "直播中" : (status == 3 ? "已结束" : "待开始"),
        manualPushMode: manualPushMode,
        viewsCount: 10
    )
}

private func folder(id: Int, items: [LiveAlbumFileItem] = [albumFile(id: 1)]) -> LiveAlbumFolderItem {
    LiveAlbumFolderItem(id: id, albumId: id + 100, name: "相册\(id)", creator: "摄影师", items: items)
}

private func albumFile(id: Int) -> LiveAlbumFileItem {
    LiveAlbumFileItem(id: id, url: "https://cdn.example.com/\(id).jpg", type: 1, size: 100)
}

private func date(_ value: String) -> Date {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    formatter.timeZone = TimeZone(secondsFromGMT: 0)
    return formatter.date(from: value)!
}

private enum LiveTestError: LocalizedError {
    case sample
    var errorDescription: String? { "测试错误" }
}

private func XCTAssertThrowsErrorAsync<T>(
    _ expression: @autoclosure () async throws -> T,
    file: StaticString = #filePath,
    line: UInt = #line
) async {
    do {
        _ = try await expression()
        XCTFail("Expected error", file: file, line: line)
    } catch {}
}
