//
//  SampleManagementViewModelTests.swift
//  suixinkanTests
//

import Foundation
import XCTest
@testable import suixinkan

@MainActor
/// 样片管理 ViewModel 测试。
final class SampleManagementViewModelTests: XCTestCase {
    func testAllFilterStatusesMatchAndroidValues() {
        XCTAssertEqual(SampleFilterStatus.all.apiStatus, 4)
        XCTAssertEqual(SampleFilterStatus.approved.apiStatus, 1)
        XCTAssertEqual(SampleFilterStatus.pending.apiStatus, 0)
        XCTAssertEqual(SampleFilterStatus.rejected.apiStatus, 2)
    }

    func testListLoadsRefreshesAndMapsFilterStatus() async {
        let api = MockSampleAPI()
        api.listResponses = [
            SampleMaterialListResponse(total: 12, list: (0 ..< 10).map { makeItem(id: $0) }),
            SampleMaterialListResponse(total: 12, list: [makeItem(id: 10), makeItem(id: 11)]),
            SampleMaterialListResponse(total: 1, list: [makeItem(id: 99)]),
        ]
        let viewModel = SampleListViewModel()

        await viewModel.loadInitial(api: api)
        await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api)
        await viewModel.selectFilter(.pending, api: api)

        XCTAssertEqual(api.listCalls.map(\.status), [4, 4, 0])
        XCTAssertEqual(api.listCalls.map(\.page), [1, 2, 1])
        XCTAssertEqual(viewModel.items.map(\.id), [99])
    }

    func testRejectedSampleCannotToggleListing() async {
        let api = MockSampleAPI()
        api.listResponses = [SampleMaterialListResponse(total: 1, list: [makeItem(id: 1, status: 0, listingStatus: 0)])]
        let viewModel = SampleListViewModel()
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.loadInitial(api: api)
        let resolved = await viewModel.toggleListing(sampleId: 1, checked: true, api: api)

        XCTAssertEqual(messages.last, "审核通过才可以操作上下架")
        XCTAssertTrue(api.operationCalls.isEmpty)
        XCTAssertFalse(resolved)
        XCTAssertFalse(viewModel.isOperating)
    }

    func testApprovedSampleToggleUpdatesLocalItem() async {
        let api = MockSampleAPI()
        api.listResponses = [SampleMaterialListResponse(total: 1, list: [makeItem(id: 2, status: 1, listingStatus: 0)])]
        let viewModel = SampleListViewModel()

        await viewModel.loadInitial(api: api)
        let resolved = await viewModel.toggleListing(sampleId: 2, checked: true, api: api)

        XCTAssertEqual(api.operationCalls.first?.id, 2)
        XCTAssertEqual(api.operationCalls.first?.listingStatus, 1)
        XCTAssertEqual(viewModel.items.first?.listingStatus, 1)
        XCTAssertTrue(resolved)
        XCTAssertFalse(viewModel.isOperating)
    }

    func testApprovedSampleExposesOperatingStateDuringRequest() async {
        let api = MockSampleAPI()
        api.listResponses = [SampleMaterialListResponse(total: 1, list: [makeItem(id: 2, status: 1, listingStatus: 0)])]
        api.operationDelayNanoseconds = 50_000_000
        let viewModel = SampleListViewModel()
        await viewModel.loadInitial(api: api)

        let operation = Task { await viewModel.toggleListing(sampleId: 2, checked: true, api: api) }
        await Task.yield()

        XCTAssertTrue(viewModel.operatingSampleIDs.contains(2))
        XCTAssertTrue(viewModel.isOperating)
        let resolved = await operation.value
        XCTAssertTrue(resolved)
        XCTAssertFalse(viewModel.isOperating)
    }

    func testListingFailureReturnsAuthoritativePreviousState() async {
        let api = MockSampleAPI()
        api.listResponses = [SampleMaterialListResponse(total: 1, list: [makeItem(id: 3, status: 1, listingStatus: 0)])]
        api.operationError = APIError.emptyData
        let viewModel = SampleListViewModel()

        await viewModel.loadInitial(api: api)
        let resolved = await viewModel.toggleListing(sampleId: 3, checked: true, api: api)

        XCTAssertFalse(resolved)
        XCTAssertEqual(viewModel.items.first?.listingStatus, 0)
        XCTAssertFalse(viewModel.isOperating)
    }

    func testSearchAndDuplicateLoadingProtection() async {
        let api = MockSampleAPI()
        api.listDelayNanoseconds = 50_000_000
        api.listResponses = [SampleMaterialListResponse(total: 1, list: [makeItem(id: 1)])]
        let viewModel = SampleListViewModel()

        let first = Task { await viewModel.updateSearchText("  雪山  ", api: api) }
        await Task.yield()
        await viewModel.loadInitial(api: api)
        await first.value

        XCTAssertEqual(api.listCalls.count, 1)
        XCTAssertEqual(api.listCalls.first?.keyword, "雪山")
        XCTAssertEqual(api.listCalls.first?.pageSize, 10)
    }

    func testChangingMediaTypeClearsMaterialsButKeepsCover() async {
        let viewModel = UploadSampleViewModel(currentScenicIdProvider: { 3 })
        let uploader = MockSampleUploader(urls: ["https://cdn/a.jpg", "https://cdn/cover.jpg"])

        await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
        await viewModel.setCoverImage(makeUploadItem(fileName: "cover.jpg"), uploader: uploader)
        viewModel.selectMediaType(.video)

        XCTAssertTrue(viewModel.uploadedMediaList.isEmpty)
        XCTAssertEqual(viewModel.coverImage?.ossUrl, "https://cdn/cover.jpg")
        XCTAssertEqual(viewModel.mediaType, .video)
    }

    func testScenicCascadeClearsSelectionsAndProjectPagination() async {
        let firstScenic = ScenicInfo(id: 3, name: "景区A", status: 1, coverImg: nil)
        let secondScenic = ScenicInfo(id: 4, name: "景区B", status: 1, coverImg: nil)
        let api = MockSampleAPI()
        api.projectResponses = [
            SampleProjectListResponse(list: (0 ..< 10).map { SampleProjectItem(id: $0, name: "项目\($0)") }, total: 11),
            SampleProjectListResponse(list: [SampleProjectItem(id: 10, name: "项目10")], total: 11),
        ]
        let viewModel = UploadSampleViewModel(currentScenicIdProvider: { 3 })

        await viewModel.selectScenic(firstScenic, api: api)
        await viewModel.loadProjects(refresh: true, api: api)
        await viewModel.loadProjects(refresh: false, api: api)
        viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
        viewModel.selectProject(SampleProjectItem(id: 10, name: "项目10"))

        XCTAssertEqual(api.projectCalls.map(\.page), [1, 2])
        XCTAssertEqual(viewModel.projectList.count, 11)

        await viewModel.selectScenic(secondScenic, api: api)

        XCTAssertNil(viewModel.selectedScenicSpot)
        XCTAssertNil(viewModel.selectedProject)
        XCTAssertTrue(viewModel.projectList.isEmpty)
        XCTAssertFalse(viewModel.projectCanLoadMore)
    }

    func testMaterialCountLimitsFollowMediaType() async {
        let imageViewModel = UploadSampleViewModel(currentScenicIdProvider: { 3 })
        await imageViewModel.addLocalMedia(
            (0 ..< 10).map { makeUploadItem(fileName: "\($0).jpg") },
            uploader: MockSampleUploader(urls: (0 ..< 9).map { "https://cdn/\($0).jpg" })
        )
        XCTAssertEqual(imageViewModel.uploadedMediaList.count, 9)

        let videoViewModel = UploadSampleViewModel(currentScenicIdProvider: { 3 })
        videoViewModel.selectMediaType(.video)
        await videoViewModel.addLocalMedia(
            [makeUploadItem(fileName: "a.mp4"), makeUploadItem(fileName: "b.mp4")],
            uploader: MockSampleUploader(urls: ["https://cdn/a.mp4"])
        )
        XCTAssertEqual(videoViewModel.uploadedMediaList.count, 1)
    }

    func testSequentialUploadContinuesAfterOneFailure() async {
        let viewModel = UploadSampleViewModel(currentScenicIdProvider: { 3 })
        let uploader = MockSampleUploader(results: [
            .failure(APIError.emptyData),
            .success("https://cdn/b.jpg"),
        ])

        await viewModel.addLocalMedia(
            [makeUploadItem(fileName: "a.jpg"), makeUploadItem(fileName: "b.jpg")],
            uploader: uploader
        )

        XCTAssertEqual(viewModel.uploadedMediaList[0].ossUrl, "")
        XCTAssertEqual(viewModel.uploadedMediaList[1].ossUrl, "https://cdn/b.jpg")
        XCTAssertNil(viewModel.uploadDialogState)
    }

    func testLateProgressCallbackCannotReopenCompletedUploadDialog() async {
        let viewModel = UploadSampleViewModel(currentScenicIdProvider: { 3 })
        let uploader = MockSampleUploader(urls: ["https://cdn/a.jpg"])
        uploader.emitLateProgress = true

        await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
        try? await Task.sleep(nanoseconds: 1_000_000)

        XCTAssertEqual(viewModel.uploadedMediaList.first?.ossUrl, "https://cdn/a.jpg")
        XCTAssertFalse(viewModel.uploadedMediaList.first?.isUploading ?? true)
        XCTAssertNil(viewModel.uploadDialogState)
    }

    func testUploadValidationMessages() async {
        let api = MockSampleAPI()
        let viewModel = UploadSampleViewModel(
            scenicListProvider: { [ScenicInfo(id: 3, name: "景区", status: 1, coverImg: nil)] },
            currentScenicIdProvider: { 3 }
        )
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.uploadSample(api: api)
        viewModel.updateAlbumName("样片")
        await viewModel.uploadSample(api: api)
        await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg", ossUrl: "")], uploader: MockSampleUploader(urls: ["https://cdn/a.jpg"]))
        await viewModel.uploadSample(api: api)
        await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg", ossUrl: ""), uploader: MockSampleUploader(urls: ["https://cdn/c.jpg"]))
        await viewModel.uploadSample(api: api)

        XCTAssertEqual(messages.prefix(4), [
            "请输入相册名称",
            "请至少上传一个素材",
            "请上传封面图片",
            "请选择关联景区",
        ])
        XCTAssertNil(api.lastUploadRequest)
    }

    func testUploadValidationRequiresScenicSpotThenProject() async {
        let scenic = ScenicInfo(id: 3, name: "景区", status: 1, coverImg: nil)
        let api = MockSampleAPI()
        let viewModel = UploadSampleViewModel(
            scenicListProvider: { [scenic] },
            currentScenicIdProvider: { 3 }
        )
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }
        viewModel.updateAlbumName("样片")
        await viewModel.addLocalMedia(
            [makeUploadItem(fileName: "a.jpg")],
            uploader: MockSampleUploader(urls: ["https://cdn/a.jpg"])
        )
        await viewModel.setCoverImage(
            makeUploadItem(fileName: "cover.jpg"),
            uploader: MockSampleUploader(urls: ["https://cdn/cover.jpg"])
        )
        await viewModel.selectScenic(scenic, api: api)

        await viewModel.uploadSample(api: api)
        viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
        await viewModel.uploadSample(api: api)

        XCTAssertEqual(Array(messages.suffix(2)), ["请选择关联打卡点", "请选择关联项目"])
        XCTAssertNil(api.lastUploadRequest)
    }

    func testUploadSuccessBuildsSampleRequest() async {
        let scenic = ScenicInfo(id: 3, name: "景区", status: 1, coverImg: nil)
        let api = MockSampleAPI()
        api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
        let viewModel = UploadSampleViewModel(
            scenicListProvider: { [scenic] },
            currentScenicIdProvider: { 3 }
        )
        let uploader = MockSampleUploader(urls: ["https://cdn/a.jpg", "https://cdn/c.jpg"])
        var didSucceed = false
        viewModel.onUploadSuccess = { didSucceed = true }

        viewModel.updateAlbumName("样片")
        await viewModel.selectScenic(scenic, api: api)
        viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
        viewModel.selectProject(SampleProjectItem(id: 7, name: "项目"))
        await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
        await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: uploader)
        await viewModel.uploadSample(api: api)

        XCTAssertTrue(didSucceed)
        XCTAssertEqual(api.lastUploadRequest?.name, "样片")
        XCTAssertEqual(api.lastUploadRequest?.type, "2")
        XCTAssertEqual(api.lastUploadRequest?.mediaType, "1")
        XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.ossUrl, "https://cdn/a.jpg")
        XCTAssertEqual(api.lastUploadRequest?.coverUrl, "https://cdn/c.jpg")
        XCTAssertEqual(api.lastUploadRequest?.scenicSpotId, 9)
        XCTAssertEqual(api.lastUploadRequest?.projectId, 7)
    }
}

