//
//  ScenicQueueSettingsViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/25.
//

import XCTest
@testable import suixinkan

@MainActor
/// 排队设置测试，覆盖加载、保存和校验。
final class ScenicQueueSettingsViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        clearQueueSettingsDefaults()
    }

    func testLoadSelectsFirstSpotAndAppliesLocalSnapshotWhenServerMissing() async {
        let snapshot = ScenicQueueSettingsSnapshot(
            shootMinute: 4,
            shootSecond: 15,
            firstAheadCount: 6,
            firstSms: false,
            firstPhone: true,
            secondAheadCount: 2,
            secondSms: true,
            secondPhone: false,
            broadcastIntervalSec: 55,
            countdownThresholdSec: 25,
            queueDistanceMeter: 8_880,
            queueTakeLimit: 11,
            missCallRequeueOffset: 5,
            showStartShootingButton: false,
            autoCallAheadCount: 3,
            quickCallButtonEnabled: true,
            prepareCallButtonEnabled: true,
            businessOpen: false,
            businessStartTime: "09:30",
            businessEndTime: "18:45"
        )
        ScenicQueueSettingsStore.saveSettingsSnapshot(snapshot, userId: "77", scenicId: 88, spotId: 901)
        let api = ScenicQueueMock()
        api.settingResponse = ScenicQueueSettingData(exists: false)
        let viewModel = ScenicQueueSettingsViewModel()

        await viewModel.load(api: api, scenicId: 88, userId: "77", spots: settingsSpots)

        XCTAssertEqual(viewModel.selectedSpotId, 901)
        XCTAssertEqual(viewModel.photoEstimateMin, "4")
        XCTAssertEqual(viewModel.photoEstimateSec, "15")
        XCTAssertEqual(viewModel.firstAhead, "6")
        XCTAssertTrue(viewModel.firstPhone)
        XCTAssertEqual(viewModel.secondAhead, "2")
        XCTAssertEqual(viewModel.broadcastIntervalSec, "55")
        XCTAssertEqual(viewModel.countdownThresholdSec, "25")
        XCTAssertEqual(viewModel.maxQueueRangeKm, "8.88")
        XCTAssertEqual(viewModel.localQueueLimit, "11")
        XCTAssertEqual(viewModel.localPassDelay, "5")
        XCTAssertFalse(viewModel.showStartShootingButton)
        XCTAssertEqual(viewModel.autoCallAheadCount, "3")
        XCTAssertTrue(viewModel.quickCallButtonEnabled)
        XCTAssertTrue(viewModel.prepareCallButtonEnabled)
        XCTAssertFalse(viewModel.queueOpen)
    }

    func testSaveNormalizesBodyAndPersistsSnapshot() async throws {
        let api = ScenicQueueMock()
        let viewModel = ScenicQueueSettingsViewModel()
        viewModel.scenicSpots = settingsSpots
        viewModel.selectedSpotId = 901
        viewModel.photoEstimateMin = "1"
        viewModel.photoEstimateSec = "20"
        viewModel.firstAhead = "5"
        viewModel.secondAhead = "2"
        viewModel.broadcastIntervalSec = "45"
        viewModel.countdownThresholdSec = "20"
        viewModel.localQueueLimit = "7"
        viewModel.localPassDelay = "3"
        viewModel.showStartShootingButton = false
        viewModel.autoCallAheadCount = "4"
        viewModel.quickCallButtonEnabled = true
        viewModel.prepareCallButtonEnabled = true
        viewModel.queueOpen = false
        viewModel.customTtsText = "  {number} 请到拍摄点  "
        viewModel.presetVoices = ["预设播报", "  ", "第二条"]
        viewModel.businessStartTime = ScenicQueueSettingsViewModel.businessTime(hour: 11, minute: 0)
        viewModel.businessEndTime = ScenicQueueSettingsViewModel.businessTime(hour: 21, minute: 30)
        viewModel.updateMaxQueueRange("12.3")

        try await viewModel.save(api: api, scenicId: 88, userId: "77")

        let request = try XCTUnwrap(api.saveSettingRequests.first)
        XCTAssertEqual(request.scenicId, 88)
        XCTAssertEqual(request.scenicSpotId, 901)
        XCTAssertEqual(request.photoEstimateMin, 1)
        XCTAssertEqual(request.photoEstimateSec, 20)
        XCTAssertEqual(request.queueDistanceMeter, 12_300)
        XCTAssertEqual(request.queueTakeLimit, 7)
        XCTAssertEqual(request.missCallRequeueOffset, 3)
        XCTAssertEqual(request.showStartShootButton, 0)
        XCTAssertEqual(request.autoCallNextCount, 4)
        XCTAssertEqual(request.businessStartTime, "11:00")
        XCTAssertEqual(request.businessEndTime, "21:30")
        XCTAssertEqual(request.voiceBroadcasts.map(\.content), ["预设播报", "第二条"])
        XCTAssertEqual(request.status, 0)
        XCTAssertEqual(request.remark, "{number} 请到拍摄点")

        XCTAssertEqual(ScenicQueueSettingsStore.selectedSpotId(userId: "77", scenicId: 88), 901)
        let snapshot = try XCTUnwrap(ScenicQueueSettingsStore.settingsSnapshot(userId: "77", scenicId: 88, spotId: 901))
        XCTAssertEqual(snapshot.queueDistanceMeter, 12_300)
        XCTAssertEqual(snapshot.autoCallAheadCount, 4)
        XCTAssertTrue(snapshot.quickCallButtonEnabled)
    }

    func testValidationRejectsInvalidValues() async {
        let api = ScenicQueueMock()
        let viewModel = ScenicQueueSettingsViewModel()
        viewModel.selectedSpotId = 901
        viewModel.photoEstimateMin = "0"
        viewModel.photoEstimateSec = "0"

        await XCTAssertThrowsErrorAsync(try await viewModel.save(api: api, scenicId: 88, userId: "77"))

        viewModel.photoEstimateSec = "20"
        viewModel.firstAhead = "1"
        viewModel.secondAhead = "2"
        await XCTAssertThrowsErrorAsync(try await viewModel.save(api: api, scenicId: 88, userId: "77"))

        viewModel.firstAhead = "2"
        viewModel.secondAhead = "1"
        viewModel.broadcastIntervalSec = "30"
        await XCTAssertThrowsErrorAsync(try await viewModel.save(api: api, scenicId: 88, userId: "77"))
    }

    func testFetchQRCodeRejectsEmptyURL() async {
        let api = ScenicQueueMock()
        api.qrcodeURL = ""
        let viewModel = ScenicQueueSettingsViewModel()
        viewModel.selectedSpotId = 901

        await XCTAssertThrowsErrorAsync(try await viewModel.fetchQRCode(api: api, scenicId: 88))
    }
}

