//
//  PilotCertificationTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/25.
//

import XCTest
@testable import suixinkan

@MainActor
/// 飞手认证测试，覆盖接口编码、加载、验证码、校验和提交流程。
final class PilotCertificationTests: XCTestCase {
    func testAPIsUseExpectedPathsAndBodies() async throws {
        let session = PilotRecordingURLSession(responses: [
            try TestFixture.data(named: "flyer_detail_success"),
            try TestFixture.data(named: "empty_success"),
            try TestFixture.data(named: "empty_success"),
            try TestFixture.data(named: "empty_success")
        ])
        let api = PilotCertificationAPI(client: APIClient(session: session))

        let detail = try await api.flyerDetail()
        XCTAssertEqual(detail.id, 801)
        XCTAssertEqual(detail.certificateNo, "UAS-2026-001")
        var request = try XCTUnwrap(session.requests.last)
        XCTAssertEqual(request.httpMethod, "GET")
        XCTAssertEqual(request.url?.path, "/api/app/flyer/detail")

        try await api.flyerSendCode(phone: "13800000000")
        request = try XCTUnwrap(session.requests.last)
        XCTAssertEqual(request.url?.path, "/api/app/flyer/send-code")
        XCTAssertEqual(try pilotJSONBody(from: request)["phone"] as? String, "13800000000")

        try await api.flyerApply(applyRequest())
        request = try XCTUnwrap(session.requests.last)
        XCTAssertEqual(request.url?.path, "/api/app/flyer/apply")
        var json = try pilotJSONBody(from: request)
        XCTAssertEqual(json["name"] as? String, "航拍摄影师")
        XCTAssertEqual(json["certificate_no"] as? String, "UAS-2026-001")
        XCTAssertEqual(json["drone_sn"] as? String, "SN20260523001")

        try await api.flyerEdit(editRequest())
        request = try XCTUnwrap(session.requests.last)
        XCTAssertEqual(request.url?.path, "/api/app/flyer/edit")
        json = try pilotJSONBody(from: request)
        XCTAssertEqual(json["id"] as? Int, 801)
        XCTAssertEqual(json["contact_phone"] as? String, "13800000001")
    }

    func testViewModelLoadSendCodeAndCountdown() async throws {
        let api = PilotMock()
        api.detailResponses = [try TestFixture.payload(FlyerDetailResponse.self, named: "flyer_detail_success")]
        let realName = PilotRealNameMock(response: try TestFixture.payload(RealNameInfoResponse.self, named: "real_name_info_success"))
        let viewModel = PilotCertificationViewModel()

        await viewModel.load(api: api, realNameAPI: realName)

        XCTAssertEqual(viewModel.name, "航拍摄影师")
        XCTAssertEqual(viewModel.certType, .caac)
        XCTAssertEqual(viewModel.droneSerialNo, "SN20260523001")
        XCTAssertTrue(viewModel.isRealNameVerified)
        XCTAssertEqual(viewModel.latestAuditLog?.operatorName, "审核员")

        let codeViewModel = PilotCertificationViewModel()
        fillValidForm(codeViewModel)
        try await codeViewModel.sendCode(api: api)
        XCTAssertEqual(api.sendCodePhones, ["13800000000"])
        XCTAssertEqual(codeViewModel.countdown, 60)
        codeViewModel.tickCountdown()
        XCTAssertEqual(codeViewModel.countdown, 59)

        api.sendCodeError = PilotTestError.sample
        codeViewModel.phone = "13800000001"
        while codeViewModel.countdown > 0 { codeViewModel.tickCountdown() }
        await XCTAssertThrowsErrorAsync(try await codeViewModel.sendCode(api: api))
        XCTAssertEqual(codeViewModel.countdown, 0)
    }

    func testValidationCoversRequiredFieldsPhoneImageAndDates() {
        let viewModel = PilotCertificationViewModel()

        XCTAssertEqual(viewModel.validationMessage, "请输入飞手昵称")
        fillValidForm(viewModel)
        viewModel.phone = "123"
        XCTAssertEqual(viewModel.validationMessage, "请输入有效的手机号码")

        fillValidForm(viewModel)
        viewModel.certImageUrl = ""
        XCTAssertEqual(viewModel.validationMessage, "请选择证件图片")

        fillValidForm(viewModel)
        viewModel.endDate = Date(timeInterval: -86_400, since: viewModel.startDate)
        XCTAssertEqual(viewModel.validationMessage, "截至日期不能早于起始日期")
    }

    func testSubmitUploadsAndAppliesThenClearsPendingImage() async throws {
        let api = PilotMock()
        let uploader = PilotUploadMock()
        let viewModel = PilotCertificationViewModel()
        fillValidForm(viewModel)
        viewModel.certImageUrl = ""
        viewModel.prepareCertificateImage(data: Data([1, 2, 3]), fileName: "cert.jpg")
        uploader.url = "https://cdn.example.com/pilot/cert.jpg"

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

        XCTAssertEqual(uploader.uploads.first?.fileName, "cert.jpg")
        XCTAssertEqual(uploader.uploads.first?.scenicId, 88)
        XCTAssertEqual(api.applyRequests.first?.certificateImage, "https://cdn.example.com/pilot/cert.jpg")
        XCTAssertNil(viewModel.pendingCertificateImageData)
    }