@MainActor
private final class MockSampleAPI: SampleManagementServing {
    var listResponses: [SampleMaterialListResponse] = []
    var listCalls: [(status: Int, keyword: String?, page: Int, pageSize: Int)] = []
    var operationCalls: [(id: Int, listingStatus: Int)] = []
    var lastUploadRequest: SampleUploadMaterialRequest?
    var spotResponse = ScenicSpotListResponse(list: [], total: 0)
    var projectResponses: [SampleProjectListResponse] = []
    var operationError: Error?
    var listDelayNanoseconds: UInt64 = 0
    var operationDelayNanoseconds: UInt64 = 0
    var projectCalls: [(scenicId: String, page: Int, pageSize: Int)] = []

    func list(status: Int, keyword: String?, page: Int, pageSize: Int) async throws -> SampleMaterialListResponse {
        listCalls.append((status, keyword, page, pageSize))
        if listDelayNanoseconds > 0 {
            try await Task.sleep(nanoseconds: listDelayNanoseconds)
        }
        guard !listResponses.isEmpty else { return SampleMaterialListResponse() }
        return listResponses.removeFirst()
    }

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

    func setListingStatus(id: Int, listingStatus: Int) async throws {
        operationCalls.append((id, listingStatus))
        if operationDelayNanoseconds > 0 {
            try await Task.sleep(nanoseconds: operationDelayNanoseconds)
        }
        if let operationError { throw operationError }
    }

