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

import Foundation
import XCTest
@testable import suixinkan

@MainActor
/// 任务 ViewModel 测试，覆盖列表、发布任务和云盘选择逻辑。
final class TaskViewModelTests: XCTestCase {
    /// 测试无景区时清空任务列表且不请求接口。
    func testTaskManagementClearsWithoutScenic() async throws {
        let api = TaskMockService()
        api.taskListResponses = [try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_success")]
        let viewModel = TaskManagementViewModel()

        try await viewModel.reload(api: api, scenicId: 88)
        XCTAssertFalse(viewModel.tasks.isEmpty)

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

        XCTAssertTrue(viewModel.tasks.isEmpty)
        XCTAssertEqual(viewModel.total, 0)
        XCTAssertEqual(viewModel.page, 1)
        XCTAssertEqual(api.taskListCalls.count, 1)
        XCTAssertEqual(viewModel.errorMessage, "请先选择景区")
    }

    /// 测试任务列表首屏和加载更多会更新分页。
    func testTaskManagementReloadAndLoadMore() async throws {
        let api = TaskMockService()
        api.taskListResponses = [
            try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_success"),
            try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_page2_success")
        ]
        let viewModel = TaskManagementViewModel()

        try await viewModel.reload(api: api, scenicId: 88)
        viewModel.total = 3
        try await viewModel.loadMore(api: api, scenicId: 88)

        XCTAssertEqual(viewModel.tasks.count, 3)
        XCTAssertEqual(viewModel.page, 2)
        XCTAssertEqual(api.taskListCalls.map(\.page), [1, 2])
    }

    /// 测试筛选参数会传给任务服务层。
    func testTaskManagementUsesFilters() async throws {
        let api = TaskMockService()
        api.taskListResponses = [try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_success")]
        let viewModel = TaskManagementViewModel()
        viewModel.selectedStatus = .accepted
        viewModel.searchText = " 亲子 "
        viewModel.startDate = Self.date("2026-06-01")
        viewModel.endDate = Self.date("2026-06-30")

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

        let call = try XCTUnwrap(api.taskListCalls.first)
        XCTAssertEqual(call.taskStatus, TaskStatusFilter.accepted.rawValue)
        XCTAssertEqual(call.taskName, "亲子")
        XCTAssertEqual(call.startTime, "2026-06-01")
        XCTAssertEqual(call.endTime, "2026-06-30")
    }

    /// 测试任务详情加载成功后保存详情。
    func testTaskDetailLoadsDetail() async throws {
        let api = TaskMockService()
        api.detailResponse = try TestFixture.payload(TaskDetailResponse.self, named: "task_detail_success")
        let viewModel = TaskDetailViewModel()

        await viewModel.load(api: api, taskId: 101)

        XCTAssertEqual(api.detailIds, [101])
        XCTAssertEqual(viewModel.detail?.name, "游客精修任务")
        XCTAssertNil(viewModel.errorMessage)
    }

    /// 测试发布任务校验必填项和景区。
    func testTaskCreateValidation() async throws {
        let api = TaskMockService()
        let upload = TaskMockUploadService()
        let viewModel = TaskCreateViewModel()

        let noScenic = try await viewModel.submit(api: api, uploadService: upload, scenicId: nil)
        XCTAssertFalse(noScenic)
        XCTAssertEqual(viewModel.errorMessage, "请先选择景区")

        viewModel.taskName = "任务"
        let noRemark = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
        XCTAssertFalse(noRemark)
        XCTAssertEqual(viewModel.errorMessage, "请输入任务备注")
    }

    /// 测试发布任务会把云盘文件和本地上传文件写入请求。
    func testTaskCreateUploadsLocalFilesBeforeSubmit() async throws {
        let api = TaskMockService()
        let upload = TaskMockUploadService()
        upload.taskUploadURLs = ["https://cdn.example.com/task/a.jpg"]
        let viewModel = TaskCreateViewModel()
        viewModel.taskName = "任务"
        viewModel.remark = "备注"
        viewModel.urgentHourText = "6"
        viewModel.mergeCloudFiles([TaskCloudSelectionItem(id: 501, fileName: "cloud.jpg", fileType: 2, remark: "")])
        viewModel.addLocalFile(data: Data([1, 2, 3]), fileName: "local.jpg")

        let success = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)

        XCTAssertTrue(success)
        XCTAssertEqual(upload.taskUploads.count, 1)
        let request = try XCTUnwrap(api.addTaskRequests.first)
        XCTAssertEqual(request.scenicId, "88")
        XCTAssertEqual(request.name, "任务")
        XCTAssertEqual(request.cloudFile.first?.fileId, 501)
        XCTAssertEqual(request.uploadFile.first?.fileUrl, "https://cdn.example.com/task/a.jpg")
        XCTAssertTrue(viewModel.selectedLocalFiles.isEmpty)
    }

    /// 测试本地文件上传失败时不会提交发布任务。
    func testTaskCreateUploadFailureStopsSubmit() async {
        let api = TaskMockService()
        let upload = TaskMockUploadService()
        upload.uploadError = APIError.networkFailed("上传失败")
        let viewModel = TaskCreateViewModel()
        viewModel.taskName = "任务"
        viewModel.remark = "备注"
        viewModel.addLocalFile(data: Data([1]), fileName: "local.jpg")

        do {
            _ = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
            XCTFail("Expected upload failure")
        } catch APIError.networkFailed(let message) {
            XCTAssertEqual(message, "上传失败")
        } catch {
            XCTFail("Unexpected error: \(error)")
        }

        XCTAssertTrue(api.addTaskRequests.isEmpty)
        XCTAssertEqual(viewModel.selectedLocalFiles.first?.errorMessage, "网络请求失败：上传失败")
    }

    /// 测试云盘选择器进入文件夹、分页、选择文件和忽略文件夹选择。
    func testCloudSelectionNavigationAndSelection() async throws {
        let api = TaskMockService()
        let firstPage = try TestFixture.payload(ListPayload<CloudDriveFile>.self, named: "cloud_file_list_success")
        let secondPage = ListPayload(total: 3, list: [firstPage.list[1]])
        api.cloudFileResponses = [firstPage, secondPage, firstPage]
        let viewModel = TaskCloudFileSelectionViewModel()

        try await viewModel.reload(api: api)
        viewModel.total = 3
        try await viewModel.loadMore(api: api)
        XCTAssertEqual(viewModel.files.count, 3)

        let folder = firstPage.list[0]
        try await viewModel.enterFolder(folder, api: api)
        XCTAssertEqual(viewModel.currentFolderId, folder.id)

        viewModel.toggleSelection(folder)
        XCTAssertTrue(viewModel.selectedFiles.isEmpty)

        let file = firstPage.list[1]
        viewModel.toggleSelection(file)
        XCTAssertEqual(viewModel.makeSelectionItems().first?.id, file.id)
        XCTAssertEqual(api.cloudFileCalls.map(\.page), [1, 2, 1])
    }

    /// 测试发布任务会加载可关联订单，无景区时清空列表。
    func testTaskCreateLoadAvailableOrders() async throws {
        let api = TaskMockService()
        api.availableOrders = try TestFixture.payload([AvailableOrderResponse].self, named: "available_order_success")
        let viewModel = TaskCreateViewModel()

        await viewModel.loadAvailableOrders(api: api, scenicId: nil)
        XCTAssertTrue(viewModel.availableOrders.isEmpty)
        XCTAssertTrue(api.availableOrderScenicIds.isEmpty)

        await viewModel.loadAvailableOrders(api: api, scenicId: 88)
        XCTAssertEqual(api.availableOrderScenicIds, [88])
        XCTAssertEqual(viewModel.availableOrders.count, 1)
        XCTAssertFalse(viewModel.isLoadingOrders)
    }

    /// 测试加载可关联订单失败时会清空列表并提示错误。
    func testTaskCreateLoadAvailableOrdersFailure() async {
        let api = TaskMockService()
        api.availableOrderError = APIError.networkFailed("订单加载失败")
        let viewModel = TaskCreateViewModel()

        await viewModel.loadAvailableOrders(api: api, scenicId: 88)

        XCTAssertTrue(viewModel.availableOrders.isEmpty)
        XCTAssertEqual(viewModel.errorMessage, "网络请求失败：订单加载失败")
    }

    /// 测试上传中的本地附件会阻止提交。
    func testTaskCreateCanSubmitBlocksWhileUploading() {
        let viewModel = TaskCreateViewModel()
        viewModel.taskName = "任务"
        viewModel.remark = "备注"
        viewModel.selectedLocalFiles = [
            TaskLocalUploadItem(
                id: UUID(),
                data: Data([1]),
                fileName: "local.jpg",
                fileType: 2,
                remark: "",
                uploadedURL: nil,
                progress: 50,
                errorMessage: nil
            )
        ]

        XCTAssertFalse(viewModel.canSubmit)
        XCTAssertTrue(viewModel.selectedLocalFiles.first?.isUploading == true)
        XCTAssertEqual(viewModel.selectedLocalFiles.first?.statusText, "上传中 50%")
    }

    /// 测试云盘附件合并会去重，移除接口能删除指定文件。
    func testTaskCreateMergeAndRemoveAttachments() {
        let viewModel = TaskCreateViewModel()
        let file = TaskCloudSelectionItem(id: 501, fileName: "cloud.jpg", fileType: 2, remark: "备注")
        viewModel.mergeCloudFiles([file, file])
        viewModel.addLocalFile(data: Data([1]), fileName: "photo.jpg")
        guard let localID = viewModel.selectedLocalFiles.first?.id else {
            return XCTFail("应存在本地附件")
        }

        XCTAssertEqual(viewModel.selectedCloudFiles.count, 1)
        viewModel.removeCloudFile(id: 501)
        viewModel.removeLocalFile(id: localID)
        XCTAssertTrue(viewModel.selectedCloudFiles.isEmpty)
        XCTAssertTrue(viewModel.selectedLocalFiles.isEmpty)
    }

    /// 测试关联订单和紧急小时会写入发布任务请求。
    func testTaskCreateSubmitIncludesSelectedOrderAndUrgentHour() async throws {
        let api = TaskMockService()
        let upload = TaskMockUploadService()
        let viewModel = TaskCreateViewModel()
        viewModel.taskName = "  任务  "
        viewModel.remark = "备注"
        viewModel.urgentHourText = " 12 "
        viewModel.selectOrder(taskAvailableOrder(orderNumber: "ORD-1001"))

        let success = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)

        XCTAssertTrue(success)
        let request = try XCTUnwrap(api.addTaskRequests.first)
        XCTAssertEqual(request.name, "任务")
        XCTAssertEqual(request.orderNumber, "ORD-1001")
        XCTAssertEqual(request.urgentHour, 12)
    }

