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

import UIKit
import XCTest
@testable import suixinkan

@MainActor
/// 个人中心二级页面测试，覆盖设置、账号切换和实名认证基础逻辑。
final class ProfileSecondaryPagesTests: XCTestCase {
    /// 测试设置页版本号展示规则。
    func testSettingsVersionTextUsesBuildNumberWhenNeeded() {
        XCTAssertEqual(
            SettingsDisplayPolicy.versionText(infoDictionary: [
                "CFBundleShortVersionString": "1.2",
                "CFBundleVersion": "45"
            ]),
            "1.2.45"
        )
        XCTAssertEqual(
            SettingsDisplayPolicy.versionText(infoDictionary: [
                "CFBundleShortVersionString": "1.2.3",
                "CFBundleVersion": "45"
            ]),
            "1.2.3"
        )
        XCTAssertEqual(SettingsDisplayPolicy.versionText(infoDictionary: nil), "1.0.0")
    }

    /// 测试协议页面映射到预期 H5 路径。
    func testAgreementPagesUseExpectedPaths() {
        let pages: [(AgreementPage, String, String)] = [
            (.about, "关于我们", "/h5/app/about-us"),
            (.userAgreement, "用户协议", "/h5/app/user-agreement"),
            (.privacyPolicy, "隐私政策", "/h5/app/privacy-policy"),
            (.walletUserNotice, "钱包用户须知", "/h5/app/wallet-user-notice"),
            (.walletPrivacy, "钱包隐私政策", "/h5/app/wallet-privacy")
        ]

        for (page, title, path) in pages {
            XCTAssertEqual(page.title, title)
            XCTAssertEqual(page.id, title)
            XCTAssertEqual(page.url.path, path)
        }
    }

