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

import XCTest
@testable import suixinkan

@MainActor
/// 位置上报 API 测试，覆盖旧工程接口路径、参数和宽松解码。
final class LocationReportAPITests: XCTestCase {
    /// 测试上报接口携带旧工程需要的 query 参数。
    func testReportLocationUsesExpectedQuery() async throws {
        let session = LocationRecordingSession(data: Self.submitResponse)
        let api = LocationReportAPI(client: APIClient(session: session))

        let response = try await api.reportLocation(staffId: 77, latitude: 30.1, longitude: 120.2, address: "入口", type: .immediate, scenicId: 88)

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/loacation/report")
        let query = locationQueryItems(from: request)
        XCTAssertEqual(query["staff_id"], "77")
        XCTAssertEqual(query["latitude"], "30.1")
        XCTAssertEqual(query["longitude"], "120.2")
        XCTAssertEqual(query["address"], "入口")
        XCTAssertEqual(query["type"], "1")
        XCTAssertEqual(query["scenic_id"], "88")
        XCTAssertEqual(response.expired, 7200)
    }

    /// 测试历史接口支持类型、日期和分页参数。
    func testHistoryUsesExpectedQueryAndDecodesLossyFields() async throws {
        let session = LocationRecordingSession(data: Self.historyResponse)
        let api = LocationReportAPI(client: APIClient(session: session))

        let payload = try await api.locationReportList(staffId: 77, page: 0, pageSize: 0, type: .marked, startDate: "2026-06-01", endDate: "2026-06-24")

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/loacation/list")
        let query = locationQueryItems(from: request)
        XCTAssertEqual(query["staff_id"], "77")
        XCTAssertEqual(query["page"], "1")
        XCTAssertEqual(query["page_size"], "1")
        XCTAssertEqual(query["type"], "2")
        XCTAssertEqual(query["start_date"], "2026-06-01")
        XCTAssertEqual(query["end_date"], "2026-06-24")
        XCTAssertEqual(payload.total, 1)
        XCTAssertEqual(payload.list.first?.staffId, 77)
        XCTAssertEqual(payload.list.first?.latitude, "30.1")
    }

    /// 测试详情接口路径与字段解码。
    func testDetailUsesExpectedQueryAndDecodesFields() async throws {
        let session = LocationRecordingSession(data: Self.detailResponse)
        let api = LocationReportAPI(client: APIClient(session: session))

        let detail = try await api.locationDetail(staffId: 77)

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/loacation/detail")
        XCTAssertEqual(locationQueryItems(from: request)["staff_id"], "77")
        XCTAssertEqual(detail.status, 1)
        XCTAssertTrue(detail.needReport)
        XCTAssertEqual(detail.expireTime, 1_718_000_000)
    }