    /// 测试任务名称和紧急小时校验失败时不会提交。
    func testTaskCreateValidationCoversTaskNameAndUrgentHour() async throws {
        let api = TaskMockService()
        let upload = TaskMockUploadService()
        let viewModel = TaskCreateViewModel()
        viewModel.remark = "备注"

        let missingName = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
        XCTAssertFalse(missingName)
        XCTAssertEqual(viewModel.errorMessage, "请输入任务名称")

        viewModel.taskName = "任务"
        viewModel.urgentHourText = "abc"
        let invalidHour = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
        XCTAssertFalse(invalidHour)
        XCTAssertEqual(viewModel.errorMessage, "请输入正确的紧急小时")
    }

    /// 测试本地视频附件会识别为视频类型。
    func testTaskCreateDetectsVideoFileType() {
        let viewModel = TaskCreateViewModel()
        viewModel.addLocalFile(data: Data([1]), fileName: "clip.mp4")

        XCTAssertEqual(viewModel.selectedLocalFiles.first?.fileType, 1)
    }

    /// 测试云盘选择器支持返回上级目录、搜索筛选和取消选择。
    func testCloudSelectionPopToFolderSearchAndDeselect() async throws {
        let api = TaskMockService()
        let firstPage = try TestFixture.payload(ListPayload<CloudDriveFile>.self, named: "cloud_file_list_success")
        api.cloudFileResponses = [firstPage, firstPage]
        let viewModel = TaskCloudFileSelectionViewModel()
        let folder = firstPage.list[0]
        let file = firstPage.list[1]

        try await viewModel.enterFolder(folder, api: api)
        viewModel.searchText = " photo "
        viewModel.selectedFilter = .image
        try await viewModel.reload(api: api)

        let call = try XCTUnwrap(api.cloudFileCalls.last)
        XCTAssertEqual(call.parentFolderId, folder.id)
        XCTAssertEqual(call.name, "photo")
        XCTAssertEqual(call.type, TaskCloudFileFilter.image.rawValue)

        viewModel.toggleSelection(file)
        XCTAssertTrue(viewModel.isSelected(file))
        viewModel.toggleSelection(file)
        XCTAssertFalse(viewModel.isSelected(file))

        try await viewModel.popToFolder(at: 0, api: api)
        XCTAssertEqual(viewModel.currentFolderId, 0)
    }

