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

import XCTest
@testable import suixinkan

@MainActor
/// 订单详情 ViewModel 测试，覆盖详情加载、缺失上下文和错误兜底。
final class OrderDetailViewModelTests: XCTestCase {
    /// 测试有门店 ID 时请求详情接口并展示服务端字段。
    func testLoadRequestsDetailWhenStoreIdExists() async throws {
        let item = try firstOrder()
        let api = DetailMockOrderService()
        api.detailResponses = [try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success")]
        let viewModel = OrderDetailViewModel(item: item)

        await viewModel.load(api: api, fallbackStoreId: 66)

        XCTAssertEqual(api.detailCalls.count, 1)
        XCTAssertEqual(api.detailCalls[0].storeId, 66)
        XCTAssertEqual(api.detailCalls[0].orderNumber, "ORDV2001")
        XCTAssertEqual(viewModel.display.projectName, "亲子跟拍")
        XCTAssertEqual(viewModel.projectInfo?.settleSpotNum, 3)
        XCTAssertEqual(viewModel.shootingList.first?.scenicSpotName, "东门")
        XCTAssertNil(viewModel.errorMessage)
    }

    /// 测试缺少门店 ID 时不请求接口并保留列表摘要。
    func testLoadWithoutStoreIdUsesSummaryOnly() async throws {
        let item = try firstOrder()
        let api = DetailMockOrderService()
        let viewModel = OrderDetailViewModel(item: item)

        await viewModel.load(api: api, fallbackStoreId: nil)

        XCTAssertTrue(api.detailCalls.isEmpty)
        XCTAssertEqual(viewModel.display.orderNumber, "ORDV2001")
        XCTAssertEqual(viewModel.contextMessage, "缺少门店上下文，当前展示订单摘要。")
    }

    /// 测试详情接口失败时保留摘要并暴露错误信息。
    func testLoadFailureKeepsSummaryAndExposesError() async throws {
        let item = try firstOrder()
        let api = DetailMockOrderService()
        api.detailError = APIError.httpStatus(500, "server error")
        let viewModel = OrderDetailViewModel(item: item)

        await viewModel.load(api: api, fallbackStoreId: 66)

        XCTAssertEqual(api.detailCalls.count, 1)
        XCTAssertEqual(viewModel.display.orderNumber, "ORDV2001")
        XCTAssertEqual(viewModel.contextMessage, "详情加载失败，当前展示订单摘要。")
        XCTAssertNotNil(viewModel.errorMessage)
    }

    /// 测试详情模型兼容后端数字和字符串混合字段。
    func testDetailResponseDecodesMixedNumericAndStringFields() throws {
        let detail = try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success")

        XCTAssertEqual(detail.orderType, 19)
        XCTAssertEqual(detail.actualRefundAmount, "0")
        XCTAssertEqual(detail.userId, 7001)
        XCTAssertEqual(detail.multiTravel?.projectInfo?.singleSpotVideoNum, 1)
        XCTAssertEqual(detail.multiTravel?.shootingList.first?.startAvg, 4.8)
    }

    /// 返回订单列表 fixture 中的第一条订单。
    private func firstOrder() throws -> OrderEntity {
        let payload = try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")
        return try XCTUnwrap(payload.list.first)
    }
}

@MainActor
/// 订单详情服务测试替身，专门用于详情 ViewModel 测试。
private final class DetailMockOrderService: OrderServing {
    var detailResponses: [StoreOrderDetailResponse] = []
    var detailError: Error?
    private(set) var detailCalls: [(storeId: Int, orderNumber: String)] = []

    /// 订单列表接口在详情测试中不会被调用。
    func orderList(
        scenicId: Int,
        page: Int,
        pageSize: Int,
        orderStatus: Int?,
        userPhone: String?,
        startTime: String?,
        endTime: String?,
        isRefined: Int?,
        isScenicAdmin: Bool
    ) async throws -> ListPayload<OrderEntity> {
        XCTFail("orderList should not be called")
        return ListPayload(total: 0, list: [])
    }

    /// 核销列表接口在详情测试中不会被调用。
    func writeOffList(scenicId: Int, storeId: Int?, page: Int, pageSize: Int) async throws -> ListPayload<WriteOffOrderItem> {
        XCTFail("writeOffList should not be called")
        return ListPayload(total: 0, list: [])
    }

    /// 核销接口在详情测试中不会被调用。
    func writeOff(orderNumber: String) async throws {
        XCTFail("writeOff should not be called")
    }

    /// 返回预置的订单详情响应。
    func storeOrderDetail(storeId: Int, orderNumber: String) async throws -> StoreOrderDetailResponse {
        detailCalls.append((storeId: storeId, orderNumber: orderNumber))
        if let detailError {
            throw detailError
        }
        return detailResponses.isEmpty ? try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success") : detailResponses.removeFirst()
    }
}