    /// 测试账号切换列表接口使用旧工程一致的 v9 accounts 路径。
    func testSwitchableAccountsUsesExpectedEndpoint() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "v9_login_multi_success")
        ])
        let api = ProfileAPI(client: APIClient(session: session))

        let response = try await api.switchableAccounts()

        XCTAssertEqual(response.accounts.count, 2)
        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "GET")
        XCTAssertEqual(request.url?.path, "/api/app/v9/accounts")
    }

    /// 测试账号切换 ViewModel 能加载账号并默认选中第一项。
    func testAccountSwitchViewModelLoadsAccounts() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "v9_login_multi_success")
        ])
        let api = ProfileAPI(client: APIClient(session: session))
        let viewModel = AccountSwitchViewModel()

        try await viewModel.load(api: api)

        XCTAssertEqual(viewModel.accounts.map(\.businessUserId), [101, 201])
        XCTAssertEqual(viewModel.selectedAccount?.businessUserId, 101)
        XCTAssertFalse(viewModel.loading)
    }

    /// 测试账号切换提交使用 set-user 接口和账号对应请求体。
    func testAccountSwitchViewModelSubmitsSetUser() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "v9_set_store_user_success")
        ])
        let api = AuthAPI(client: APIClient(session: session))
        let account = AccountSwitchAccount(
            accountType: V9StoreUser.accountTypeValue,
            businessUserId: 201,
            title: "示例门店",
            subtitle: "示例景区",
            phone: "13800138000",
            avatar: "",
            scenicName: "示例景区",
            storeId: 20,
            storeName: "示例门店",
            scenicId: 10,
            isCurrent: false
        )
        let viewModel = AccountSwitchViewModel()

        let response = try await viewModel.switchAccount(account, api: api)

        XCTAssertFalse(response.token.isEmpty)
        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "POST")
        XCTAssertEqual(request.url?.path, "/api/app/v9/set-user")
        let json = try XCTUnwrap(JSONSerialization.jsonObject(with: XCTUnwrap(request.httpBody)) as? [String: Any])
        XCTAssertEqual(json["store_user_id"] as? Int, 201)
        XCTAssertNil(json["ss_user_id"] as? Int)
    }

    /// 测试实名认证信息解码兼容字符串数字字段。
    func testRealNameInfoDecodesLossyFields() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "real_name_info_success")
        ])
        let api = ProfileAPI(client: APIClient(session: session))

        let response = try await api.realNameInfo()
        let info = try XCTUnwrap(response.realNameInfo)

        XCTAssertEqual(info.realName, "测试摄影师")
        XCTAssertEqual(info.idCardNo, "320100********0012")
        XCTAssertTrue(info.verified)
        XCTAssertFalse(info.isLongValid)
        XCTAssertEqual(info.auditStatus, 2)
        XCTAssertEqual(info.auditorId, 7)
        XCTAssertEqual(info.auditor?.name, "审核员")
        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/real-name/info")
    }

    /// 测试实名认证短信和提交接口路径及请求体。
    func testRealNameSmsAndSubmitUseExpectedEndpoints() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "empty_success"),
            try TestFixture.data(named: "empty_success")
        ])
        let api = ProfileAPI(client: APIClient(session: session))

        try await api.realNameSmsVerifyCode()
        try await api.realNameSubmit(
            RealNameAuthRequest(
                realName: "测试摄影师",
                idCardNo: "11010519491231002X",
                smsVerifyCode: "123456",
                startDate: "2026-01-01",
                endDate: "2046-01-01",
                isLongValid: 0,
                frontUrl: "https://cdn.example.com/front.jpg",
                backUrl: "https://cdn.example.com/back.jpg"
            )
        )

        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/yf-handset-app/photog/real-name/sms-verify-code",
            "/api/yf-handset-app/photog/real-name/submit"
        ])
        let json = try XCTUnwrap(JSONSerialization.jsonObject(with: XCTUnwrap(session.requests.last?.httpBody)) as? [String: Any])
        XCTAssertEqual(json["real_name"] as? String, "测试摄影师")
        XCTAssertEqual(json["id_card_no"] as? String, "11010519491231002X")
        XCTAssertEqual(json["sms_verify_code"] as? String, "123456")
        XCTAssertEqual(json["is_long_valid"] as? Int, 0)
    }

    /// 测试身份证号校验和格式归一化。
    func testRealNameIDCardValidation() {
        XCTAssertTrue(RealNameAuthViewModel.isValidMainlandIDCardNumber("11010519491231002x"))
        XCTAssertEqual(RealNameAuthViewModel.normalizedIDCardNumber(" 11010519491231002x "), "11010519491231002X")
        XCTAssertFalse(RealNameAuthViewModel.isValidMainlandIDCardNumber("110105194912310021"))
        XCTAssertFalse(RealNameAuthViewModel.isValidMainlandIDCardNumber("123"))
    }

    /// 测试头像变更时先上传 OSS，再回写头像 URL。
    func testProfileAvatarUploadUpdatesAvatarURL() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "empty_success")
        ])
        let api = ProfileAPI(client: APIClient(session: session))
        let uploader = MockOSSUploader()
        let viewModel = ProfileViewModel()
        viewModel.userInfo = UserInfoResponse(avatar: "", nickname: "旧昵称")
        viewModel.beginEditing()
        try viewModel.prepareAvatarImage(data: makeJPEGData(), timestamp: 1_767_225_600)

        try await viewModel.saveProfile(api: api, uploader: uploader, scenicId: 10)

        XCTAssertEqual(uploader.userAvatarUploads.count, 1)
        XCTAssertEqual(uploader.userAvatarUploads.first?.scenicId, 10)
        XCTAssertEqual(viewModel.userInfo?.avatar, uploader.avatarURL)
        XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/yf-handset-app/userinfo-update-avatar-url"])
    }

    /// 测试头像未变更时不会调用 OSS 上传。
    func testProfileNicknameOnlySaveDoesNotUploadAvatar() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "empty_success")
        ])
        let api = ProfileAPI(client: APIClient(session: session))
        let uploader = MockOSSUploader()
        let viewModel = ProfileViewModel()
        viewModel.userInfo = UserInfoResponse(avatar: "https://cdn.example.com/old.jpg", nickname: "旧昵称")
        viewModel.beginEditing()
        viewModel.editingNickname = "新昵称"

        try await viewModel.saveProfile(api: api, uploader: uploader, scenicId: 10)

        XCTAssertTrue(uploader.userAvatarUploads.isEmpty)
        XCTAssertEqual(viewModel.userInfo?.nickname, "新昵称")
        XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/yf-handset-app/userinfo-update"])
    }

    /// 测试实名认证提交前会上传正反面图片并提交最终 URL。
    func testRealNameSubmitUploadsImagesBeforeSubmitting() async throws {
        let session = ProfileSecondaryURLSession(responses: [
            try TestFixture.data(named: "empty_success"),
            try TestFixture.data(named: "real_name_info_success")
        ])
        let api = ProfileAPI(client: APIClient(session: session))
        let uploader = MockOSSUploader()
        let viewModel = RealNameAuthViewModel()
        viewModel.realName = "测试摄影师"
        viewModel.idCardNo = "11010519491231002X"
        viewModel.smsCode = "123456"
        try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .front, timestamp: 1_767_225_600)
        try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .back, timestamp: 1_767_225_601)

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

        XCTAssertEqual(uploader.realNameUploads.map(\.fileName), ["real_name_front_1767225600.jpg", "real_name_back_1767225601.jpg"])
        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/yf-handset-app/photog/real-name/submit",
            "/api/yf-handset-app/photog/real-name/info"
        ])
        let body = try XCTUnwrap(session.requests.first?.httpBody)
        let json = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
        XCTAssertEqual(json["front_url"] as? String, uploader.realNameURLs[0])
        XCTAssertEqual(json["back_url"] as? String, uploader.realNameURLs[1])
    }

    /// 测试实名认证缺少图片时阻止提交。
    func testRealNameSubmitRequiresImages() async throws {
        let api = ProfileAPI(client: APIClient(session: ProfileSecondaryURLSession(responses: [])))
        let uploader = MockOSSUploader()
        let viewModel = RealNameAuthViewModel()
        viewModel.realName = "测试摄影师"
        viewModel.idCardNo = "11010519491231002X"
        viewModel.smsCode = "123456"

        do {
            try await viewModel.submit(api: api, uploader: uploader, scenicId: 10)
            XCTFail("缺少证件图片时应阻止提交")
        } catch {
            XCTAssertEqual(error as? RealNameValidationError, .message("请选择身份证人像面图片"))
        }
        XCTAssertTrue(uploader.realNameUploads.isEmpty)
    }

    /// 测试实名认证图片上传失败时不会提交资料。
    func testRealNameSubmitStopsWhenUploadFails() async throws {
        let session = ProfileSecondaryURLSession(responses: [])
        let api = ProfileAPI(client: APIClient(session: session))
        let uploader = MockOSSUploader(error: OSSUploadError.unsupportedFileType)
        let viewModel = RealNameAuthViewModel()
        viewModel.realName = "测试摄影师"
        viewModel.idCardNo = "11010519491231002X"
        viewModel.smsCode = "123456"
        try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .front)
        try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .back)

        do {
            try await viewModel.submit(api: api, uploader: uploader, scenicId: 10)
            XCTFail("上传失败时应阻止提交")
        } catch {
            XCTAssertEqual(error as? OSSUploadError, .unsupportedFileType)
        }

        XCTAssertTrue(session.requests.isEmpty)
        XCTAssertFalse(viewModel.submitting)
    }

    /// 测试首页系统设置 URI 已接入真实设置页。
    func testHomeSystemSettingsRouteResolvesToSettingsPage() {
        XCTAssertEqual(
            HomeMenuRouter.resolve(uri: "system_settings", title: "设置中心"),
            .destination(.settings)
        )
    }
}