    func testRejectedSubmitUsesEditAndFailureKeepsForm() async throws {
        let api = PilotMock()
        api.detailResponses = [
            FlyerDetailResponse(id: 801, name: "旧资料", status: 3, certificateImage: "https://cdn.example.com/old.jpg")
        ]
        let viewModel = PilotCertificationViewModel()
        await viewModel.load(api: api, realNameAPI: PilotRealNameMock())
        fillValidForm(viewModel)

        try await viewModel.submit(api: api, uploader: PilotUploadMock(), scenicId: 88)
        XCTAssertEqual(api.editRequests.first?.id, 801)

        let failingUploader = PilotUploadMock()
        failingUploader.error = PilotTestError.sample
        viewModel.certImageUrl = ""
        viewModel.prepareCertificateImage(data: Data([9]), fileName: "new.jpg")
        await XCTAssertThrowsErrorAsync(try await viewModel.submit(api: api, uploader: failingUploader, scenicId: 88))
        XCTAssertEqual(viewModel.pendingCertificateFileName, "new.jpg")
    }
}

@MainActor
private final class PilotMock: PilotCertificationServing {
    var detailResponses: [FlyerDetailResponse] = []
    var sendCodePhones: [String] = []
    var applyRequests: [FlyerApplyRequest] = []
    var editRequests: [FlyerEditRequest] = []
    var sendCodeError: Error?

    func flyerDetail() async throws -> FlyerDetailResponse {
        detailResponses.isEmpty ? FlyerDetailResponse() : detailResponses.removeFirst()
    }

    func flyerSendCode(phone: String) async throws {
        if let sendCodeError { throw sendCodeError }
        sendCodePhones.append(phone)
    }

    func flyerApply(_ request: FlyerApplyRequest) async throws {
        applyRequests.append(request)
    }

    func flyerEdit(_ request: FlyerEditRequest) async throws {
        editRequests.append(request)
    }
}

private struct PilotRealNameMock: PilotRealNameServing {
    var response = RealNameInfoResponse(realNameInfo: nil)

    func realNameInfo() async throws -> RealNameInfoResponse {
        response
    }
}

@MainActor
private final class PilotUploadMock: OSSUploadServing {
    var url = "https://cdn.example.com/pilot/cert.jpg"
    var error: Error?
    var uploads: [(fileName: String, scenicId: Int)] = []

    func uploadPilotCertificateImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if let error { throw error }
        uploads.append((fileName, scenicId))
        onProgress(100)
        return url
    }

    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 uploadAliveAlbumFile(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 uploadProjectImage(data: Data, fileName: String, 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 { "" }
}

private final class PilotRecordingURLSession: 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 ? try TestFixture.data(named: "empty_success") : responses.removeFirst()
        return (data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
    }
}

private enum PilotTestError: LocalizedError {
    case sample

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

private func fillValidForm(_ viewModel: PilotCertificationViewModel) {
    viewModel.name = "航拍摄影师"
    viewModel.certType = .caac
    viewModel.certNo = "UAS-2026-001"
    viewModel.certImageUrl = "https://cdn.example.com/flyer/cert.jpg"
    viewModel.startDate = Date(timeIntervalSince1970: 1_767_225_600)
    viewModel.endDate = Date(timeIntervalSince1970: 1_830_384_000)
    viewModel.droneModel = "DJI Mini"
    viewModel.droneSerialNo = "SN20260523001"
    viewModel.phone = "13800000000"
    viewModel.verifyCode = "123456"
}

private func applyRequest() -> FlyerApplyRequest {
    FlyerApplyRequest(
        name: "航拍摄影师",
        realnameStatus: 1,
        certificateType: 2,
        certificateNo: "UAS-2026-001",
        certificateStartDate: "2026-01-01",
        certificateEndDate: "2028-01-01",
        certificateImage: "https://cdn.example.com/flyer/cert.jpg",
        droneModel: "DJI Mini",
        droneSn: "SN20260523001",
        contactPhone: "13800000000",
        code: "123456"
    )
}

private func editRequest() -> FlyerEditRequest {
    FlyerEditRequest(
        id: 801,
        name: "航拍摄影师",
        realnameStatus: 1,
        certificateType: 2,
        certificateNo: "UAS-2026-001",
        certificateStartDate: "2026-01-01",
        certificateEndDate: "2028-01-01",
        certificateImage: "https://cdn.example.com/flyer/cert.jpg",
        droneModel: "DJI Mini",
        droneSn: "SN20260523002",
        contactPhone: "13800000001",
        code: "654321"
    )
}

private func pilotJSONBody(from request: URLRequest) throws -> [String: Any] {
    let body = try XCTUnwrap(request.httpBody)
    return try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
}

private func XCTAssertThrowsErrorAsync<T>(
    _ expression: @autoclosure () async throws -> T,
    file: StaticString = #filePath,
    line: UInt = #line
) async {
    do {
        _ = try await expression()
        XCTFail("Expected async expression to throw", file: file, line: line)
    } catch {
        // Expected path.
    }
}
