//
//  WalletFeatureTests.swift
//  suixinkanTests
//

import XCTest
import UIKit
@testable import suixinkan

/// 钱包功能模型、API 和 ViewModel 测试。
@MainActor
final class WalletFeatureTests: XCTestCase {
    func testWalletModelsDecodeLossyFields() throws {
        let withdrawJSON = Data(#"{"total":"2","item":[{"id":"10","amount":88.5,"created_at":"2026-07-01","settlement_status":"40","withdraw_status":1,"expected_at":"T+1","status_label":"打款中"}]}"#.utf8)
        let withdraw = try JSONDecoder().decode(WalletWithdrawListResponse.self, from: withdrawJSON)

        XCTAssertEqual(withdraw.total, 2)
        XCTAssertEqual(withdraw.item[0].id, 10)
        XCTAssertEqual(withdraw.item[0].amount, "88.5")
        XCTAssertEqual(withdraw.item[0].settlementStatus, 40)

        let earningJSON = Data(#"{"total_amount":12.3,"total_points":"7","total":"1","list":[{"date":"2026-07-01","day_amount":"12.30","day_points":"7","items":[{"id":"1","amount":"12.30","points":"7","type_label":"订单收益","order_number_suffix":"001","created_at":"10:00","withdraw_label":"可提现","source":"wallet"}]}]}"#.utf8)
        let earning = try JSONDecoder().decode(EarningDetailResponse.self, from: earningJSON)

        XCTAssertEqual(earning.totalAmount, "12.3")
        XCTAssertEqual(earning.totalPoints, 7)
        XCTAssertEqual(earning.list[0].items[0].points, 7)

        let pointJSON = Data(#"{"total":"1","list":[{"id":"2","points":"24","amount":"2.0","status":"2","payment_time":"2026-07-02","created_at":"2026-07-01"}]}"#.utf8)
        let points = try JSONDecoder().decode(PointWithdrawListResponse.self, from: pointJSON)

        XCTAssertEqual(points.total, 1)
        XCTAssertEqual(points.list[0].points, 24)
        XCTAssertEqual(points.list[0].amount, 2.0)
    }

    func testWalletAPIUsesExpectedRequests() async throws {
        let session = MockURLSession(responses: [
            Self.envelope(#"{"amount_total":"100.00","amount_current_balance":"80.00","amount_withdrawable":"50.00"}"#),
            Self.envelope(#"{"total":0,"list":[]}"#),
            Self.envelope(#"{"total":0,"item":[]}"#),
            Self.envelope(#"{"total_amount":"0.00","total_points":0,"total":0,"list":[]}"#),
            Self.envelope(#"{"amount_withdrawable":"50.00","min_withdraw_amount":"1","max_single_withdraw_amount":"100","max_daily_withdraw_amount":"200","user_phone":"13800138000","bank_card":{"real_name":"张三","bank_name":"招商银行","card_number":"622200001234"},"settlement":"T+1","withdraw_info":["说明"]}"#),
            Self.envelope(#"{}"#),
            Self.envelope(#"{}"#),
            Self.envelope(#"{"total_points":100,"available_points":80,"withdrawn_points":60,"pending_points":20,"time":"2026"}"#),
            Self.envelope(#"{}"#),
            Self.envelope(#"{"total":0,"list":[]}"#),
        ])
        let api = WalletAPI(client: APIClient(environment: .testing, session: session))

        _ = try await api.walletSummary(type: 0)
        _ = try await api.walletTransactionList(startDate: "2026-07-01", endDate: "2026-07-09", page: 0, pageSize: 0, type: 2)
        _ = try await api.walletWithdrawList(page: 0, pageSize: 0)
        _ = try await api.earningDetail(startDate: "2026-07-01", endDate: "2026-07-09", page: 2, pageSize: 10)
        _ = try await api.withdrawInfo()
        try await api.withdrawSendSMS()
        try await api.withdrawApply(amount: "12.30", smsCode: "123456")
        _ = try await api.pointOverview(staffId: 9)
        try await api.pointWithdrawApply(points: 24, remark: "积分提现申请")
        _ = try await api.pointWithdrawList(status: nil, page: 1, pageSize: 10)

        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/yf-handset-app/photog/wallet/summary",
            "/api/yf-handset-app/photog/wallet/transaction-list",
            "/api/yf-handset-app/photog/wallet/withdraw-list",
            "/api/yf-handset-app/photog/wallet/earning-detail",
            "/api/yf-handset-app/photog/wallet/withdraw-info",
            "/api/yf-handset-app/photog/wallet/withdraw-send-sms",
            "/api/yf-handset-app/photog/wallet/withdraw-apply",
            "/api/yf-handset-app/photog/point/overview",
            "/api/yf-handset-app/photog/point/withdraw/apply",
            "/api/yf-handset-app/photog/point/withdraw/list",
        ])
        XCTAssertEqual(session.requests.map(\.httpMethod), ["GET", "GET", "GET", "GET", "GET", "POST", "POST", "POST", "POST", "POST"])
        XCTAssertEqual(Self.queryItems(from: session.requests[0])["type"], "0")
        XCTAssertEqual(Self.queryItems(from: session.requests[1]), [
            "start_date": "2026-07-01", "end_date": "2026-07-09", "page": "1", "page_size": "1", "type": "2",
        ])
        XCTAssertEqual(Self.queryItems(from: session.requests[2]), ["page": "1", "page_size": "1"])
        XCTAssertEqual(Self.queryItems(from: session.requests[3]), [
            "start_date": "2026-07-01", "end_date": "2026-07-09", "page": "2", "page_size": "10",
        ])
        XCTAssertEqual(Self.queryItems(from: session.requests[7])["staff_id"], "9")
        XCTAssertNil(session.requests[5].httpBody)
        XCTAssertNil(session.requests[7].httpBody)
        XCTAssertEqual(Self.bodyString(from: session.requests[6]).contains(#""sms_code":"123456""#), true)
        XCTAssertEqual(Self.bodyString(from: session.requests[8]).contains(#""points":24"#), true)
        XCTAssertEqual(Self.bodyString(from: session.requests[9]).contains(#""page_size":10"#), true)
    }

    func testWalletViewModelLoadsAndPaginates() async {
        let api = FakeWalletPageService()
        let viewModel = WalletViewModel(staffIdProvider: { 9 })

        await viewModel.loadInitial(api: api)

        XCTAssertEqual(viewModel.summary?.amountWithdrawable, "50.00")
        XCTAssertEqual(viewModel.availablePoints, 80)
        XCTAssertEqual(viewModel.withdrawRecords.count, 1)
        XCTAssertEqual(viewModel.selectedTab, .withdraw)

        await viewModel.selectTab(.transaction, api: api)
        XCTAssertEqual(viewModel.transactionEntries.count, 2)
        XCTAssertEqual(viewModel.totalIncomeLabel, "¥ 12.30")

        await viewModel.selectFilter(.last30Days, api: api)
        XCTAssertEqual(viewModel.filter, .last30Days)
        XCTAssertGreaterThanOrEqual(api.earningQueries.count, 2)
    }

    func testWalletFiltersUseAndroidDateOffsets() async {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = TimeZone(secondsFromGMT: 0)!
        let now = calendar.date(from: DateComponents(year: 2026, month: 7, day: 13, hour: 12))!
        let api = FakeWalletPageService()
        let viewModel = WalletViewModel(
            staffIdProvider: { 9 },
            calendar: calendar,
            nowProvider: { now }
        )

        await viewModel.refreshTransaction(api: api)
        await viewModel.selectFilter(.last30Days, api: api)
        await viewModel.selectFilter(.last180Days, api: api)

        XCTAssertEqual(api.earningQueries.map(\.start), ["2026-07-06", "2026-06-13", "2026-01-14"])
        XCTAssertEqual(api.earningQueries.map(\.end), ["2026-07-13", "2026-07-13", "2026-07-13"])
    }

    func testRefreshFailureKeepsWalletAndPointRecords() async {
        let api = FakeWalletPageService()
        let walletViewModel = WalletViewModel(staffIdProvider: { 9 })
        let pointsViewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })

        await walletViewModel.refreshWithdraw(api: api)
        await walletViewModel.refreshTransaction(api: api)
        await pointsViewModel.refreshWithdrawList(api: api)
        api.shouldFailWithdrawList = true
        api.shouldFailEarningDetail = true
        api.shouldFailPointWithdrawList = true

        await walletViewModel.refreshWithdraw(api: api)
        await walletViewModel.refreshTransaction(api: api)
        await pointsViewModel.refreshWithdrawList(api: api)

        XCTAssertEqual(walletViewModel.withdrawRecords.count, 1)
        XCTAssertEqual(walletViewModel.transactionGroups.count, 1)
        XCTAssertEqual(pointsViewModel.withdrawRecords.count, 1)
    }

    func testCurrentTabRefreshAndPaginationThresholds() async {
        let api = FakeWalletPageService()
        api.pagedRecordTotal = 25
        let walletViewModel = WalletViewModel(staffIdProvider: { 9 })
        let pointsViewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })

        await walletViewModel.refreshSelectedTab(api: api)
        XCTAssertEqual(api.withdrawListPages, [1])
        XCTAssertTrue(api.earningQueries.isEmpty)

        await walletViewModel.loadMoreWithdrawIfNeeded(currentIndex: 7, api: api)
        XCTAssertEqual(api.withdrawListPages, [1])
        await walletViewModel.loadMoreWithdrawIfNeeded(currentIndex: 8, api: api)
        XCTAssertEqual(api.withdrawListPages, [1, 2])

        await walletViewModel.selectTab(.transaction, api: api)
        XCTAssertEqual(api.earningQueries.map(\.page), [1])
        await walletViewModel.refreshSelectedTab(api: api)
        XCTAssertEqual(api.earningQueries.map(\.page), [1, 1])
        await walletViewModel.loadMoreTransactionIfNeeded(currentIndex: 8, api: api)
        XCTAssertEqual(api.earningQueries.map(\.page), [1, 1])
        await walletViewModel.loadMoreTransactionIfNeeded(currentIndex: 9, api: api)
        XCTAssertEqual(api.earningQueries.map(\.page), [1, 1, 2])

        await pointsViewModel.refreshWithdrawList(api: api)
        XCTAssertEqual(api.pointWithdrawListPages, [1])
        await pointsViewModel.loadMoreWithdrawListIfNeeded(currentIndex: 7, api: api)
        XCTAssertEqual(api.pointWithdrawListPages, [1])
        await pointsViewModel.loadMoreWithdrawListIfNeeded(currentIndex: 8, api: api)
        XCTAssertEqual(api.pointWithdrawListPages, [1, 2])
    }

    func testWalletWithdrawDestinationFlow() async throws {
        let viewModel = WalletViewModel()

        var profile = FakeWalletProfileService(realNameStatus: nil, bankCardStatus: nil)
        var destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
        XCTAssertEqual(destination, .realNameAuth)

        profile = FakeWalletProfileService(realNameStatus: 1, bankCardStatus: nil)
        destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
        XCTAssertEqual(destination, .realNamePending)

        profile = FakeWalletProfileService(realNameStatus: 3, bankCardStatus: nil)
        destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
        guard case .realNameAudit = destination else {
            return XCTFail("实名认证驳回应进入审核页")
        }

        profile = FakeWalletProfileService(realNameStatus: 2, bankCardStatus: nil)
        destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
        XCTAssertEqual(destination, .withdrawalSettings)

        profile = FakeWalletProfileService(realNameStatus: 2, bankCardStatus: 1)
        destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
        XCTAssertEqual(destination, .bankCardPending)

        profile = FakeWalletProfileService(realNameStatus: 2, bankCardStatus: 3)
        destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
        guard case .withdrawalAudit = destination else {
            return XCTFail("银行卡驳回应进入审核页")
        }

        profile = FakeWalletProfileService(realNameStatus: 2, bankCardStatus: 2)
        destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
        XCTAssertEqual(destination, .withdraw)
    }

    func testWithdrawViewModelValidatesAndSubmits() async {
        let api = FakeWalletPageService()
        let viewModel = WithdrawViewModel()

        await viewModel.load(api: api)
        viewModel.updateAmount("0012.345")
        viewModel.updateVerificationCode("12a456")

        XCTAssertEqual(viewModel.amount, "12.34")
        XCTAssertEqual(viewModel.verificationCode, "12a456")

        let success = await viewModel.submit(api: api)
        XCTAssertTrue(success)
        XCTAssertEqual(api.withdrawApplyRequests.first?.amount, "12.34")

        viewModel.updateAmount("0")
        XCTAssertNil(viewModel.validationMessage)
        viewModel.updateAmount("99")
        XCTAssertEqual(viewModel.validationMessage, "金额不能大于可提现金额")

        await viewModel.requestVerificationCode(api: api)
        XCTAssertEqual(viewModel.countdown, 60)
        viewModel.decrementCountdown()
        XCTAssertEqual(viewModel.countdown, 59)
    }

    func testWithdrawAllAndSubmitFailureKeepsForm() async {
        let api = FakeWalletPageService()
        let viewModel = WithdrawViewModel()
        await viewModel.load(api: api)
        viewModel.withdrawAll()
        viewModel.updateVerificationCode("123456")
        api.shouldFailWithdrawApply = true

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

        XCTAssertFalse(success)
        XCTAssertEqual(viewModel.amount, "50.00")
        XCTAssertEqual(viewModel.verificationCode, "123456")
        XCTAssertEqual(viewModel.errorMessage, "请求失败")
    }

    func testPointsRedemptionViewModelClampsAndRefreshes() async {
        let api = FakeWalletPageService()
        let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })

        await viewModel.loadInitial(api: api)
        viewModel.updatePointsInput("0")
        XCTAssertEqual(viewModel.pointsInput, "0")
        viewModel.updatePointsInput("999")
        XCTAssertEqual(viewModel.pointsInput, "60")

        await viewModel.applyWithdraw(api: api)

        XCTAssertEqual(api.pointApplyRequests, [60])
        XCTAssertEqual(viewModel.pointsInput, "")
        XCTAssertEqual(viewModel.withdrawRecords.count, 1)
    }

    func testPointApplyFailureKeepsInputAndExistingRecords() async {
        let api = FakeWalletPageService()
        let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
        await viewModel.loadInitial(api: api)
        viewModel.updatePointsInput("24")
        api.shouldFailPointApply = true

        await viewModel.applyWithdraw(api: api)

        XCTAssertEqual(viewModel.pointsInput, "24")
        XCTAssertEqual(viewModel.withdrawRecords.count, 1)
        XCTAssertEqual(viewModel.errorMessage, "请求失败")
    }

    func testWalletTokensAndHomeIconMatchAndroid() {
        XCTAssertEqual(WalletTokens.pageBackground.walletHexRGB, 0xF5F6F8)
        XCTAssertEqual(WalletTokens.withdrawPageBackground.walletHexRGB, 0xF9FAFB)
        XCTAssertEqual(WalletTokens.summaryBackground.walletHexRGB, 0x1677FF)
        XCTAssertEqual(WalletTokens.primary.walletHexRGB, 0x0073FF)
        XCTAssertEqual(WalletTokens.summaryRadius, 24)
        XCTAssertEqual(WalletTokens.contentRadius, 16)
        XCTAssertEqual(WalletTokens.cardRadius, 12)
        XCTAssertEqual(HomeMenuCatalog.item(for: "wallet")?.iconName, "home_menu_wallet")
        XCTAssertNotNil(UIImage(named: "home_menu_wallet"))
    }

    func testWalletAndPointStatusPresentationMatchesAndroid() {
        XCTAssertEqual(
            [10, 20, 30, 40, 50, 60].map { WalletWithdrawStatusDisplay(status: $0).progress },
            [10, 80, 100, 40, 100, 0]
        )
        XCTAssertEqual(
            [10, 20, 30, 40, 50, 60].map { WalletWithdrawStatusDisplay(status: $0).showsTimeline },
            [true, true, true, true, false, true]
        )

        let pending = PointWithdrawStatusDisplay(status: 1)
        XCTAssertEqual(pending.title, "提现中")
        XCTAssertEqual(pending.progress, 40)
        XCTAssertEqual(pending.timeTitle, "预计到账时间：")

        let success = PointWithdrawStatusDisplay(status: 2)
        XCTAssertEqual(success.title, "提现成功")
        XCTAssertEqual(success.progress, 0)
        XCTAssertEqual(success.timeTitle, "到账时间：")

        let failed = PointWithdrawStatusDisplay(status: 3)
        XCTAssertEqual(failed.title, "提现失败")
        XCTAssertEqual(failed.progress, 100)
        XCTAssertEqual(failed.timeTitle, "审核时间：")
    }

    private static func envelope(_ dataJSON: String) -> Data {
        Data(#"{"code":100000,"msg":"success","data":\#(dataJSON)}"#.utf8)
    }

    private static func queryItems(from request: URLRequest) -> [String: String] {
        URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?
            .queryItems?
            .reduce(into: [String: String]()) { result, item in
                result[item.name] = item.value
            } ?? [:]
    }

    private static func bodyString(from request: URLRequest) -> String {
        String(data: request.httpBody ?? Data(), encoding: .utf8) ?? ""
    }

}

@MainActor
/// 钱包页面服务测试替身。
private final class FakeWalletPageService: WalletPageServing {
    private(set) var earningQueries: [(start: String, end: String, page: Int)] = []
    private(set) var withdrawListPages: [Int] = []
    private(set) var pointWithdrawListPages: [Int] = []
    private(set) var withdrawApplyRequests: [(amount: String, smsCode: String)] = []
    private(set) var pointApplyRequests: [Int] = []
    var pagedRecordTotal = 1
    var shouldFailWithdrawList = false
    var shouldFailEarningDetail = false
    var shouldFailPointWithdrawList = false
    var shouldFailWithdrawApply = false
    var shouldFailPointApply = false

    func walletSummary(type: Int) async throws -> WalletSummaryResponse {
        WalletSummaryResponse(amountTotal: "100.00", amountCurrentBalance: "80.00", amountWithdrawable: "50.00")
    }

    func walletTransactionList(
        startDate: String,
        endDate: String,
        page: Int,
        pageSize: Int,
        type: Int
    ) async throws -> WalletTransactionListResponse {
        WalletTransactionListResponse()
    }

    func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse {
        if shouldFailWithdrawList { throw FakeWalletError.requestFailed }
        withdrawListPages.append(page)
        let count = pagedRecordTotal > 1 ? pageSize : 1
        return WalletWithdrawListResponse(
            total: pagedRecordTotal,
            item: (0 ..< count).map {
                WalletWithdrawItem(id: page * 100 + $0, amount: "10.00", createdAt: "2026-07-01", settlementStatus: 40, statusLabel: "打款中")
            }
        )
    }

    func earningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> EarningDetailResponse {
        if shouldFailEarningDetail { throw FakeWalletError.requestFailed }
        earningQueries.append((startDate, endDate, page))
        let count = pagedRecordTotal > 1 ? pageSize : 1
        return EarningDetailResponse(
            totalAmount: "12.30",
            totalPoints: 7,
            total: pagedRecordTotal,
            list: [
                EarningDetailGroup(
                    date: "2026-07-\(String(format: "%02d", page))",
                    dayAmount: "12.30",
                    dayPoints: 7,
                    items: (0 ..< count).map {
                        EarningDetailItem(id: page * 100 + $0, amount: "12.30", points: 7, typeLabel: "订单收益", createdAt: "10:00", withdrawLabel: "可提现")
                    }
                ),
            ]
        )
    }

    func withdrawInfo() async throws -> WithdrawInfoResponse {
        WithdrawInfoResponse(
            amountWithdrawable: "50.00",
            minWithdrawAmount: "1",
            maxSingleWithdrawAmount: "100",
            maxDailyWithdrawAmount: "200",
            userPhone: "13800138000",
            bankCard: WithdrawBankCardInfo(realName: "张三", bankName: "招商银行", cardNumber: "622200001234"),
            settlement: "T+1",
            withdrawInfo: ["说明"]
        )
    }

    func withdrawSendSMS() async throws {}

    func withdrawApply(amount: String, smsCode: String) async throws {
        if shouldFailWithdrawApply { throw FakeWalletError.requestFailed }
        withdrawApplyRequests.append((amount, smsCode))
    }

    func pointOverview(staffId: Int) async throws -> PointOverviewResponse {
        PointOverviewResponse(totalPoints: 100, availablePoints: 80, withdrawnPoints: 60, pendingPoints: 20)
    }

    func pointWithdrawApply(points: Int, remark: String) async throws {
        if shouldFailPointApply { throw FakeWalletError.requestFailed }
        pointApplyRequests.append(points)
    }

    func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse {
        if shouldFailPointWithdrawList { throw FakeWalletError.requestFailed }
        pointWithdrawListPages.append(page)
        let count = pagedRecordTotal > 1 ? pageSize : 1
        return PointWithdrawListResponse(
            total: pagedRecordTotal,
            list: (0 ..< count).map {
                PointWithdrawItem(id: page * 100 + $0, points: 24, amount: 2, status: 1, createdAt: "2026-07-01")
            }
        )
    }
}

/// 钱包测试替身错误。
private enum FakeWalletError: LocalizedError {
    case requestFailed

    var errorDescription: String? { "请求失败" }
}

@MainActor
/// 钱包前置资料服务测试替身。
private struct FakeWalletProfileService: WalletProfileServing {
    let realNameStatus: Int?
    let bankCardStatus: Int?

    func realNameInfo() async throws -> RealNameInfoResponse {
        let data: Data = if let realNameStatus {
            Data(#"{"real_name_info":{"real_name":"张三","id_card_no":"110101199001010010","audit_status":\#(realNameStatus),"is_long_valid":1}}"#.utf8)
        } else {
            Data(#"{"real_name_info":null}"#.utf8)
        }
        return try JSONDecoder().decode(RealNameInfoResponse.self, from: data)
    }

    func bankCardInfo() async throws -> BankCardInfoResponse {
        let data: Data = if let bankCardStatus {
            Data(#"{"bank_card":{"real_name":"张三","bank_name":"招商银行","branch_name":"支行","card_number":"622200001234","audit_status":\#(bankCardStatus),"audit_status_label":"审核中"}}"#.utf8)
        } else {
            Data(#"{"bank_card":null}"#.utf8)
        }
        return try JSONDecoder().decode(BankCardInfoResponse.self, from: data)
    }
}

private extension UIColor {
    var walletHexRGB: UInt {
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return UInt(Int(round(red * 255)) << 16 | Int(round(green * 255)) << 8 | Int(round(blue * 255)))
    }
}
