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

import Foundation
import XCTest
@testable import suixinkan

@MainActor
/// 景区权限申请测试，覆盖角色景区权限申请和新增景区申请流程。
final class ScenicPermissionViewModelTests: XCTestCase {
    /// 测试角色选项会按角色 ID 去重。
    func testPermissionApplyDeduplicatesRoles() {
        let viewModel = PermissionApplyViewModel()

        viewModel.bootstrap(rolePermissions: [
            RolePermissionResponse(role: RoleInfo(id: 1, name: "摄影师"), scenic: []),
            RolePermissionResponse(role: RoleInfo(id: 1, name: "摄影师重复"), scenic: [])
        ])

        XCTAssertEqual(viewModel.roleOptions.map(\.id), [1])
        XCTAssertEqual(viewModel.roleOptions.first?.name, "摄影师")
    }

    /// 测试已拥有的景区会被禁用，不能作为新增申请提交。
    func testPermissionApplyDisablesExistingScenics() async {
        let api = ScenicPermissionServingFake()
        api.scenicList = ScenicListAllResponse(total: 2, list: [
            ScenicListItem(id: 1, name: "已有景区"),
            ScenicListItem(id: 2, name: "新增景区")
        ])
        let viewModel = PermissionApplyViewModel()
        viewModel.bootstrap(rolePermissions: [
            RolePermissionResponse(role: RoleInfo(id: 1, name: "摄影师"), scenic: [ScenicInfo(id: 1, name: "已有景区")])
        ])

        viewModel.selectRole(id: 1)
        await viewModel.loadScenicListIfNeeded(api: api)
        viewModel.toggleScenic(id: 1)
        viewModel.toggleScenic(id: 2)

        XCTAssertTrue(viewModel.scenicOptions.first { $0.id == 1 }?.disabled == true)
        XCTAssertEqual(viewModel.selectedScenicIds, [2])
        XCTAssertTrue(viewModel.canSubmit)
    }

    /// 测试切换角色后清空当前景区选择。
    func testPermissionApplyChangingRoleClearsSelection() async {
        let api = ScenicPermissionServingFake()
        api.scenicList = ScenicListAllResponse(total: 1, list: [ScenicListItem(id: 2, name: "新增景区")])
        let viewModel = PermissionApplyViewModel()
        viewModel.bootstrap(rolePermissions: [
            RolePermissionResponse(role: RoleInfo(id: 1, name: "摄影师"), scenic: []),
            RolePermissionResponse(role: RoleInfo(id: 2, name: "店长"), scenic: [])
        ])
        viewModel.selectRole(id: 1)
        await viewModel.loadScenicListIfNeeded(api: api)
        viewModel.toggleScenic(id: 2)

        viewModel.selectRole(id: 2)

        XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
        XCTAssertTrue(viewModel.scenicOptions.isEmpty)
    }

    /// 测试无角色或无景区时禁止提交。
    func testPermissionApplyRequiresRoleAndScenicBeforeSubmit() {
        let viewModel = PermissionApplyViewModel()

        XCTAssertFalse(viewModel.canSubmit)

        viewModel.bootstrap(rolePermissions: [RolePermissionResponse(role: RoleInfo(id: 1, name: "摄影师"), scenic: [])])
        viewModel.selectRole(id: 1)

        XCTAssertFalse(viewModel.canSubmit)
    }

    /// 测试提交成功和失败会更新状态。
    func testPermissionApplySubmitSuccessAndFailure() async {
        let api = ScenicPermissionServingFake()
        let viewModel = PermissionApplyViewModel()
        viewModel.bootstrap(rolePermissions: [RolePermissionResponse(role: RoleInfo(id: 1, name: "摄影师"), scenic: [])])
        viewModel.selectRole(id: 1)
        viewModel.scenicOptions = [PermissionScenicOption(id: 2, name: "新增景区", selected: false, disabled: false)]
        viewModel.toggleScenic(id: 2)

        await viewModel.submit(api: api)

        XCTAssertEqual(api.submittedRoleId, 1)
        XCTAssertEqual(api.submittedScenicIds, [2])
        XCTAssertEqual(viewModel.message, "提交成功，等待审核")

        api.roleApplySubmitError = TestError.sample
        await viewModel.submit(api: api)

        XCTAssertEqual(viewModel.message, TestError.sample.localizedDescription)
    }

    /// 测试地区加载失败会进入初始化失败态。
    func testScenicApplicationAreaFailureMarksLoadFailed() async {
        let api = ScenicPermissionServingFake()
        api.areasError = TestError.sample
        let viewModel = ScenicApplicationViewModel()

        await viewModel.loadInitial(api: api)

        XCTAssertTrue(viewModel.loadFailed)
        XCTAssertTrue(viewModel.provinces.isEmpty)
    }

    /// 测试待审核或驳回申请能回填表单。
    func testScenicApplicationPendingRefillsForm() async {
        let api = ScenicPermissionServingFake()
        api.areas = [ScenicAreaNode(id: "zj", name: "浙江", children: [ScenicAreaNode(id: "hz", name: "杭州")])]
        api.scenicPendings = ScenicApplicationPendingsResponse(items: [
            ScenicApplicationPendingResponse(
                id: 1,
                code: "A001",
                scenicId: 9,
                scenicName: "新景区",
                scenicImages: "https://a.com/1.jpg,https://a.com/2.jpg",
                scenicProvince: "浙江",
                scenicCity: "杭州",
                coopType: 3,
                remark: "备注",
                status: 3
            )
        ])
        let viewModel = ScenicApplicationViewModel()

        await viewModel.loadInitial(api: api)

        XCTAssertEqual(viewModel.scenicName, "新景区")
        XCTAssertEqual(viewModel.remoteImageURLs.count, 2)
        XCTAssertEqual(viewModel.selectedProvince, "浙江")
        XCTAssertEqual(viewModel.selectedCity, "杭州")
        XCTAssertEqual(viewModel.coopType, 3)
        XCTAssertFalse(viewModel.isReadOnly)
    }