    /// 测试云盘列表加载失败时会清空状态并抛出错误。
    func testCloudSelectionReloadFailureClearsState() async {
        let api = TaskMockService()
        api.cloudFileError = APIError.networkFailed("云盘失败")
        let viewModel = TaskCloudFileSelectionViewModel()
        viewModel.files = [CloudDriveFile(id: 1, name: "占位")]
        viewModel.total = 5
        viewModel.page = 2

        do {
            try await viewModel.reload(api: api)
            XCTFail("加载失败时应抛出错误")
        } catch APIError.networkFailed(let message) {
            XCTAssertEqual(message, "云盘失败")
        } catch {
            XCTFail("意外错误: \(error)")
        }

        XCTAssertTrue(viewModel.files.isEmpty)
        XCTAssertEqual(viewModel.total, 0)
        XCTAssertEqual(viewModel.page, 1)
        XCTAssertEqual(viewModel.errorMessage, "网络请求失败：云盘失败")
    }

    /// 测试云盘没有更多数据时不会继续分页。
    func testCloudSelectionLoadMoreStopsWhenNoMore() async throws {
        let api = TaskMockService()
        let page = try TestFixture.payload(ListPayload<CloudDriveFile>.self, named: "cloud_file_list_success")
        api.cloudFileResponses = [page]
        let viewModel = TaskCloudFileSelectionViewModel()

        try await viewModel.reload(api: api)
        viewModel.total = viewModel.files.count
        try await viewModel.loadMore(api: api)

        XCTAssertEqual(api.cloudFileCalls.count, 1)
        XCTAssertFalse(viewModel.isLoadingMore)
    }

