//
//  WalletViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/22.
//

import XCTest
@testable import suixinkan

@MainActor
/// 钱包模块测试，覆盖首页、提现资格、银行卡上传和积分兑换。
final class WalletViewModelTests: XCTestCase {
    /// 测试钱包首页会加载汇总和收益明细。
    func testWalletInitialLoadFetchesSummaryAndEarnings() async {
        let api = WalletMockAPI()
        api.summary = WalletSummaryResponse(amountTotal: "100", amountCurrentBalance: "80", amountWithdrawable: "60")
        api.earningResponses = [WalletEarningDetailResponse(total: 1, list: [WalletEarningDetailGroup(date: "2026-06-22")])]
        let viewModel = WalletViewModel()

        await viewModel.loadInitial(api: api, staffId: 7)

        XCTAssertEqual(viewModel.withdrawableText, "¥ 60")
        XCTAssertEqual(api.pointOverviewStaffIds, [7])
        XCTAssertEqual(api.earningQueries.count, 1)
        XCTAssertEqual(viewModel.earningsGroups.map(\.date), ["2026-06-22"])
    }

    /// 测试切换到提现记录 Tab 会请求提现分页。
    func testSelectWithdrawTabLoadsWithdrawRecords() async {
        let api = WalletMockAPI()
        let viewModel = WalletViewModel()

        await viewModel.selectTab(.withdraws, api: api)

        XCTAssertEqual(api.withdrawListPages, [1])
    }

    /// 测试提现资格流根据实名认证和银行卡状态进入正确页面。
    func testWithdrawDecisionRoutesByRealNameAndBankCardState() async throws {
        let profile = WalletRealNameMockAPI()
        let wallet = WalletMockAPI()
        let viewModel = WalletViewModel()

        profile.response = RealNameInfoResponse(realNameInfo: nil)
        var decision = await viewModel.resolveWithdrawDecision(profileAPI: profile, walletAPI: wallet)
        XCTAssertEqual(decision, .route(.realNameAuth))

        profile.response = try Self.realNameInfo(status: 2)
        wallet.bankCardResponse = BankCardInfoResponse(bankCard: nil)
        decision = await viewModel.resolveWithdrawDecision(profileAPI: profile, walletAPI: wallet)
        XCTAssertEqual(decision, .route(.bankCardSettings))

        wallet.bankCardResponse = try BankCardInfoResponse(bankCard: Self.bankCard(status: 2))
        decision = await viewModel.resolveWithdrawDecision(profileAPI: profile, walletAPI: wallet)
        XCTAssertEqual(decision, .route(.withdrawApply))
    }

    /// 测试提现申请提交会校验金额并传递规范化金额。
    func testWithdrawApplySubmitsNormalizedAmount() async throws {
        let api = WalletMockAPI()
        api.withdrawInfoResponse = try Self.withdrawInfo(withdrawable: "100")
        let viewModel = WithdrawApplyViewModel()
        await viewModel.load(api: api)
        viewModel.amountText = "12.345"
        viewModel.smsCode = "8899"

        let success = await viewModel.submit(api: api)

        XCTAssertTrue(success)
        XCTAssertEqual(api.withdrawApplyBodies.map(\.amount), ["12.35"])
        XCTAssertEqual(api.withdrawApplyBodies.map(\.smsCode), ["8899"])
    }

    /// 测试银行卡设置会先上传正反面照片，再提交最终 URL。
    func testWithdrawalSettingsUploadsImagesBeforeSubmit() async {
        let api = WalletMockAPI()
        let uploader = WalletUploadMock()
        let viewModel = WithdrawalSettingsViewModel()
        viewModel.realName = "张三"
        viewModel.cardNumber = "622200"
        viewModel.bankName = "中国银行"
        viewModel.branchName = "杭州支行"
        viewModel.provinceCode = "330000"
        viewModel.cityCode = "330100"
        viewModel.smsCode = "1234"
        viewModel.frontImageData = Data([1])
        viewModel.backImageData = Data([2])

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

        XCTAssertTrue(success)
        XCTAssertEqual(uploader.bankUploads.count, 2)
        XCTAssertEqual(api.updateBankRequests.first?.frontUrl, "https://cdn.example.com/bank_card_front.jpg")
        XCTAssertEqual(api.updateBankRequests.first?.backUrl, "https://cdn.example.com/bank_card_back.jpg")
    }

    /// 测试积分兑换不能超过可兑换积分，成功后会刷新列表。
    func testPointsRedemptionValidationAndSubmit() async {
        let api = WalletMockAPI()
        api.pointOverviewResponse = PointOverviewResponse(withdrawnPoints: 100)
        let viewModel = PointsRedemptionViewModel()
        await viewModel.load(api: api, staffId: 7)
        viewModel.pointsText = "120"

        var success = await viewModel.submit(api: api, staffId: 7)
        XCTAssertFalse(success)
        XCTAssertEqual(viewModel.errorMessage, "兑换积分不能超过可兑换积分")

        viewModel.pointsText = "80"
        success = await viewModel.submit(api: api, staffId: 7)
        XCTAssertTrue(success)
        XCTAssertEqual(api.pointApplyBodies.map(\.points), [80])
    }

