//
//  MaterialManagementViewModelTests.swift
//  suixinkanTests
//

import Foundation
import XCTest
@testable import suixinkan

/// 素材管理 ViewModel 测试。
@MainActor
final class MaterialManagementViewModelTests: XCTestCase {
    func testDetailCarouselLoopsAcrossCopiedBoundaryPages() {
        XCTAssertEqual(MaterialCarouselIndexMapper.initialRenderedPage(mediaCount: 3), 1)
        XCTAssertEqual(MaterialCarouselIndexMapper.normalizedRenderedPage(0, mediaCount: 3), 3)
        XCTAssertEqual(MaterialCarouselIndexMapper.logicalPage(renderedPage: 3, mediaCount: 3), 2)
        XCTAssertEqual(MaterialCarouselIndexMapper.normalizedRenderedPage(4, mediaCount: 3), 1)
        XCTAssertEqual(MaterialCarouselIndexMapper.logicalPage(renderedPage: 1, mediaCount: 3), 0)
        XCTAssertEqual(MaterialCarouselIndexMapper.initialRenderedPage(mediaCount: 1), 0)
    }

    func testListLoadsMoreSearchesAndMapsFilterStatus() async {
        let api = MockMaterialAPI()
        api.listResponses = [
            MaterialListResponse(total: 12, list: (0 ..< 10).map { makeItem(id: $0) }, order: MaterialOrderInfo(totalNum: 1386, avgOrderAmount: "12", refundTotal: "3")),
            MaterialListResponse(total: 12, list: [makeItem(id: 9), makeItem(id: 10), makeItem(id: 11)]),
            MaterialListResponse(total: 1, list: [makeItem(id: 99)]),
        ]
        let viewModel = MaterialListViewModel()

        await viewModel.loadInitial(api: api)
        await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api)
        XCTAssertEqual(viewModel.items.map(\.id), Array(0 ... 11))
        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 testRejectedMaterialCannotToggleListing() async {
        let api = MockMaterialAPI()
        api.listResponses = [MaterialListResponse(total: 1, list: [makeItem(id: 1, status: 0, listingStatus: 0)])]
        let viewModel = MaterialListViewModel()
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.loadInitial(api: api)
        await viewModel.toggleListing(materialId: 1, checked: true, api: api)

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

    func testApprovedMaterialToggleUpdatesLocalItem() async {
        let api = MockMaterialAPI()
        api.listResponses = [MaterialListResponse(total: 1, list: [makeItem(id: 2, status: 1, listingStatus: 0)])]
        let viewModel = MaterialListViewModel()

        await viewModel.loadInitial(api: api)
        await viewModel.toggleListing(materialId: 2, checked: true, api: api)

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

    func testDetailDeleteCallsCallback() async {
        let api = MockMaterialAPI()
        let viewModel = MaterialDetailViewModel(materialId: 7)
        var deletedId: Int?
        viewModel.onDeleted = { deletedId = $0 }

        await viewModel.delete(api: api)

        XCTAssertEqual(api.deletedIds, [7])
        XCTAssertEqual(deletedId, 7)
    }

    func testUploadValidationMessages() async {
        let api = MockMaterialAPI()
        let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.submit(api: api)
        viewModel.updateMaterialName("素材")
        await viewModel.submit(api: api)
        await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: MockMaterialUploader(urls: ["https://cdn/a.jpg"]))
        await viewModel.submit(api: api)
        await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: MockMaterialUploader(urls: ["https://cdn/c.jpg"]))
        await viewModel.submit(api: api)

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

    func testUploadSuccessBuildsMaterialRequestWithTags() async {
        let api = MockMaterialAPI()
        api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
        let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
        let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg", "https://cdn/c.jpg"])
        var didSucceed = false
        viewModel.onSubmitSuccess = { didSucceed = true }

        await viewModel.loadInitial(api: api)
        viewModel.updateMaterialName("素材")
        viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
        viewModel.updateTagInput("snow-tag")
        await viewModel.addTag(api: api)
        await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
        await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: uploader)
        await viewModel.submit(api: api)

        XCTAssertTrue(didSucceed)
        XCTAssertEqual(api.lastUploadRequest?.name, "素材")
        XCTAssertEqual(api.lastUploadRequest?.type, "1")
        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?.materialTag, "42")
        XCTAssertNil(api.lastUploadRequest?.id)
    }

    func testChangingMediaTypeClearsSelectedMedia() async {
        let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
        await viewModel.addLocalMedia(
            [makeUploadItem(fileName: "image.jpg")],
            uploader: MockMaterialUploader(urls: ["https://cdn/image.jpg"])
        )

        viewModel.selectMediaType(.video)

        XCTAssertEqual(viewModel.mediaType, .video)
        XCTAssertTrue(viewModel.uploadedMediaList.isEmpty)
    }

    func testMediaLimitsAndSequentialUploadMatchAndroid() async {
        let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
        let imageItems = (0 ..< 11).map { makeUploadItem(fileName: "\($0).jpg") }
        let uploader = MockMaterialUploader(urls: (0 ..< 11).map { "https://cdn/\($0).jpg" })

        await viewModel.addLocalMedia(imageItems, uploader: uploader)

        XCTAssertEqual(viewModel.uploadedMediaList.count, 9)
        XCTAssertEqual(uploader.uploadedFileNames, (0 ..< 9).map { "\($0).jpg" })

        viewModel.selectMediaType(.video)
        let videoUploader = MockMaterialUploader(urls: ["https://cdn/a.mp4", "https://cdn/b.mp4"])
        await viewModel.addLocalMedia(
            [makeUploadItem(fileName: "a.mp4"), makeUploadItem(fileName: "b.mp4")],
            uploader: videoUploader
        )

        XCTAssertEqual(viewModel.uploadedMediaList.map(\.fileName), ["a.mp4"])
        XCTAssertEqual(videoUploader.uploadedFileNames, ["a.mp4"])
    }

    func testTagDeletionRequiresConfirmation() async {
        let api = MockMaterialAPI()
        let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
        viewModel.updateTagInput("snow-tag")
        await viewModel.addTag(api: api)
        let tag = try! XCTUnwrap(viewModel.tagList.first)

        viewModel.requestDeleteTag(tag)
        XCTAssertEqual(viewModel.pendingDeleteTag, tag)
        XCTAssertEqual(viewModel.tagList, [tag])

        viewModel.cancelDeleteTag()
        XCTAssertNil(viewModel.pendingDeleteTag)
        XCTAssertEqual(viewModel.tagList, [tag])

        viewModel.requestDeleteTag(tag)
        viewModel.confirmDeleteTag()
        XCTAssertNil(viewModel.pendingDeleteTag)
        XCTAssertTrue(viewModel.tagList.isEmpty)
    }

    func testInputLengthsAreCappedLikeAndroid() {
        let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })

        viewModel.updateMaterialName(String(repeating: "名", count: 35))
        viewModel.updateTagInput(String(repeating: "标", count: 20))

        XCTAssertEqual(viewModel.materialName.count, 30)
        XCTAssertEqual(viewModel.tagInput.count, 15)
    }

    func testUploadDialogStaysHiddenAfterCompletionProgressCallback() async {
        let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
        let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg"])

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

        XCTAssertNil(viewModel.uploadDialogState)
        XCTAssertFalse(viewModel.uploadedMediaList.first?.isUploading ?? true)
        XCTAssertEqual(viewModel.uploadedMediaList.first?.uploadProgress, 100)
    }

    func testEditLoadsDetailAndBuildsEditRequest() async {
        let api = MockMaterialAPI()
        api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
        api.detailResponse = try! decodeDetail("""
        {
          "id": 11,
          "name": "旧素材",
          "status": 1,
          "listing_status": 1,
          "cover_url": "https://cdn/cover.jpg",
          "cover_size": 22,
          "scenic_spot_id": 9,
          "material_tag": [{"id": 3, "name": "cloudy"}],
          "media_list": [{
            "id": 5,
            "original_name": "old.jpg",
            "oss_url": "https://cdn/old.jpg",
            "type": 1,
            "size": 10,
            "width": 100,
            "height": 80
          }]
        }
        """)
        let viewModel = MaterialFormViewModel(mode: .edit(id: 11), currentScenicIdProvider: { 3 })

        await viewModel.loadInitial(api: api)
        await viewModel.submit(api: api)

        XCTAssertEqual(viewModel.materialName, "旧素材")
        XCTAssertEqual(viewModel.selectedScenicSpot?.id, 9)
        XCTAssertEqual(viewModel.uploadedMediaList.first?.ossUrl, "https://cdn/old.jpg")
        XCTAssertEqual(api.lastEditRequest?.id, 11)
        XCTAssertEqual(api.lastEditRequest?.type, "1")
        XCTAssertEqual(api.lastEditRequest?.materialTag, "3")
    }
}