    /// 构造固定日期。
    private static func date(_ text: String) -> Date {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter.date(from: text)!
    }
}

@MainActor
/// 任务服务测试替身。
private final class TaskMockService: TaskServing {
    struct TaskListCall {
        let scenicId: Int
        let page: Int
        let pageSize: Int
        let taskStatus: Int
        let taskName: String?
        let startTime: String?
        let endTime: String?
    }

    struct CloudFileCall {
        let parentFolderId: Int
        let name: String
        let type: Int
        let orderBy: Int
        let page: Int
        let pageSize: Int
    }

    var taskListResponses: [ListPayload<PhotographerTaskItem>] = []
    var detailResponse: TaskDetailResponse?
    var availableOrders: [AvailableOrderResponse] = []
    var availableOrderError: Error?
    var cloudFileResponses: [ListPayload<CloudDriveFile>] = []
    var cloudFileError: Error?
    private(set) var taskListCalls: [TaskListCall] = []
    private(set) var detailIds: [Int] = []
    private(set) var availableOrderScenicIds: [Int] = []
    private(set) var addTaskRequests: [AddTaskRequest] = []
    private(set) var cloudFileCalls: [CloudFileCall] = []

    /// 记录任务列表请求并返回下一份响应。
    func taskList(
        scenicId: Int,
        page: Int,
        pageSize: Int,
        taskStatus: Int,
        taskName: String?,
        startTime: String?,
        endTime: String?
    ) async throws -> ListPayload<PhotographerTaskItem> {
        taskListCalls.append(
            TaskListCall(
                scenicId: scenicId,
                page: page,
                pageSize: pageSize,
                taskStatus: taskStatus,
                taskName: taskName,
                startTime: startTime,
                endTime: endTime
            )
        )
        return taskListResponses.isEmpty ? ListPayload(total: 0, list: []) : taskListResponses.removeFirst()
    }

    /// 记录任务详情请求并返回响应。
    func taskDetail(id: Int) async throws -> TaskDetailResponse {
        detailIds.append(id)
        return detailResponse!
    }

    /// 记录可关联订单请求并返回响应。
    func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
        availableOrderScenicIds.append(scenicId)
        if let availableOrderError { throw availableOrderError }
        return availableOrders
    }

    /// 记录发布任务请求。
    func addTask(_ request: AddTaskRequest) async throws {
        addTaskRequests.append(request)
    }

    /// 记录云盘文件请求并返回下一份响应。
    func cloudFileList(
        parentFolderId: Int,
        name: String,
        type: Int,
        orderBy: Int,
        page: Int,
        pageSize: Int
    ) async throws -> ListPayload<CloudDriveFile> {
        cloudFileCalls.append(
            CloudFileCall(
                parentFolderId: parentFolderId,
                name: name,
                type: type,
                orderBy: orderBy,
                page: page,
                pageSize: pageSize
            )
        )
        if let cloudFileError { throw cloudFileError }
        return cloudFileResponses.isEmpty ? ListPayload(total: 0, list: []) : cloudFileResponses.removeFirst()
    }
}

/// 构造可关联订单测试数据。
private func taskAvailableOrder(orderNumber: String) -> AvailableOrderResponse {
    let data = Data(#"{"project_name":"项目","order_number":"\#(orderNumber)","order_status":"1","order_status_label":"已支付","pay_time":"2026-06-24","user_phone":"13800000000"}"#.utf8)
    return try! JSONDecoder().decode(AvailableOrderResponse.self, from: data)
}

@MainActor
/// OSS 上传服务测试替身。
private final class TaskMockUploadService: OSSUploadServing {
    struct TaskUpload {
        let data: Data
        let fileName: String
        let fileType: Int
        let scenicId: Int
    }

    var taskUploadURLs: [String] = []
    var uploadError: Error?
    private(set) var taskUploads: [TaskUpload] = []

    /// 模拟上传任务附件。
    func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        taskUploads.append(TaskUpload(data: data, fileName: fileName, fileType: fileType, scenicId: scenicId))
        onProgress(50)
        if let uploadError {
            throw uploadError
        }
        onProgress(100)
        return taskUploadURLs.isEmpty ? "https://cdn.example.com/task/default.jpg" : taskUploadURLs.removeFirst()
    }

    /// 模拟上传项目图片。
    func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 模拟上传头像。
    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 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 { "" }
}
