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

import XCTest
@testable import suixinkan

@MainActor
/// 排班 API 和 ViewModel 测试。
final class ScheduleTests: XCTestCase {
    /// 测试排班 API path 和 query/body。
    func testScheduleAPIUsesExpectedRequests() async throws {
        let session = ScheduleRecordingURLSession(responses: [
            Data(#"{"code":100000,"message":"ok","data":["2026-06-24"]}"#.utf8),
            Data(#"{"code":100000,"message":"ok","data":[{"id":"1","name":"拍摄","start_time":"09:00","end_time":"10:00","schedule_date":"2026-06-24"}]}"#.utf8),
            Data(#"{"code":100000,"message":"ok","data":{}}"#.utf8),
            Data(#"{"code":100000,"message":"ok","data":{}}"#.utf8)
        ])
        let api = ScheduleAPI(client: APIClient(session: session))

        let days = try await api.monthScheduleDays(scenicId: 9, yearMonth: "2026-06")
        let list = try await api.dayScheduleList(scenicId: 9, date: "2026-06-24")
        try await api.addSchedule(AddScheduleRequest(scenicId: "9", name: "拍摄", remark: "备注", startTime: "09:00", endTime: "10:00", scheduleDate: "2026-06-24", orderNumber: "ORD001"))
        try await api.deleteSchedule(id: 1)

        XCTAssertEqual(days, ["2026-06-24"])
        XCTAssertEqual(list.first?.id, 1)
        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/yf-handset-app/photog/schedule/list-date",
            "/api/yf-handset-app/photog/schedule/list",
            "/api/yf-handset-app/photog/schedule/add",
            "/api/yf-handset-app/photog/schedule/delete"
        ])
        XCTAssertEqual(scheduleQueryItems(from: session.requests[0])["date"], "2026-06")
        XCTAssertEqual(scheduleQueryItems(from: session.requests[1])["date"], "2026-06-24")
        XCTAssertEqual((try scheduleBodyObject(from: session.requests[2]))["order_number"] as? String, "ORD001")
        XCTAssertEqual((try scheduleBodyObject(from: session.requests[3]))["id"] as? Int, 1)
    }

    /// 测试无景区时排班管理清空状态且不请求接口。
    func testScheduleManagementWithoutScenicClearsState() async {
        let api = FakeScheduleService()
        let viewModel = ScheduleManagementViewModel()

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

        XCTAssertTrue(viewModel.items.isEmpty)
        XCTAssertTrue(viewModel.markedDays.isEmpty)
        XCTAssertEqual(api.monthCalls.count, 0)
    }

    /// 测试排班管理加载月份和当天列表。
    func testScheduleManagementReloadLoadsMonthAndDay() async {
        let api = FakeScheduleService()
        api.monthDays = ["2026-06-24"]
        api.dayItems = [.fixture(id: 1)]
        let viewModel = ScheduleManagementViewModel()

        await viewModel.reload(api: api, scenicId: 9)

        XCTAssertEqual(api.monthCalls.first?.scenicId, 9)
        XCTAssertEqual(api.dayCalls.first?.scenicId, 9)
        XCTAssertEqual(viewModel.markedDays, ["2026-06-24"])
        XCTAssertEqual(viewModel.items.first?.id, 1)
    }

    /// 测试新增排班优先使用选择的订单号。
    func testScheduleAddUsesSelectedOrderBeforeManualOrder() async {
        let api = FakeScheduleService()
        let viewModel = ScheduleAddViewModel()
        viewModel.draft.name = "拍摄"
        viewModel.draft.remark = ""
        viewModel.draft.manualOrderNumber = "MANUAL"
        viewModel.draft.selectedOrder = AvailableOrderResponse.fixture(orderNumber: "PICKED")
        let start = fixedDate(hour: 9)
        let end = fixedDate(hour: 10)

        let success = await viewModel.submit(api: api, scenicId: 9, startDate: start, endDate: end)

        XCTAssertTrue(success)
        XCTAssertEqual(api.addedRequests.first?.orderNumber, "PICKED")
        XCTAssertEqual(api.addedRequests.first?.remark, "拍摄")
    }

    /// 测试结束时间早于开始时间时禁止提交。
    func testScheduleAddRejectsInvalidTime() async {
        let api = FakeScheduleService()
        let viewModel = ScheduleAddViewModel()
        viewModel.draft.name = "拍摄"

        let success = await viewModel.submit(api: api, scenicId: 9, startDate: fixedDate(hour: 10), endDate: fixedDate(hour: 9))

        XCTAssertFalse(success)
        XCTAssertTrue(api.addedRequests.isEmpty)
        XCTAssertEqual(viewModel.errorMessage, ScheduleValidationError.invalidTime.localizedDescription)
    }

    private func fixedDate(hour: Int) -> Date {
        Calendar.current.date(from: DateComponents(year: 2026, month: 6, day: 24, hour: hour, minute: 0)) ?? Date()
    }
}

private extension ScheduleItem {
    /// 创建排班测试项。
    static func fixture(id: Int) -> ScheduleItem {
        let data = Data(#"{"id":\#(id),"name":"拍摄","start_time":"09:00","end_time":"10:00","schedule_date":"2026-06-24"}"#.utf8)
        return try! JSONDecoder().decode(ScheduleItem.self, from: data)
    }
}

private extension AvailableOrderResponse {
    /// 创建可关联订单测试项。
    static func fixture(orderNumber: String) -> AvailableOrderResponse {
        let data = Data(#"{"project_name":"项目","order_number":"\#(orderNumber)","order_status":"1","order_status_label":"已支付","pay_time":"2026-06-24","user_phone":"13800000000"}"#.utf8)
        return try! JSONDecoder().decode(AvailableOrderResponse.self, from: data)
    }
}

/// 排班 API 测试 URLSession。
private final class ScheduleRecordingURLSession: URLSessionProtocol {
    private var responses: [Data]
    private(set) var requests: [URLRequest] = []

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

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

/// 排班服务测试替身。
@MainActor
private final class FakeScheduleService: ScheduleServing {
    var monthDays: [String] = []
    var dayItems: [ScheduleItem] = []
    var monthCalls: [(scenicId: Int, yearMonth: String)] = []
    var dayCalls: [(scenicId: Int, date: String)] = []
    var addedRequests: [AddScheduleRequest] = []

    func monthScheduleDays(scenicId: Int, yearMonth: String) async throws -> [String] {
        monthCalls.append((scenicId, yearMonth))
        return monthDays
    }

    func dayScheduleList(scenicId: Int, date: String) async throws -> [ScheduleItem] {
        dayCalls.append((scenicId, date))
        return dayItems
    }

    func addSchedule(_ request: AddScheduleRequest) async throws {
        addedRequests.append(request)
    }

    func deleteSchedule(id: Int) async throws {}

    func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
        []
    }
}

/// 从请求中提取 query 字典。
private func scheduleQueryItems(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) }
    })
}

/// 将请求体解析为字典。
private func scheduleBodyObject(from request: URLRequest) throws -> [String: Any] {
    let body = try XCTUnwrap(request.httpBody)
    return try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
}