private let settingsSpots = [
    ScenicSpotItem(id: 901, name: "东门打卡点"),
    ScenicSpotItem(id: 902, name: "西门打卡点")
]

private func clearQueueSettingsDefaults() {
    let defaults = UserDefaults.standard
    [
        ScenicQueueLocalSettings.selectedSpotIdKey,
        ScenicQueueLocalSettings.selectedSpotNameKey,
        ScenicQueueLocalSettings.customTtsTextKey,
        ScenicQueueLocalSettings.photoEstimateSecondsKey,
        ScenicQueueLocalSettings.broadcastIntervalSecondsKey,
        ScenicQueueLocalSettings.countdownThresholdSecondsKey,
        ScenicQueueLocalSettings.showStartShootingButtonKey,
        ScenicQueueLocalSettings.autoCallAheadCountKey,
        ScenicQueueLocalSettings.quickCallButtonEnabledKey,
        ScenicQueueLocalSettings.prepareCallButtonEnabledKey,
        ScenicQueueSettingsStore.scopedKey(base: ScenicQueueLocalSettings.selectedSpotIdKey, userId: "77", scenicId: 88),
        ScenicQueueSettingsStore.scopedKey(base: ScenicQueueLocalSettings.selectedSpotNameKey, userId: "77", scenicId: 88),
        ScenicQueueSettingsStore.scopedSpotKey(base: ScenicQueueLocalSettings.settingsSnapshotKey, userId: "77", scenicId: 88, spotId: 901),
        ScenicQueueSettingsStore.scopedSpotKey(base: ScenicQueueLocalSettings.customTtsTextKey, userId: "77", scenicId: 88, spotId: 901),
        ScenicQueueSettingsStore.scopedSpotKey(base: ScenicQueueLocalSettings.presetVoicesKey, userId: "77", scenicId: 88, spotId: 901)
    ].forEach(defaults.removeObject)
}

private func XCTAssertThrowsErrorAsync<T>(
    _ expression: @autoclosure () async throws -> T,
    file: StaticString = #filePath,
    line: UInt = #line
) async {
    do {
        try await expression()
        XCTFail("Expected error to be thrown", file: file, line: line)
    } catch {
    }
}