/// 素材管理 API mock。
/// 素材管理 API mock。
@MainActor
private final class MockMaterialAPI: MaterialManagementServing {
    var listResponses: [MaterialListResponse] = []
    var listCalls: [(status: Int, keyword: String?, page: Int, pageSize: Int)] = []
    var operationCalls: [(id: Int, listingStatus: Int)] = []
    var deletedIds: [Int] = []
    var tagId = 42
    var lastUploadRequest: MaterialUploadRequest?
    var lastEditRequest: MaterialUploadRequest?
    var spotResponse = ScenicSpotListResponse(list: [], total: 0)
    var detailResponse = try! decodeDetail(#"{"id":1,"status":1,"listing_status":1}"#)

    func list(status: Int, keyword: String?, page: Int, pageSize: Int) async throws -> MaterialListResponse {
        listCalls.append((status, keyword, page, pageSize))
        guard !listResponses.isEmpty else { return MaterialListResponse() }
        return listResponses.removeFirst()
    }

    func detail(id: Int) async throws -> MaterialDetail {
        detailResponse
    }

    func setListingStatus(id: Int, listingStatus: Int) async throws {
        operationCalls.append((id, listingStatus))
    }

    func delete(id: Int) async throws {
        deletedIds.append(id)
    }

    func addTag(name: String) async throws -> Int {
        tagId
    }

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

    func edit(_ request: MaterialUploadRequest) async throws {
        lastEditRequest = request
    }

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

/// 素材管理 OSS 上传 mock。
/// 素材管理 OSS 上传 mock。
@MainActor
private final class MockMaterialUploader: MaterialOSSUploading {
    private var urls: [String]
    private(set) var uploadedFileNames: [String] = []

    init(urls: [String]) {
        self.urls = urls
    }

    func uploadMaterialFile(
        data: Data,
        fileName: String,
        fileType: Int,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String {
        uploadedFileNames.append(fileName)
        onProgress(100)
        return urls.isEmpty ? "https://cdn/default.jpg" : urls.removeFirst()
    }
}

private func makeItem(id: Int, status: Int = 1, listingStatus: Int = 0) -> MaterialItem {
    MaterialItem(
        id: id,
        name: "素材\(id)",
        coverUrl: "",
        createdAt: "",
        status: status,
        downloadCount: 0,
        likesCount: 0,
        collectCount: 0,
        shareCount: 0,
        listingStatus: listingStatus
    )
}

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

private func decodeDetail(_ json: String) throws -> MaterialDetail {
    try JSONDecoder().decode(MaterialDetail.self, from: Data(json.utf8))
}
