//
//  TravelAlbumViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/29.
//

import XCTest
@testable import suixinkan

@MainActor
/// 旅拍相册 ViewModel 测试。
final class TravelAlbumViewModelTests: XCTestCase {
    /// 测试先拍再买模式缺少单价时创建失败。
    func testCreatePreShootRequiresSinglePrice() async {
        let viewModel = TravelAlbumEntryViewModel()
        let api = MockTravelAlbumAPI()

        let success = await viewModel.confirmCreateTask(
            api: api,
            mode: TravelAlbumEntryViewModel.modePreShoot,
            freeCount: "3",
            singlePrice: "",
            packagePrice: "99",
            order: nil,
            scenicSelected: true
        )

        XCTAssertFalse(success)
        XCTAssertEqual(viewModel.errorMessage, "请输入单张照片价格")
        XCTAssertEqual(api.createCallCount, 0)
    }

    /// 测试买了再拍模式缺少订单时创建失败。
    func testCreatePreOrderRequiresOrder() async {
        let viewModel = TravelAlbumEntryViewModel()
        let api = MockTravelAlbumAPI()

        let success = await viewModel.confirmCreateTask(
            api: api,
            mode: TravelAlbumEntryViewModel.modePreOrder,
            freeCount: "",
            singlePrice: "",
            packagePrice: "",
            order: nil,
            scenicSelected: true
        )

        XCTAssertFalse(success)
        XCTAssertEqual(viewModel.errorMessage, "请选择绑定订单")
    }

    /// 测试创建成功后刷新列表。
    func testCreateSuccessReloadsList() async {
        let viewModel = TravelAlbumEntryViewModel()
        let api = MockTravelAlbumAPI()

        let success = await viewModel.confirmCreateTask(
            api: api,
            mode: TravelAlbumEntryViewModel.modePreShoot,
            freeCount: "3",
            singlePrice: "9.9",
            packagePrice: "99",
            order: nil,
            scenicSelected: true
        )

        XCTAssertTrue(success)
        XCTAssertEqual(api.createCallCount, 1)
        XCTAssertEqual(api.listCallCount, 1)
    }

    /// 测试相册名称按日期递增。
    func testBuildTodayAlbumNameIncrementsSuffix() {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        let today = formatter.string(from: Date())

        let name = TravelAlbumEntryViewModel.buildTodayAlbumName(
            existingNames: ["\(today)-001", "\(today)-002", "2026-06-28-001"]
        )
        XCTAssertTrue(name.hasSuffix("003"))
    }

    /// 测试切换已购 Tab 时携带 is_purchased 参数并重置选择状态。
    func testDetailSelectPurchasedTabUsesPurchasedFilter() async {
        let viewModel = TravelAlbumDetailViewModel()
        let api = MockTravelAlbumDetailAPI()

        await viewModel.refreshAll(api: api, albumID: 8)
        viewModel.isSelectionMode = true
        viewModel.selectedMaterialIDs = [1]

        await viewModel.selectTab(TravelAlbumDetailViewModel.tabPurchased, api: api)

        XCTAssertEqual(viewModel.selectedTab, TravelAlbumDetailViewModel.tabPurchased)
        XCTAssertFalse(viewModel.isSelectionMode)
        XCTAssertTrue(viewModel.selectedMaterialIDs.isEmpty)
        XCTAssertEqual(api.materialListCalls.last?.isPurchased, 1)
    }

    /// 测试排序变更触发重载。
    func testDetailSetOrderByReloadsMaterials() async {
        let viewModel = TravelAlbumDetailViewModel()
        let api = MockTravelAlbumDetailAPI()

        await viewModel.refreshAll(api: api, albumID: 8)
        let initialCount = api.materialListCalls.count

        await viewModel.setOrderBy(3, api: api)

        XCTAssertEqual(viewModel.orderBy, 3)
        XCTAssertGreaterThan(api.materialListCalls.count, initialCount)
        XCTAssertEqual(api.materialListCalls.last?.orderBy, 3)
    }

    /// 测试仅未购买素材可选中。
    func testDetailToggleSelectionRejectsPurchasedMaterial() async {
        let viewModel = TravelAlbumDetailViewModel()
        viewModel.isSelectionMode = true

        let purchased = try! decodeMaterial(id: 2, status: 2)
        viewModel.toggleMaterialSelection(purchased)
        XCTAssertTrue(viewModel.selectedMaterialIDs.isEmpty)
        XCTAssertEqual(viewModel.errorMessage, "仅未购买素材可删除")

        let unpurchased = try! decodeMaterial(id: 3, status: TravelAlbumDetailViewModel.materialStatusUnpurchased)
        viewModel.toggleMaterialSelection(unpurchased)
        XCTAssertEqual(viewModel.selectedMaterialIDs, [3])
    }

