//
//  PunchPointViewModelTests.swift
//  suixinkanTests
//

import CoreLocation
import XCTest
@testable import suixinkan

/// 打卡点列表与表单 ViewModel 行为测试。
@MainActor
final class PunchPointViewModelTests: XCTestCase {
    func testFilterRefreshAndPaginationUseAndroidStatusAndPageSize() async {
        let api = MockPunchPointAPI()
        api.listResponses = [
            PunchPointListResponse(total: 12, list: (1...10).map { item(id: Int64($0)) }),
            PunchPointListResponse(total: 12, list: [item(id: 11), item(id: 12)]),
            PunchPointListResponse(total: 1, list: [item(id: 21)]),
        ]
        let viewModel = PunchPointListViewModel(currentScenicIdProvider: { 9 })

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

        XCTAssertEqual(viewModel.items.map(\.id), [21])
        XCTAssertEqual(api.listCalls.map(\.status), [0, 0, 2])
        XCTAssertEqual(api.listCalls.map(\.page), [1, 2, 1])
        XCTAssertEqual(api.listCalls.map(\.pageSize), [10, 10, 10])
        XCTAssertEqual(api.listCalls.map(\.scenicAreaId), ["9", "9", "9"])
    }

    func testDeleteSuccessRemovesItemAndQRCodeValidationMatchesAndroid() async {
        let api = MockPunchPointAPI()
        api.listResponses = [PunchPointListResponse(total: 2, list: [
            item(id: 1, status: 1, mpQrcode: "https://cdn/qr.png"),
            item(id: 2, status: 3, mpQrcode: nil),
        ])]
        let viewModel = PunchPointListViewModel(currentScenicIdProvider: { 9 })
        var messages: [String] = []
        var qrcode: String?
        var removalID: Int64?
        viewModel.onShowMessage = { messages.append($0) }
        viewModel.onShowQRCode = { qrcode = $0 }
        viewModel.onBeginDeleteAnimation = { removalID = $0 }

        await viewModel.loadInitial(api: api)
        viewModel.showQRCode(for: viewModel.items[1])
        viewModel.showQRCode(for: PunchPointItem(id: 3, status: 1, mpQrcode: " "))
        viewModel.showQRCode(for: viewModel.items[0])
        await viewModel.delete(item: viewModel.items[0], api: api)

        XCTAssertEqual(messages, ["未审核通过不可查看二维码", "暂无二维码"])
        XCTAssertEqual(qrcode, "https://cdn/qr.png")
        XCTAssertEqual(removalID, 1)
        XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
        viewModel.completeDeleteAnimation(id: 1)
        XCTAssertEqual(viewModel.items.map(\.id), [2])
        XCTAssertEqual(api.deletedIds, [1])
    }

    func testFormValidationUploadAndCreateRequestBody() async {
        let api = MockPunchPointAPI()
        let uploader = MockPunchPointUploader(resultURL: "https://cdn/upload.jpg")
        let location = MockPunchPointLocationProvider(reverseAddress: "地图地址")
        let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.submit(api: api)
        viewModel.updateName("湖边打卡")
        await viewModel.selectCoordinate(CLLocationCoordinate2D(latitude: 30.1, longitude: 120.2))
        viewModel.updateDescription(String(repeating: "景", count: 80))
        await viewModel.addLocalImages([PunchPointImageState(data: Data([1, 2]), fileName: "a.jpg")], uploader: uploader)
        await viewModel.submit(api: api)

        XCTAssertEqual(messages.first, "请输入打卡点名称")
        XCTAssertEqual(viewModel.description.count, 50)
        XCTAssertEqual(uploader.uploads.map(\.moduleFileName), ["a.jpg"])
        let request = api.addRequests.first
        XCTAssertEqual(request?.scenicAreaId, "7")
        XCTAssertEqual(request?.name, "湖边打卡")
        XCTAssertEqual(request?.region.lat, 30.1)
        XCTAssertEqual(request?.region.lot, 120.2)
        XCTAssertEqual(request?.region.address, "地图地址")
        XCTAssertEqual(request?.scenicSpotString, "地图地址")
        XCTAssertEqual(request?.guideImages, ["https://cdn/upload.jpg"])
    }