    private static let submitResponse = Data(#"{"code":100000,"msg":"ok","data":{"staff_id":77,"expired":"7200","status":"1"}}"#.utf8)
    private static let historyResponse = Data(#"{"code":100000,"msg":"ok","data":{"total":"1","list":[{"id":"1","staff_id":"77","type":"2","latitude":30.1,"longitude":"120.2","address":"入口","ip":"127.0.0.1","remark":"ok","created_at":"2026-06-24 10:00:00"}]}}"#.utf8)
    private static let detailResponse = Data(#"{"code":100000,"msg":"ok","data":{"status":"1","need_report":"1","expire_time":"1718000000"}}"#.utf8)
}

@MainActor
/// 位置上报 ViewModel 测试，覆盖提交保护和类型选择。
final class LocationReportViewModelTests: XCTestCase {
    /// 测试缺 staffId 或 scenicId 时禁止上报。
    func testSubmitRequiresStaffAndScenic() async {
        let api = MockLocationReportService()
        let homeVM = HomeLocationViewModel(api: api, store: makeIsolatedStore())
        let toast = ToastCenter()
        let viewModel = LocationReportViewModel()
        viewModel.applyCurrentLocation(latitude: 30.1, longitude: 120.2, address: "入口")

        let missingStaff = await viewModel.submit(type: .immediate, staffId: nil, scenicId: 88, homeLocationViewModel: homeVM, toast: toast)
        let missingScenic = await viewModel.submit(type: .immediate, staffId: 77, scenicId: nil, homeLocationViewModel: homeVM, toast: toast)

        XCTAssertFalse(missingStaff)
        XCTAssertFalse(missingScenic)
        XCTAssertEqual(api.reportRequests.count, 0)
    }

    /// 测试标记点上报使用正确坐标并更新倒计时。
    func testSubmitUsesExpectedCoordinates() async {
        let api = MockLocationReportService()
        let homeVM = HomeLocationViewModel(api: api, store: makeIsolatedStore())
        let toast = ToastCenter()
        let viewModel = LocationReportViewModel()
        viewModel.applyCurrentLocation(latitude: 30.1, longitude: 120.2, address: "入口")
        viewModel.applyMarkedLocation(latitude: 31.1, longitude: 121.2, address: "标记点")

        _ = await viewModel.submit(type: .marked, staffId: 77, scenicId: 88, homeLocationViewModel: homeVM, toast: toast)

        XCTAssertEqual(api.reportRequests.map(\.type), [.marked])
        XCTAssertEqual(api.reportRequests.first?.latitude, 31.1)
        XCTAssertEqual(homeVM.secondsUntilReport, 600)
    }

    /// 测试立即上报成功更新倒计时。
    func testSubmitImmediateUpdatesCountdown() async {
        let api = MockLocationReportService()
        let homeVM = HomeLocationViewModel(api: api, store: makeIsolatedStore())
        let toast = ToastCenter()
        let viewModel = LocationReportViewModel()
        viewModel.applyCurrentLocation(latitude: 30.1, longitude: 120.2, address: "入口")

        _ = await viewModel.submit(type: .immediate, staffId: 77, scenicId: 88, homeLocationViewModel: homeVM, toast: toast)

        XCTAssertEqual(api.reportRequests.map(\.type), [.immediate])
        XCTAssertEqual(homeVM.secondsUntilReport, 600)
    }

    /// 测试上报失败保留当前倒计时。
    func testSubmitFailureKeepsCountdown() async {
        let api = MockLocationReportService()
        api.shouldFailReport = true
        let homeVM = HomeLocationViewModel(api: api, store: makeIsolatedStore())
        let toast = ToastCenter()
        let viewModel = LocationReportViewModel()
        viewModel.applyCurrentLocation(latitude: 30.1, longitude: 120.2, address: "入口")

        let success = await viewModel.submit(type: .immediate, staffId: 77, scenicId: 88, homeLocationViewModel: homeVM, toast: toast)

        XCTAssertFalse(success)
        XCTAssertEqual(homeVM.secondsUntilReport, 0)
        XCTAssertNotNil(viewModel.errorMessage)
    }

    /// 测试历史筛选和分页请求正确。
    func testHistoryFilterAndPagination() async {
        let api = MockLocationReportService()
        api.historyPages = [
            ListPayload(total: 2, list: [LocationReportHistoryItem.fixture(id: 1)]),
            ListPayload(total: 2, list: [LocationReportHistoryItem.fixture(id: 2)])
        ]
        let viewModel = LocationReportHistoryViewModel()
        viewModel.selectedType = .marked

        await viewModel.reload(staffId: 77, api: api)
        await viewModel.loadMore(staffId: 77, api: api)
        await viewModel.loadMore(staffId: 77, api: api)

        XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
        XCTAssertEqual(api.historyRequests.map(\.page), [1, 2])
        XCTAssertEqual(api.historyRequests.first?.type, .marked)
    }

    private func makeIsolatedStore() -> LocationStateStore {
        let suiteName = "LocationReportViewModelTests.\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suiteName)!
        return LocationStateStore(defaults: defaults)
    }
}

@MainActor
/// 位置状态存储测试。
final class LocationStateStoreTests: XCTestCase {
    /// 测试在线状态、过期时间、上报时间和提醒分钟持久化。
    func testPersistsOnlineReminderReportAndExpireTime() {
        let store = makeIsolatedStore()
        store.saveOnlineStatus(true)
        store.saveReminderMinutes(15)
        store.saveLastReportTime(1_000)
        store.saveExpireTime(1_782_726_128)

        XCTAssertTrue(store.loadOnlineStatus())
        XCTAssertEqual(store.loadReminderMinutes(), 15)
        XCTAssertEqual(store.loadLastReportTime(), 1_000)
        XCTAssertEqual(store.loadExpireTime(), 1_782_726_128)
    }

    /// 测试将 Unix 时间戳和剩余秒数规范化为过期时间戳。
    func testNormalizedExpireTimestamp() {
        let store = makeIsolatedStore()
        let now = Date(timeIntervalSince1970: 1_782_718_928)

        XCTAssertEqual(store.normalizedExpireTimestamp(from: 1_782_726_128, now: now), 1_782_726_128)
        XCTAssertEqual(store.normalizedExpireTimestamp(from: 7_200, now: now), 1_782_726_128)
        XCTAssertEqual(store.normalizedExpireTimestamp(from: 0, now: now), 1_782_726_128)
    }

    /// 测试根据过期时间戳计算剩余倒计时。
    func testRemainingSecondsUntilExpireTime() {
        let store = makeIsolatedStore()
        let now = Date(timeIntervalSince1970: 1_782_719_528)

        XCTAssertEqual(store.remainingSeconds(untilExpireTime: 1_782_726_128, now: now), 6_600)
        XCTAssertEqual(store.remainingSeconds(untilExpireTime: 1_782_719_528, now: now), 0)
    }

    /// 测试剩余倒计时与过期判断（兼容旧版仅保存上报时间）。
    func testRemainingSecondsAndWindow() {
        let store = makeIsolatedStore()
        let anchor = Date(timeIntervalSince1970: 1_000).timeIntervalSince1970 * 1000
        let beforeExpiry = Date(timeIntervalSince1970: 1_000 + 3_600)
        let afterExpiry = Date(timeIntervalSince1970: 1_000 + 8_000)

        XCTAssertTrue(store.isWithinOnlineWindow(since: anchor, now: beforeExpiry))
        XCTAssertFalse(store.isWithinOnlineWindow(since: anchor, now: afterExpiry))
        XCTAssertEqual(store.remainingSeconds(since: anchor, now: beforeExpiry), 3_600)
        XCTAssertEqual(store.remainingSeconds(since: anchor, now: afterExpiry), 0)
    }

    private func makeIsolatedStore() -> LocationStateStore {
        let suiteName = "LocationStateStoreTests.\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suiteName)!
        return LocationStateStore(defaults: defaults)
    }
}

@MainActor
/// 首页位置上报 ViewModel 测试。
final class HomeLocationViewModelTests: XCTestCase {
    /// 测试上报成功后启动倒计时并持久化在线状态。
    func testReportCoordinateStartsCountdown() async {
        let api = MockLocationReportService()
        let store = makeIsolatedStore()
        let viewModel = HomeLocationViewModel(api: api, store: store)
        let toast = ToastCenter()

        let success = await viewModel.reportCoordinate(
            latitude: 30.1,
            longitude: 120.2,
            address: "入口",
            type: .immediate,
            staffId: 77,
            scenicId: 88,
            toast: toast
        )

        XCTAssertTrue(success)
        XCTAssertTrue(viewModel.isOnline)
        XCTAssertEqual(viewModel.secondsUntilReport, 600)
        XCTAssertTrue(store.loadOnlineStatus())
        XCTAssertGreaterThan(store.loadLastReportTime(), 0)
        XCTAssertGreaterThan(store.loadExpireTime(), Int(Date().timeIntervalSince1970))
    }

    /// 测试上报成功后持久化服务端过期时间戳。
    func testReportCoordinatePersistsExpireTimestamp() async {
        let api = MockLocationReportService()
        let expireTime = Int(Date().timeIntervalSince1970) + 7_200
        api.reportExpired = expireTime
        let store = makeIsolatedStore()
        let viewModel = HomeLocationViewModel(api: api, store: store)
        let toast = ToastCenter()

        _ = await viewModel.reportCoordinate(
            latitude: 30.1,
            longitude: 120.2,
            address: "入口",
            type: .immediate,
            staffId: 77,
            scenicId: 88,
            toast: toast
        )

        XCTAssertEqual(store.loadExpireTime(), expireTime)
        XCTAssertEqual(viewModel.secondsUntilReport, 7_200)
    }

    /// 测试 30 秒操作节流。
    func testOperationIntervalBlocksFrequentReports() async {
        let api = MockLocationReportService()
        let viewModel = HomeLocationViewModel(api: api, store: makeIsolatedStore())
        let toast = ToastCenter()

        _ = await viewModel.reportCoordinate(
            latitude: 30.1,
            longitude: 120.2,
            address: "入口",
            type: .immediate,
            staffId: 77,
            scenicId: 88,
            toast: toast
        )
        let second = await viewModel.reportCoordinate(
            latitude: 30.2,
            longitude: 120.3,
            address: "入口2",
            type: .immediate,
            staffId: 77,
            scenicId: 88,
            toast: toast
        )

        XCTAssertFalse(second)
        XCTAssertEqual(api.reportRequests.count, 1)
    }

    /// 测试恢复在线倒计时（兼容旧版仅保存上报时间）。
    func testRestoreStateRecoversCountdown() {
        let store = makeIsolatedStore()
        let now = Date().timeIntervalSince1970 * 1000
        store.saveOnlineStatus(true)
        store.saveLastReportTime(now - 1_000 * 1000)
        store.saveReminderMinutes(10)

        let viewModel = HomeLocationViewModel(api: MockLocationReportService(), store: store)
        viewModel.restoreState()

        XCTAssertTrue(viewModel.isOnline)
        XCTAssertGreaterThan(viewModel.secondsUntilReport, 0)
        XCTAssertEqual(viewModel.reminderMinutes, 10)
    }

    /// 测试下次启动优先使用本地过期时间戳恢复倒计时。
    func testRestoreStateUsesPersistedExpireTime() {
        let store = makeIsolatedStore()
        let expireTime = Int(Date().timeIntervalSince1970) + 3_600
        store.saveOnlineStatus(true)
        store.saveExpireTime(expireTime)
        store.saveLastReportTime(Date().timeIntervalSince1970 * 1000)
        store.saveReminderMinutes(10)

        let viewModel = HomeLocationViewModel(api: MockLocationReportService(), store: store)
        viewModel.restoreState()

        XCTAssertTrue(viewModel.isOnline)
        XCTAssertEqual(viewModel.secondsUntilReport, 3_600)
        XCTAssertEqual(viewModel.reminderMinutes, 10)
    }

    /// 测试在线且倒计时进入提醒窗口时弹出超时对话框。
    func testCheckLocationTimeoutShowsDialogWhenOnlineAndWithinReminderWindow() async {
        let store = makeIsolatedStore()
        let expireTime = Int(Date().timeIntervalSince1970) + 120
        store.saveOnlineStatus(true)
        store.saveExpireTime(expireTime)
        store.saveReminderMinutes(5)

        let viewModel = HomeLocationViewModel(api: MockLocationReportService(), store: store)
        viewModel.restoreState()

        await viewModel.checkLocationTimeout(staffId: 77)

        XCTAssertTrue(viewModel.isOnline)
        XCTAssertTrue(viewModel.showTimeoutDialog)
    }

    /// 测试离线状态不弹出超时对话框。
    func testCheckLocationTimeoutDoesNotShowDialogWhenOffline() async {
        let store = makeIsolatedStore()
        store.saveOnlineStatus(false)
        store.saveReminderMinutes(5)

        let viewModel = HomeLocationViewModel(api: MockLocationReportService(), store: store)
        viewModel.restoreState()

        await viewModel.checkLocationTimeout(staffId: 77)

        XCTAssertFalse(viewModel.showTimeoutDialog)
    }

    /// 测试在线但倒计时未进入提醒窗口时不弹窗。
    func testCheckLocationTimeoutDoesNotShowDialogWhenOutsideReminderWindow() async {
        let store = makeIsolatedStore()
        let expireTime = Int(Date().timeIntervalSince1970) + 3_600
        store.saveOnlineStatus(true)
        store.saveExpireTime(expireTime)
        store.saveReminderMinutes(5)

        let viewModel = HomeLocationViewModel(api: MockLocationReportService(), store: store)
        viewModel.restoreState()

        await viewModel.checkLocationTimeout(staffId: 77)

        XCTAssertTrue(viewModel.isOnline)
        XCTAssertFalse(viewModel.showTimeoutDialog)
    }

    private func makeIsolatedStore() -> LocationStateStore {
        let suiteName = "HomeLocationViewModelTests.\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suiteName)!
        return LocationStateStore(defaults: defaults)
    }
}

/// 位置上报 API 测试用 URLSession。
private final class LocationRecordingSession: URLSessionProtocol {
    let data: Data
    private(set) var requests: [URLRequest] = []

    /// 初始化测试 Session。
    init(data: Data) {
        self.data = data
    }

    /// 记录请求并返回成功响应。
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        requests.append(request)
        let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
        return (data, response)
    }
}

@MainActor
/// 位置上报服务测试替身。
final class MockLocationReportService: LocationReportServing {
    var shouldFailReport = false
    var reportExpired = 600
    var detailResponse = LocationDetailResponse(status: 0, needReport: false, expireTime: 0)
    var reportRequests: [(staffId: Int, latitude: Double, longitude: Double, address: String, type: LocationReportType, scenicId: Int)] = []
    var historyPages: [ListPayload<LocationReportHistoryItem>] = [ListPayload(total: 0, list: [])]
    var historyRequests: [(staffId: Int, page: Int, pageSize: Int, type: LocationReportType, startDate: String?, endDate: String?)] = []

    func reportLocation(staffId: Int, latitude: Double, longitude: Double, address: String, type: LocationReportType, scenicId: Int) async throws -> LocationReportSubmitResponse {
        if shouldFailReport { throw NSError(domain: "location", code: 1) }
        reportRequests.append((staffId, latitude, longitude, address, type, scenicId))
        return LocationReportSubmitResponse(staffId: "\(staffId)", expired: reportExpired, status: 1)
    }

    func locationReportList(staffId: Int, page: Int, pageSize: Int, type: LocationReportType, startDate: String?, endDate: String?) async throws -> ListPayload<LocationReportHistoryItem> {
        historyRequests.append((staffId, page, pageSize, type, startDate, endDate))
        return historyPages.isEmpty ? ListPayload(total: 0, list: []) : historyPages.removeFirst()
    }

    func locationDetail(staffId: Int) async throws -> LocationDetailResponse {
        detailResponse
    }
}

private extension LocationReportHistoryItem {
    /// 创建历史记录测试实体。
    static func fixture(id: Int) -> LocationReportHistoryItem {
        let data = Data(#"{"id":\#(id),"staff_id":77,"type":1,"latitude":"30.1","longitude":"120.2","address":"入口","ip":"","remark":"","created_at":"2026-06-24"}"#.utf8)
        return try! JSONDecoder().decode(LocationReportHistoryItem.self, from: data)
    }
}

/// 从请求中提取 query 字典。
private func locationQueryItems(from request: URLRequest) -> [String: String] {
    guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
        return [:]
    }
    return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in
        item.value.map { (item.name, $0) }
    })
}