    /// 测试批量删除按顺序调用 API。
    func testDetailConfirmDeleteSelectedMaterialsDeletesInOrder() async {
        let viewModel = TravelAlbumDetailViewModel()
        let api = MockTravelAlbumDetailAPI()
        viewModel.selectedMaterialIDs = [11, 12, 13]

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

        XCTAssertTrue(success)
        XCTAssertEqual(Set(api.deletedMaterialIDs), Set([11, 12, 13]))
        XCTAssertFalse(viewModel.isSelectionMode)
        XCTAssertTrue(viewModel.selectedMaterialIDs.isEmpty)
    }

    /// 测试全部 Tab 下 allPhotoCount 与 total 同步。
    func testDetailAllPhotoCountSyncsWithTotalOnAllTab() async {
        let viewModel = TravelAlbumDetailViewModel()
        let api = MockTravelAlbumDetailAPI(materialTotal: 42)

        await viewModel.refreshAll(api: api, albumID: 8)

        XCTAssertEqual(viewModel.allPhotoCount, 42)
    }
}

private func decodeMaterial(id: Int, status: Int) throws -> TravelAlbumMaterial {
    let json = """
    {"code":100000,"msg":"success","data":{"id":"\(id)","user_equity_travel_id":"8","status":"\(status)","order_number":"","user_id":"100","file_name":"DSC.JPG","file_type":"2","file_url":"https://cdn/a.jpg","file_size":"2048","cover_url":"","is_purchased":"0","created_at":"2026","updated_at":"2026"}}
    """
    let data = Data(json.utf8)
    let envelope = try JSONDecoder().decode(APIEnvelope<TravelAlbumMaterial>.self, from: data)
    return try XCTUnwrap(envelope.data)
}

@MainActor
private final class MockTravelAlbumDetailAPI: TravelAlbumServing {
    struct MaterialListCall {
        let orderBy: Int
        let isPurchased: Int?
        let page: Int
    }

    var materialTotal: Int
    var materialListCalls: [MaterialListCall] = []
    var deletedMaterialIDs: [Int] = []

    init(materialTotal: Int = 5) {
        self.materialTotal = materialTotal
    }

    func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] }
    func createAlbum(_ request: TravelAlbumCreateRequest) async throws -> TravelAlbumCreateResponse {
        TravelAlbumCreateResponse(id: 1)
    }
    func albumList(page: Int, pageSize: Int) async throws -> ListPayload<TravelAlbumItem> {
        ListPayload(total: 0, list: [])
    }
    func albumInfo(id: Int) async throws -> TravelAlbumItem {
        let data = Data(
            #"{"code":100000,"msg":"success","data":{"id":"8","store_user_id":"1","name":"2026-06-30-001","type":"1","order_number":"","material_num":"3","material_price":"990","material_package_price":"9900","photo_price":"0","cover_url":"","user_id":"100","status":"1","created_at":"2026-06-30","updated_at":"2026-06-30","user":{"id":"200","phone":"13800138000"}}}"#.utf8
        )
        let envelope = try JSONDecoder().decode(APIEnvelope<TravelAlbumItem>.self, from: data)
        return try XCTUnwrap(envelope.data)
    }
    func editAlbum(_ request: TravelAlbumEditRequest) async throws {}
    func deleteAlbum(_ request: TravelAlbumDeleteRequest) async throws {}
    func materialList(
        userEquityTravelId: Int,
        page: Int,
        pageSize: Int,
        orderBy: Int,
        isPurchased: Int?
    ) async throws -> ListPayload<TravelAlbumMaterial> {
        materialListCalls.append(MaterialListCall(orderBy: orderBy, isPurchased: isPurchased, page: page))
        return ListPayload(total: materialTotal, list: [])
    }
    func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial {
        throw APIError.emptyData
    }
    func deleteMaterial(_ request: TravelAlbumDeleteMaterialRequest) async throws {
        deletedMaterialIDs.append(request.id)
    }
    func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse {
        TravelAlbumMpCodeResponse(mpCodeOssURL: "")
    }
}

@MainActor
private final class MockTravelAlbumAPI: TravelAlbumServing {
    var createCallCount = 0
    var listCallCount = 0

    func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] }

    func createAlbum(_ request: TravelAlbumCreateRequest) async throws -> TravelAlbumCreateResponse {
        createCallCount += 1
        return TravelAlbumCreateResponse(id: 9)
    }

    func albumList(page: Int, pageSize: Int) async throws -> ListPayload<TravelAlbumItem> {
        listCallCount += 1
        return ListPayload(total: 0, list: [])
    }

    func albumInfo(id: Int) async throws -> TravelAlbumItem {
        throw APIError.emptyData
    }

    func editAlbum(_ request: TravelAlbumEditRequest) async throws {}

    func deleteAlbum(_ request: TravelAlbumDeleteRequest) async throws {}

    func materialList(
        userEquityTravelId: Int,
        page: Int,
        pageSize: Int,
        orderBy: Int,
        isPurchased: Int?
    ) async throws -> ListPayload<TravelAlbumMaterial> {
        ListPayload(total: 0, list: [])
    }

    func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial {
        throw APIError.emptyData
    }

    func deleteMaterial(_ request: TravelAlbumDeleteMaterialRequest) async throws {}

    func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse {
        TravelAlbumMpCodeResponse(mpCodeOssURL: "")
    }
}