    func testEditInitializationAndSubmitUsesEditAPI() async {
        let api = MockPunchPointAPI()
        let viewModel = PunchPointFormViewModel(mode: .edit(id: 66), currentScenicIdProvider: { 8 }, locationProvider: MockPunchPointLocationProvider())
        var changedCoordinate: CLLocationCoordinate2D?
        viewModel.onCoordinateChange = { changedCoordinate = $0 }

        viewModel.initialize(with: PunchPointDetail(
            id: 66,
            name: "旧打卡点",
            region: PunchPointRegion(lat: 31.1, lot: 121.1, address: "旧地址"),
            description: "说明",
            guideImages: ["https://cdn/old.jpg"]
        ))
        viewModel.updateName("新打卡点")
        await viewModel.submit(api: api)

        XCTAssertEqual(changedCoordinate?.latitude, 31.1)
        XCTAssertEqual(viewModel.images.first?.uploadedURL, "https://cdn/old.jpg")
        XCTAssertEqual(api.editRequests.first?.id, 66)
        XCTAssertEqual(api.editRequests.first?.name, "新打卡点")
        XCTAssertEqual(api.editRequests.first?.guideImages, ["https://cdn/old.jpg"])
    }

    func testUploadingAndFailedImagesBlockSubmit() async {
        let api = MockPunchPointAPI()
        let slowUploader = SlowPunchPointUploader()
        let location = MockPunchPointLocationProvider(reverseAddress: "地址")
        let uploadingViewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
        uploadingViewModel.updateName("上传中")
        await uploadingViewModel.selectCoordinate(CLLocationCoordinate2D(latitude: 1, longitude: 2))
        let started = expectation(description: "upload started")
        slowUploader.onStart = { started.fulfill() }
        let task = Task {
            await uploadingViewModel.addLocalImages([PunchPointImageState(data: Data([1]), fileName: "slow.jpg")], uploader: slowUploader)
        }
        await fulfillment(of: [started], timeout: 1)
        var uploadMessages: [String] = []
        uploadingViewModel.onShowMessage = { uploadMessages.append($0) }

        await uploadingViewModel.submit(api: api)
        task.cancel()

        let failedViewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
        failedViewModel.updateName("失败图")
        await failedViewModel.selectCoordinate(CLLocationCoordinate2D(latitude: 1, longitude: 2))
        await failedViewModel.addLocalImages(
            [PunchPointImageState(data: Data([1]), fileName: "fail.jpg")],
            uploader: MockPunchPointUploader(error: URLError(.cannotConnectToHost))
        )
        var failedMessages: [String] = []
        failedViewModel.onShowMessage = { failedMessages.append($0) }
        await failedViewModel.submit(api: api)

        XCTAssertEqual(uploadMessages.last, "图片正在上传，请稍后提交")
        XCTAssertEqual(failedMessages.last, "请先完成图片上传")
        XCTAssertTrue(api.addRequests.isEmpty)
    }

