//
//  StatisticsViewModelTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/22.
//

import XCTest
@testable import suixinkan

@MainActor
/// 数据统计 ViewModel 测试，覆盖汇总、日数据、分页和角色接口选择。
final class StatisticsViewModelTests: XCTestCase {
    /// 测试首屏会加载汇总和第一页日数据。
    func testReloadLoadsSummaryAndDailyItems() async throws {
        let api = MockStatisticsService()
        api.summaryResponses = [try TestFixture.payload(StatisticsSummaryResponse.self, named: "statistics_summary_success")]
        api.dailyResponses = [try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3")]
        let viewModel = StatisticsViewModel()

        try await viewModel.reload(api: api, scenicId: 88, roleId: nil)

        XCTAssertEqual(viewModel.summary.orderCount, 18)
        XCTAssertEqual(viewModel.dailyItems.map(\.date), ["2026-05-23"])
        XCTAssertEqual(viewModel.totalDailyCount, 3)
        XCTAssertTrue(viewModel.hasMore)
        XCTAssertFalse(viewModel.loading)
        XCTAssertNotNil(viewModel.lastRefreshAt)
        XCTAssertEqual(api.summaryCalls.map(\.range), ["1"])
        XCTAssertEqual(api.dailyCalls.map(\.page), [1])
    }

    /// 测试切换时间段会重置分页并使用对应 range。
    func testSelectPeriodResetsPaginationAndUsesExpectedRange() async throws {
        let api = MockStatisticsService()
        api.summaryResponses = [
            try TestFixture.payload(StatisticsSummaryResponse.self, named: "statistics_summary_success"),
            try TestFixture.payload(StatisticsSummaryResponse.self, named: "statistics_summary_success")
        ]
        api.dailyResponses = [
            try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3"),
            try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page2_total3"),
            try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3")
        ]
        let viewModel = StatisticsViewModel()

        try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
        try await viewModel.loadMore(api: api, scenicId: 88, roleId: nil)
        XCTAssertEqual(viewModel.dailyItems.count, 3)

        try await viewModel.selectPeriod(.sevenDays, api: api, scenicId: 88, roleId: nil)

        XCTAssertEqual(viewModel.selectedPeriod, .sevenDays)
        XCTAssertEqual(viewModel.dailyItems.map(\.date), ["2026-05-23"])
        XCTAssertEqual(viewModel.totalDailyCount, 3)
        XCTAssertTrue(viewModel.hasMore)
        XCTAssertEqual(api.summaryCalls.last?.range, "3")
        XCTAssertEqual(api.dailyCalls.last?.page, 1)
        XCTAssertFalse(api.dailyCalls.last?.startTime.isEmpty ?? true)
        XCTAssertFalse(api.dailyCalls.last?.endTime.isEmpty ?? true)
    }

    /// 测试加载更多会追加日数据。
    func testLoadMoreAppendsDailyItems() async throws {
        let api = MockStatisticsService()
        api.summaryResponses = [try TestFixture.payload(StatisticsSummaryResponse.self, named: "statistics_summary_success")]
        api.dailyResponses = [
            try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3"),
            try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page2_total3")
        ]
        let viewModel = StatisticsViewModel()

        try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
        try await viewModel.loadMore(api: api, scenicId: 88, roleId: nil)

        XCTAssertEqual(viewModel.dailyItems.map(\.date), ["2026-05-23", "2026-05-22", "2026-05-21"])
        XCTAssertFalse(viewModel.hasMore)
        XCTAssertFalse(viewModel.loadingMore)
        XCTAssertEqual(api.dailyCalls.map(\.page), [1, 2])
    }

    /// 测试加载更多失败时保留已有数据和下一页能力。
    func testLoadMoreFailureKeepsCurrentItems() async throws {
        let api = MockStatisticsService()
        api.summaryResponses = [try TestFixture.payload(StatisticsSummaryResponse.self, named: "statistics_summary_success")]
        api.dailyResponses = [try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3")]
        let viewModel = StatisticsViewModel()

        try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
        api.nextDailyError = APIError.httpStatus(500, "server error")

        do {
            try await viewModel.loadMore(api: api, scenicId: 88, roleId: nil)
            XCTFail("Expected load more to fail")
        } catch APIError.httpStatus(let statusCode, _) {
            XCTAssertEqual(statusCode, 500)
        } catch {
            XCTFail("Unexpected error: \(error)")
        }

        XCTAssertEqual(viewModel.dailyItems.map(\.date), ["2026-05-23"])
        XCTAssertTrue(viewModel.hasMore)
        XCTAssertFalse(viewModel.loadingMore)
    }

    /// 测试无景区时清空旧统计状态。
    func testReloadWithoutScenicClearsStaleStatisticsState() async throws {
        let api = MockStatisticsService()
        api.summaryResponses = [try TestFixture.payload(StatisticsSummaryResponse.self, named: "statistics_summary_success")]
        api.dailyResponses = [try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3")]
        let viewModel = StatisticsViewModel()

        try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
        XCTAssertEqual(viewModel.summary.orderCount, 18)

        try await viewModel.reload(api: api, scenicId: nil, roleId: nil)

        XCTAssertEqual(api.summaryCalls.count, 1)
        XCTAssertEqual(viewModel.summary.orderCount, 0)
        XCTAssertTrue(viewModel.dailyItems.isEmpty)
        XCTAssertEqual(viewModel.totalDailyCount, 0)
        XCTAssertFalse(viewModel.hasMore)
        XCTAssertNil(viewModel.lastRefreshAt)
    }

    /// 测试景区管理员角色使用管理员统计接口。
    func testStatisticsAPIUsesAdminEndpointForScenicAdminRole() async throws {
        let session = SequencedURLSession(responses: [
            try TestFixture.data(named: "statistics_summary_success"),
            try TestFixture.data(named: "statistics_daily_page1_total3")
        ])
        let api = StatisticsAPI(client: APIClient(session: session))

        _ = try await api.summary(scenicId: 88, range: "1", isScenicAdmin: true)
        _ = try await api.dailyList(
            scenicId: 88,
            startTime: "2026-05-01",
            endTime: "2026-05-31",
            page: 2,
            pageSize: 15,
            isScenicAdmin: true
        )

        XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/app/scenic-admin/analyse", "/api/app/scenic-admin/analyse/daily"])
        let dailyQuery = queryItems(from: try XCTUnwrap(session.requests.last))
        XCTAssertEqual(dailyQuery["scenic_id"], "88")
        XCTAssertEqual(dailyQuery["page"], "2")
        XCTAssertEqual(dailyQuery["start_time"], "2026-05-01")
        XCTAssertEqual(dailyQuery["end_time"], "2026-05-31")
    }
}

@MainActor
/// 数据统计服务测试替身，记录请求参数并返回预设响应。
private final class MockStatisticsService: StatisticsServing {
    struct SummaryCall: Equatable {
        let scenicId: Int
        let range: String
        let isScenicAdmin: Bool
    }

    struct DailyCall: Equatable {
        let scenicId: Int
        let startTime: String
        let endTime: String
        let page: Int
        let pageSize: Int
        let isScenicAdmin: Bool
    }

    var summaryResponses: [StatisticsSummaryResponse] = []
    var dailyResponses: [DataListPayload<StatisticsDailyItem>] = []
    var nextDailyError: Error?
    private(set) var summaryCalls: [SummaryCall] = []
    private(set) var dailyCalls: [DailyCall] = []

    /// 返回测试汇总数据。
    func summary(scenicId: Int, range: String, isScenicAdmin: Bool) async throws -> StatisticsSummaryResponse {
        summaryCalls.append(SummaryCall(scenicId: scenicId, range: range, isScenicAdmin: isScenicAdmin))
        return summaryResponses.isEmpty ? StatisticsSummaryResponse() : summaryResponses.removeFirst()
    }

    /// 返回测试每日明细数据。
    func dailyList(
        scenicId: Int,
        startTime: String,
        endTime: String,
        page: Int,
        pageSize: Int,
        isScenicAdmin: Bool
    ) async throws -> DataListPayload<StatisticsDailyItem> {
        dailyCalls.append(
            DailyCall(
                scenicId: scenicId,
                startTime: startTime,
                endTime: endTime,
                page: page,
                pageSize: pageSize,
                isScenicAdmin: isScenicAdmin
            )
        )
        if let nextDailyError {
            self.nextDailyError = nil
            throw nextDailyError
        }
        return dailyResponses.isEmpty ? DataListPayload(total: 0, data: []) : dailyResponses.removeFirst()
    }
}

/// URLSession 顺序响应测试替身。
private final class SequencedURLSession: URLSessionProtocol {
    private var responses: [Data]
    private(set) var requests: [URLRequest] = []

    init(responses: [Data]) {
        self.responses = responses
    }

    /// 依次返回预设响应。
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        requests.append(request)
        let data = responses.isEmpty ? Data() : responses.removeFirst()
        return (
            data,
            HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
        )
    }
}

private func queryItems(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) }
    })
}