    func upload(_ request: SampleUploadMaterialRequest) async throws {
        lastUploadRequest = request
    }

    func scenicSpotListAll(scenicId: String) async throws -> ScenicSpotListResponse {
        spotResponse
    }

    func projectList(scenicId: String, name: String?, page: Int, pageSize: Int) async throws -> SampleProjectListResponse {
        projectCalls.append((scenicId, page, pageSize))
        guard !projectResponses.isEmpty else { return SampleProjectListResponse() }
        return projectResponses.removeFirst()
    }
}

@MainActor
private final class MockSampleUploader: SampleOSSUploading {
    private var results: [Result<String, Error>]
    var emitLateProgress = false

    init(urls: [String]) {
        results = urls.map(Result.success)
    }

    init(results: [Result<String, Error>]) {
        self.results = results
    }

    func uploadSampleFile(
        data: Data,
        fileName: String,
        fileType: Int,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String {
        if emitLateProgress {
            onProgress(20)
            Task { @MainActor in
                await Task.yield()
                onProgress(100)
            }
        } else {
            onProgress(100)
        }
        guard !results.isEmpty else { return "https://cdn/default.jpg" }
        return try results.removeFirst().get()
    }
}

private func makeItem(id: Int, status: Int = 1, listingStatus: Int = 0) -> SampleMaterialItem {
    SampleMaterialItem(
        id: id,
        name: "样片\(id)",
        coverUrl: "",
        createdAt: "",
        status: status,
        downloadCount: 0,
        likesCount: 0,
        collectCount: 0,
        shareCount: 0,
        scenicSpotName: "打卡点",
        projectName: "项目",
        listingStatus: listingStatus
    )
}

private func makeUploadItem(fileName: String, ossUrl: String = "") -> SampleUploadedMediaItem {
    SampleUploadedMediaItem(
        data: Data(repeating: 1, count: 10),
        fileName: fileName,
        size: 10,
        width: 1920,
        height: 1080,
        ossUrl: ossUrl
    )
}
