//
//  ProjectViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/24.
//

import XCTest
@testable import suixinkan

@MainActor
/// 项目 ViewModel 测试，覆盖分页、校验和上传提交顺序。
final class ProjectViewModelTests: XCTestCase {
    /// 测试无景区时清空列表且不请求接口。
    func testProjectReloadWithoutScenicClearsListAndSkipsRequest() async {
        let api = FakeProjectService()
        let viewModel = ProjectManagementViewModel()

        await viewModel.reload(api: api, scenicId: nil)

        XCTAssertTrue(viewModel.items.isEmpty)
        XCTAssertEqual(api.projectListCalls.count, 0)
    }

    /// 测试项目列表首屏和加载更多分页。
    func testProjectListReloadAndLoadMore() async {
        let api = FakeProjectService()
        api.projectPages = [
            ListPayload(total: 2, list: [.fixture(id: 1, name: "A")]),
            ListPayload(total: 2, list: [.fixture(id: 2, name: "B")])
        ]
        let viewModel = ProjectManagementViewModel()

        await viewModel.reload(api: api, scenicId: 9)
        await viewModel.loadMore(api: api, scenicId: 9)

        XCTAssertEqual(api.projectListCalls.map(\.page), [1, 2])
        XCTAssertEqual(viewModel.items.map(\.id).sorted(), [1, 2])
        XCTAssertFalse(viewModel.hasMore)
    }

    /// 测试摄影师项目提交会先上传图片再调用创建接口。
    func testProjectEditorUploadsImagesBeforeCreate() async {
        let api = FakeProjectService()
        let uploader = FakeProjectUploader()
        let viewModel = ProjectEditorViewModel(mode: .create)
        viewModel.name = "新项目"
        viewModel.descriptionText = "描述"
        viewModel.price = "99"
        viewModel.otPrice = "199"
        viewModel.deposit = "10"
        viewModel.selectedSpotIds = [6]
        viewModel.coverImage = ProjectLocalImage(data: Data([1]), fileName: "cover.jpg", previewURL: nil)
        viewModel.carouselImages = [ProjectLocalImage(data: Data([2]), fileName: "banner.jpg", previewURL: nil)]

        let success = await viewModel.submit(scenicId: 9, userId: 100, api: api, uploadService: uploader)

        XCTAssertTrue(success)
        XCTAssertEqual(uploader.uploadedFileNames, ["cover.jpg", "banner.jpg"])
        XCTAssertEqual(api.createdProjects.first?.coverProject, "https://cdn/cover.jpg")
        XCTAssertEqual(api.createdProjects.first?.coverCarousel, ["https://cdn/banner.jpg"])
    }

    /// 测试上传失败时不提交业务接口。
    func testProjectEditorUploadFailureDoesNotCreateProject() async {
        let api = FakeProjectService()
        let uploader = FakeProjectUploader()
        uploader.shouldFail = true
        let viewModel = ProjectEditorViewModel(mode: .create)
        viewModel.name = "新项目"
        viewModel.descriptionText = "描述"
        viewModel.price = "99"
        viewModel.selectedSpotIds = [6]
        viewModel.coverImage = ProjectLocalImage(data: Data([1]), fileName: "cover.jpg", previewURL: nil)

        let success = await viewModel.submit(scenicId: 9, userId: 100, api: api, uploadService: uploader)

        XCTAssertFalse(success)
        XCTAssertTrue(api.createdProjects.isEmpty)
    }

    /// 测试店铺项目缺用户 ID 时不提交。
    func testStoreProjectEditorRequiresUserId() async {
        let viewModel = StoreProjectEditorViewModel(mode: .create)
        let success = await viewModel.submit(userId: 0, api: FakeProjectService(), uploadService: FakeProjectUploader())

        XCTAssertFalse(success)
        XCTAssertEqual(viewModel.errorMessage, ProjectEditorError.missingUser.localizedDescription)
    }
}

private extension PhotographerProjectItem {
    /// 创建测试项目列表项。
    static func fixture(id: Int, name: String) -> PhotographerProjectItem {
        PhotographerProjectItem(id: id, name: name, price: "99.00")
    }
}

/// 项目测试服务替身。
@MainActor
private final class FakeProjectService: ProjectServing {
    var projectPages: [ListPayload<PhotographerProjectItem>] = []
    var projectListCalls: [(scenicId: Int, name: String?, page: Int, pageSize: Int)] = []
    var createdProjects: [ProjectCreateRequest] = []

    /// 获取摄影师项目列表。
    func projectList(scenicId: Int, name: String?, page: Int, pageSize: Int) async throws -> ListPayload<PhotographerProjectItem> {
        projectListCalls.append((scenicId, name, page, pageSize))
        return projectPages.isEmpty ? ListPayload(total: 0, list: []) : projectPages.removeFirst()
    }

    /// 获取摄影师项目详情。
    func projectDetail(id: Int) async throws -> PhotographerProjectDetailResponse {
        try Self.detail()
    }

    /// 创建摄影师项目。
    func createProject(_ request: ProjectCreateRequest) async throws {
        createdProjects.append(request)
    }

    /// 编辑摄影师项目。
    func editProject(_ request: ProjectCreateRequest) async throws {}

    /// 删除摄影师项目。
    func deleteProject(id: Int) async throws {}

    /// 获取店铺项目可管理景区。
    func storeManagerScenicList(userId: String) async throws -> ListPayload<StoreManagerScenicItem> {
        ListPayload(total: 0, list: [])
    }

    /// 获取店铺项目列表。
    func storeManagerProjectList(userId: String?, page: Int, pageSize: Int) async throws -> ListPayload<PhotographerProjectItem> {
        ListPayload(total: 0, list: [])
    }

    /// 获取店铺项目详情。
    func storeManagerProjectDetail(id: Int) async throws -> PhotographerProjectDetailResponse {
        try Self.detail()
    }

    /// 创建店铺多点位项目。
    func storeManagerCreate(_ request: StoreManagerCreateRequest) async throws {}

    /// 创建店铺押金项目。
    func storeManagerOfflineCreate(_ request: StoreManagerOfflineCreateRequest) async throws {}

    /// 更新店铺多点位项目。
    func storeManagerUpdate(_ request: StoreManagerUpdateRequest) async throws {}

    /// 更新店铺押金项目。
    func storeManagerOfflineUpdate(_ request: StoreManagerOfflineUpdateRequest) async throws {}

    /// 删除店铺项目。
    func storeManagerDeleteProject(id: Int) async throws {}

    /// 获取全部门店。
    func storeAll() async throws -> ListPayload<StoreItem> {
        ListPayload(total: 0, list: [])
    }

    private static func detail() throws -> PhotographerProjectDetailResponse {
        let data = Data(#"{"id":1,"name":"项目","type":11,"status":1,"price":"99","description":"描述","material_num":1,"photo_num":1,"video_num":1}"#.utf8)
        return try JSONDecoder().decode(PhotographerProjectDetailResponse.self, from: data)
    }
}

/// 项目上传测试替身。
@MainActor
private final class FakeProjectUploader: OSSUploadServing {
    var uploadedFileNames: [String] = []
    var shouldFail = false

    /// 上传项目图片。
    func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if shouldFail { throw TestUploadError.failed }
        uploadedFileNames.append(fileName)
        return "https://cdn/\(fileName)"
    }

    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 uploadTaskFile(data: Data, fileName: String, fileType: Int, 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 enum TestUploadError: Error {
    case failed
}