    func testImageLimitAcceptsNineAndRejectsFurtherSelection() async {
        let uploader = MockPunchPointUploader()
        let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 })
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }
        let images = (0 ..< 10).map {
            PunchPointImageState(data: Data([UInt8($0)]), fileName: "\($0).jpg")
        }

        await viewModel.addLocalImages(images, uploader: uploader)
        await viewModel.addLocalImages(
            [PunchPointImageState(data: Data([99]), fileName: "extra.jpg")],
            uploader: uploader
        )

        XCTAssertEqual(viewModel.images.count, 9)
        XCTAssertEqual(uploader.uploads.count, 9)
        XCTAssertEqual(messages.last, "最多只能选择9张图片")
    }

    func testFailedImageCanRetryAndBecomeUploaded() async {
        let uploader = SequencedPunchPointUploader()
        let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 })

        await viewModel.addLocalImages(
            [PunchPointImageState(data: Data([1]), fileName: "retry.jpg")],
            uploader: uploader
        )
        XCTAssertNotNil(viewModel.images.first?.errorMessage)

        await viewModel.retryUpload(at: 0, uploader: uploader)

        XCTAssertNil(viewModel.images.first?.errorMessage)
        XCTAssertEqual(viewModel.images.first?.uploadedURL, "https://cdn/retry.jpg")
        XCTAssertEqual(uploader.callCount, 2)
    }

    func testAutoLocationFillsCoordinateAndAddress() async {
        let location = MockPunchPointLocationProvider(
            snapshot: HomeLocationSnapshot(latitude: 31.2, longitude: 121.5, address: "当前位置地址")
        )
        let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
        var changedCoordinate: CLLocationCoordinate2D?
        viewModel.onCoordinateChange = { changedCoordinate = $0 }

        await viewModel.autoLocation()

        XCTAssertEqual(viewModel.coordinate?.latitude, 31.2)
        XCTAssertEqual(viewModel.coordinate?.longitude, 121.5)
        XCTAssertEqual(viewModel.address, "当前位置地址")
        XCTAssertEqual(changedCoordinate?.latitude, 31.2)
    }

    private func item(id: Int64, status: Int = 1, mpQrcode: String? = nil) -> PunchPointItem {
        PunchPointItem(
            id: id,
            name: "点\(id)",
            status: status,
            statusLabel: status == 1 ? "运营中" : "未审核",
            region: PunchPointRegion(lat: 30, lot: 120, address: "地址\(id)"),
            guideImages: ["https://cdn/\(id).jpg"],
            mpQrcode: mpQrcode
        )
    }
}

@MainActor
private final class MockPunchPointAPI: PunchPointAPIProtocol {
    var listResponses: [PunchPointListResponse] = []
    var listCalls: [(scenicAreaId: String, status: Int, page: Int, pageSize: Int)] = []
    var addRequests: [PunchPointSaveRequest] = []
    var editRequests: [PunchPointSaveRequest] = []
    var deletedIds: [Int64] = []

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

    func info(id: Int64) async throws -> PunchPointDetail {
        PunchPointDetail(id: id)
    }

    func add(_ request: PunchPointSaveRequest) async throws {
        addRequests.append(request)
    }

    func edit(_ request: PunchPointSaveRequest) async throws {
        editRequests.append(request)
    }

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

@MainActor
private final class MockPunchPointUploader: PunchPointImageUploading {
    struct Upload {
        let moduleFileName: String
        let scenicId: Int
    }

    var uploads: [Upload] = []
    let resultURL: String
    let error: Error?

    init(resultURL: String = "https://cdn/default.jpg", error: Error? = nil) {
        self.resultURL = resultURL
        self.error = error
    }

    func uploadPunchPointImage(
        data: Data,
        fileName: String,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String {
        uploads.append(Upload(moduleFileName: fileName, scenicId: scenicId))
        onProgress(100)
        if let error {
            throw error
        }
        return resultURL
    }
}

@MainActor
private final class SlowPunchPointUploader: PunchPointImageUploading {
    var onStart: (() -> Void)?

    func uploadPunchPointImage(
        data: Data,
        fileName: String,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String {
        onProgress(10)
        onStart?()
        try await Task.sleep(nanoseconds: 1_000_000_000)
        onProgress(100)
        return "https://cdn/slow.jpg"
    }
}

@MainActor
private final class SequencedPunchPointUploader: PunchPointImageUploading {
    var callCount = 0

    func uploadPunchPointImage(
        data: Data,
        fileName: String,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String {
        callCount += 1
        if callCount == 1 {
            throw URLError(.cannotConnectToHost)
        }
        onProgress(100)
        return "https://cdn/retry.jpg"
    }
}

private final class MockPunchPointLocationProvider: LocationProviding, @unchecked Sendable {
    let snapshot: HomeLocationSnapshot
    let reverseAddress: String

    init(
        snapshot: HomeLocationSnapshot = HomeLocationSnapshot(latitude: 30.2, longitude: 120.3, address: "当前位置"),
        reverseAddress: String = "反查地址"
    ) {
        self.snapshot = snapshot
        self.reverseAddress = reverseAddress
    }

    func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
        snapshot
    }

    func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: snapshot.latitude, longitude: snapshot.longitude)
    }

    func reverseGeocode(latitude: Double, longitude: Double) async -> String {
        reverseAddress
    }
}