/// 个人中心测试用 URLSession 替身，按顺序返回预设响应。
private final class ProfileSecondaryURLSession: URLSessionProtocol {
    private var responses: [Data]
    private(set) var requests: [URLRequest] = []

    /// 初始化顺序响应。
    init(responses: [Data]) {
        self.responses = responses
    }

    /// 记录请求并返回下一份响应。
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        requests.append(request)
        let data = responses.isEmpty ? Data() : responses.removeFirst()
        return (
            data,
            HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
        )
    }
}

/// 个人中心测试用 OSS 上传替身，记录上传请求并返回固定 URL。
private final class MockOSSUploader: OSSUploadServing {
    struct UploadRecord: Equatable {
        let fileName: String
        let scenicId: Int
    }

    let avatarURL = "https://cdn.example.com/avatar-new.jpg"
    let realNameURLs = [
        "https://cdn.example.com/front-new.jpg",
        "https://cdn.example.com/back-new.jpg"
    ]
    private let error: Error?
    private(set) var userAvatarUploads: [UploadRecord] = []
    private(set) var realNameUploads: [UploadRecord] = []

    /// 初始化上传替身，可选指定要抛出的错误。
    init(error: Error? = nil) {
        self.error = error
    }

    /// 模拟上传用户头像。
    func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if let error { throw error }
        userAvatarUploads.append(UploadRecord(fileName: fileName, scenicId: scenicId))
        onProgress(100)
        return avatarURL
    }

    /// 模拟上传实名认证证件图片。
    func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if let error { throw error }
        realNameUploads.append(UploadRecord(fileName: fileName, scenicId: scenicId))
        onProgress(100)
        return realNameURLs[min(realNameUploads.count - 1, realNameURLs.count - 1)]
    }

    /// 模拟上传云盘文件。
    func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        avatarURL
    }

    /// 模拟上传相册文件。
    func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        avatarURL
    }

    /// 模拟上传任务附件。
    func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        avatarURL
    }

    /// 模拟上传打卡点图片。
    func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        avatarURL
    }

    /// 模拟上传景区申请图片。
    func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        avatarURL
    }

    /// 模拟上传银行卡照片。
    func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        avatarURL
    }
}

/// 生成测试用 JPEG 图片数据。
private func makeJPEGData() throws -> Data {
    let renderer = UIGraphicsImageRenderer(size: CGSize(width: 12, height: 12))
    let image = renderer.image { context in
        UIColor.systemBlue.setFill()
        context.fill(CGRect(x: 0, y: 0, width: 12, height: 12))
    }
    return try XCTUnwrap(image.jpegData(compressionQuality: 0.9))
}