    /// 测试景区申请必填项和协议校验。
    func testScenicApplicationValidation() {
        let viewModel = ScenicApplicationViewModel()

        XCTAssertEqual(viewModel.validationMessage, "请输入景区名称")

        viewModel.scenicName = "新景区"
        XCTAssertEqual(viewModel.validationMessage, "请至少上传一张景区图片")

        viewModel.imageURLs = "https://a.com/1.jpg"
        XCTAssertEqual(viewModel.validationMessage, "请选择省份")

        viewModel.selectedProvince = "浙江"
        XCTAssertEqual(viewModel.validationMessage, "请选择城市")

        viewModel.selectedCity = "杭州"
        XCTAssertEqual(viewModel.validationMessage, "请先阅读并同意用户须知与隐私政策")

        viewModel.agreed = true
        XCTAssertNil(viewModel.validationMessage)
    }

    /// 测试本地图片会先上传 OSS，再提交最终 URL。
    func testScenicApplicationUploadsBeforeSubmit() async {
        let api = ScenicPermissionServingFake()
        let uploader = OSSUploadServingFake()
        uploader.uploadedURLs = ["https://oss.com/scenic.jpg"]
        let viewModel = ScenicApplicationViewModel()
        viewModel.scenicName = "新景区"
        viewModel.selectedProvince = "浙江"
        viewModel.selectedCity = "杭州"
        viewModel.agreed = true
        viewModel.addLocalImage(data: Data([1, 2, 3]), fileName: "local.jpg")

        await viewModel.submit(api: api, uploader: uploader)

        XCTAssertEqual(uploader.uploadedFileNames, ["local.jpg"])
        XCTAssertEqual(api.submittedScenicRequest?.scenicImages, ["https://oss.com/scenic.jpg"])
        XCTAssertEqual(api.uploadPlaceholders.first?.fileName, "local.jpg")
        XCTAssertEqual(viewModel.message, "提交成功")
    }

    /// 测试图片上传失败时不提交表单。
    func testScenicApplicationUploadFailureDoesNotSubmit() async {
        let api = ScenicPermissionServingFake()
        let uploader = OSSUploadServingFake()
        uploader.uploadError = TestError.sample
        let viewModel = ScenicApplicationViewModel()
        viewModel.scenicName = "新景区"
        viewModel.selectedProvince = "浙江"
        viewModel.selectedCity = "杭州"
        viewModel.agreed = true
        viewModel.addLocalImage(data: Data([1]), fileName: "local.jpg")

        await viewModel.submit(api: api, uploader: uploader)

        XCTAssertNil(api.submittedScenicRequest)
        XCTAssertEqual(viewModel.message, TestError.sample.localizedDescription)
    }
}

@MainActor
/// 景区权限服务测试替身，用于验证 ViewModel 请求行为。
private final class ScenicPermissionServingFake: ScenicPermissionServing {
    var scenicList = ScenicListAllResponse(total: 0, list: [])
    var scenicListError: Error?
    var areas: [ScenicAreaNode] = []
    var areasError: Error?
    var scenicPendings = ScenicApplicationPendingsResponse()
    var scenicPendingsError: Error?
    var submittedScenicRequest: ScenicApplicationSubmitRequest?
    var uploadPlaceholders: [ScenicApplicationUploadPlaceholder] = []
    var roleApplies: [RoleApplyPendingResponse] = []
    var submittedRoleId: Int?
    var submittedScenicIds: [Int] = []
    var roleApplySubmitError: Error?

    /// 返回景区列表或抛出测试错误。
    func scenicListAll() async throws -> ScenicListAllResponse {
        if let scenicListError { throw scenicListError }
        return scenicList
    }

    /// 返回地区树或抛出测试错误。
    func areas() async throws -> [ScenicAreaNode] {
        if let areasError { throw areasError }
        return areas
    }

    /// 返回景区申请记录或抛出测试错误。
    func scenicApplicationPendingAll() async throws -> ScenicApplicationPendingsResponse {
        if let scenicPendingsError { throw scenicPendingsError }
        return scenicPendings
    }

    /// 记录景区申请提交请求。
    func scenicSubmit(_ request: ScenicApplicationSubmitRequest) async throws {
        submittedScenicRequest = request
    }

    /// 记录景区申请图片占位信息。
    func scenicApplicationUploadPlaceholder(_ items: [ScenicApplicationUploadPlaceholder]) async throws {
        uploadPlaceholders = items
    }

    /// 返回角色权限申请记录。
    func roleApplyAll() async throws -> [RoleApplyPendingResponse] {
        roleApplies
    }

    /// 记录角色权限申请提交请求。
    func roleApplySubmit(roleId: Int, scenicIds: [Int]) async throws {
        if let roleApplySubmitError { throw roleApplySubmitError }
        submittedRoleId = roleId
        submittedScenicIds = scenicIds
    }
}

@MainActor
/// OSS 上传测试替身，用于验证景区申请图片上传顺序。
private final class OSSUploadServingFake: OSSUploadServing {
    var uploadedURLs: [String] = []
    var uploadedFileNames: [String] = []
    var uploadError: Error?

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

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

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

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

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

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

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

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

    /// 统一记录上传请求并返回预设 URL。
    private func upload(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if let uploadError { throw uploadError }
        uploadedFileNames.append(fileName)
        onProgress(100)
        return uploadedURLs.removeFirst()
    }
}

/// 测试错误实体。
private enum TestError: LocalizedError {
    case sample

    var errorDescription: String? {
        "测试错误"
    }
}