    /// 从 JSON 创建实名认证响应。
    private static func realNameInfo(status: Int) throws -> RealNameInfoResponse {
        let data = """
        {"real_name_info":{"real_name":"张三","id_card_no":"11010519491231002X","audit_status":\(status),"is_long_valid":1}}
        """.data(using: .utf8)!
        return try JSONDecoder().decode(RealNameInfoResponse.self, from: data)
    }

    /// 从 JSON 创建银行卡信息。
    private static func bankCard(status: Int) throws -> WalletBankCardInfo {
        let data = """
        {"real_name":"张三","bank_name":"中国银行","branch_name":"杭州支行","card_number":"622200","audit_status":\(status),"audit_status_label":"通过"}
        """.data(using: .utf8)!
        return try JSONDecoder().decode(WalletBankCardInfo.self, from: data)
    }

    /// 从 JSON 创建提现信息。
    fileprivate static func withdrawInfo(withdrawable: String) throws -> WithdrawInfoResponse {
        let data = """
        {"amount_withdrawable":"\(withdrawable)","min_withdraw_amount":"1","max_single_withdraw_amount":"1000","max_daily_withdraw_amount":"1000","user_phone":"13800000000","bank_card":{"real_name":"张三","bank_name":"中国银行","card_number":"622200"},"withdraw_info":[]}
        """.data(using: .utf8)!
        return try JSONDecoder().decode(WithdrawInfoResponse.self, from: data)
    }
}

@MainActor
/// 钱包 API 测试替身。
private final class WalletMockAPI: WalletServing {
    var summary = WalletSummaryResponse()
    var earningResponses: [WalletEarningDetailResponse] = []
    var withdrawResponses: [WalletWithdrawListResponse] = []
    var withdrawInfoResponse: WithdrawInfoResponse?
    var bankCardResponse = BankCardInfoResponse(bankCard: nil)
    var pointOverviewResponse = PointOverviewResponse()
    private(set) var earningQueries: [(start: String, end: String, page: Int)] = []
    private(set) var withdrawListPages: [Int] = []
    private(set) var withdrawApplyBodies: [(amount: String, smsCode: String)] = []
    private(set) var updateBankRequests: [UpdateBankInfoRequest] = []
    private(set) var pointOverviewStaffIds: [Int] = []
    private(set) var pointApplyBodies: [(points: Int, remark: String)] = []

    func walletSummary(type: Int) async throws -> WalletSummaryResponse { summary }
    func walletEarningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> WalletEarningDetailResponse {
        earningQueries.append((startDate, endDate, page))
        return earningResponses.isEmpty ? WalletEarningDetailResponse() : earningResponses.removeFirst()
    }
    func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse {
        withdrawListPages.append(page)
        return withdrawResponses.isEmpty ? WalletWithdrawListResponse() : withdrawResponses.removeFirst()
    }
    func withdrawInfo() async throws -> WithdrawInfoResponse {
        if let withdrawInfoResponse { return withdrawInfoResponse }
        return try WalletViewModelTests.withdrawInfo(withdrawable: "0")
    }
    func withdrawSendSms() async throws {}
    func withdrawApply(amount: String, smsCode: String) async throws {
        withdrawApplyBodies.append((amount, smsCode))
    }
    func bankCardInfo() async throws -> BankCardInfoResponse { bankCardResponse }
    func bankList() async throws -> BankListResponse { BankListResponse(banks: ["中国银行"]) }
    func areas() async throws -> [AreaNode] { [] }
    func bankCardVerifyCode() async throws {}
    func updateBankInfo(_ request: UpdateBankInfoRequest) async throws {
        updateBankRequests.append(request)
    }
    func pointOverview(staffId: Int) async throws -> PointOverviewResponse {
        pointOverviewStaffIds.append(staffId)
        return pointOverviewResponse
    }
    func pointWithdrawApply(points: Int, remark: String) async throws {
        pointApplyBodies.append((points, remark))
    }
    func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse {
        PointWithdrawListResponse()
    }
}

@MainActor
/// 钱包实名认证测试替身。
private final class WalletRealNameMockAPI: WalletRealNameServing {
    var response = RealNameInfoResponse(realNameInfo: nil)

    /// 返回预设实名认证响应。
    func realNameInfo() async throws -> RealNameInfoResponse {
        response
    }
}

@MainActor
/// 钱包上传测试替身。
private final class WalletUploadMock: OSSUploadServing {
    private(set) var bankUploads: [String] = []

    func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        bankUploads.append(fileName)
        onProgress(100)
        return "https://cdn.example.com/\(fileName)"
    }

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