//
//  ProfileSpaceTests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/29.
//

import XCTest
@testable import suixinkan

@MainActor
/// 个人空间配置测试，覆盖 API、ViewModel 校验和首页路由接入。
final class ProfileSpaceTests: XCTestCase {
    /// 测试个人空间信息接口路径、查询参数和响应解码。
    func testProfileSpaceInfoAPIRequestsExpectedEndpoint() async throws {
        let session = ProfileSpaceURLSession(responses: [Self.profileInfoEnvelope])
        let api = ProfileAPI(client: APIClient(session: session))

        let response = try await api.profileSpaceInfo(scenicId: 9)

        XCTAssertEqual(response.nickname, "云端摄影师")
        XCTAssertEqual(response.cameraDevice, ["Sony A7M4"])
        XCTAssertEqual(response.schedule["2026-06-29"]?.first?.name, "上午拍摄")
        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "GET")
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/profile/info")
        let queryItems = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?.queryItems
        XCTAssertEqual(queryItems?.first(where: { $0.name == "scenic_id" })?.value, "9")
    }

    /// 测试个人空间更新接口使用正确路径和请求体。
    func testUpdateProfileSpaceAPIEncodesExpectedBody() async throws {
        let session = ProfileSpaceURLSession(responses: [Self.emptyEnvelope])
        let api = ProfileAPI(client: APIClient(session: session))
        let request = UpdateProfileSpaceRequest(
            scenicId: "9",
            description: "简介",
            cameraDevice: ["Sony"],
            attrLabel: ["亲和"],
            shootLabel: ["夜景"],
            businessStartTime: "09:00:00",
            businessEndTime: "18:00:00",
            holidayStartTime: "10:00:00",
            holidayEndTime: "20:00:00",
            businessRange: [],
            acceptOrderStatus: 1
        )

        try await api.updateProfileSpaceInfo(request)

        let urlRequest = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(urlRequest.httpMethod, "POST")
        XCTAssertEqual(urlRequest.url?.path, "/api/yf-handset-app/photog/profile/info-update")
        let body = try XCTUnwrap(urlRequest.httpBody)
        let json = try JSONSerialization.jsonObject(with: body) as? [String: Any]
        XCTAssertEqual(json?["scenic_id"] as? String, "9")
        XCTAssertEqual(json?["description"] as? String, "简介")
        XCTAssertEqual(json?["accept_order_status"] as? Int, 1)
        XCTAssertEqual(json?["business_range"] as? [Int], [])
    }

    /// 测试加载成功后回填页面状态。
    func testViewModelLoadAppliesProfileSpaceState() async throws {
        let api = MockProfileSpaceAPI()
        let viewModel = ProfileSpaceViewModel()

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

        XCTAssertEqual(viewModel.nickname, "云端摄影师")
        XCTAssertEqual(viewModel.attrLabels, ["亲和", "构图"])
        XCTAssertEqual(viewModel.cameraDevices, ["Sony A7M4"])
        XCTAssertEqual(viewModel.acceptOrderStatus, 1)
        XCTAssertEqual(viewModel.scheduleMap["2026-06-29"]?.count, 1)
        XCTAssertFalse(viewModel.hasChanges)
    }

    /// 测试缺少景区时不发请求并展示错误。
    func testViewModelLoadWithoutScenicDoesNotRequest() async {
        let api = MockProfileSpaceAPI()
        let viewModel = ProfileSpaceViewModel()

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

        XCTAssertEqual(api.infoRequests, [])
        XCTAssertEqual(viewModel.errorMessage, "当前缺少景区信息")
    }

    /// 测试标签校验覆盖空值、重复、超长和数量边界。
    func testViewModelValidatesLabels() async throws {
        let viewModel = ProfileSpaceViewModel()

        XCTAssertThrowsError(try viewModel.addAttrLabel(" ")) { error in
            XCTAssertEqual(error as? ProfileSpaceValidationError, .emptyLabel)
        }

        try viewModel.addAttrLabel("亲和")
        XCTAssertThrowsError(try viewModel.addAttrLabel("亲和")) { error in
            XCTAssertEqual(error as? ProfileSpaceValidationError, .duplicateLabel)
        }
        XCTAssertThrowsError(try viewModel.addAttrLabel(String(repeating: "长", count: 21))) { error in
            XCTAssertEqual(error as? ProfileSpaceValidationError, .labelTooLong)
        }

        for index in 0..<7 {
            try viewModel.addAttrLabel("标签\(index)")
        }
        XCTAssertThrowsError(try viewModel.addAttrLabel("第九个")) { error in
            XCTAssertEqual(error as? ProfileSpaceValidationError, .labelLimit)
        }
    }

    /// 测试营业时间必须开始早于结束。
    func testViewModelValidatesBusinessTimes() async throws {
        let api = MockProfileSpaceAPI()
        let viewModel = ProfileSpaceViewModel()
        await viewModel.load(api: api, scenicId: 9)
        viewModel.businessStartTime = Self.time(hour: 18, minute: 0)
        viewModel.businessEndTime = Self.time(hour: 9, minute: 0)

        XCTAssertThrowsError(try viewModel.makeUpdateRequest(scenicId: 9)) { error in
            XCTAssertEqual(error as? ProfileSpaceValidationError, .invalidWorkdayTime)
        }
    }

    /// 测试保存成功后清除 dirty 状态并提交更新请求。
    func testViewModelSaveClearsDirtyState() async throws {
        let api = MockProfileSpaceAPI()
        let uploader = ProfileSpaceMockUploader()
        let viewModel = ProfileSpaceViewModel()
        await viewModel.load(api: api, scenicId: 9)
        viewModel.introduction = "新的简介"

        try await viewModel.save(api: api, uploader: uploader, scenicId: 9)

        XCTAssertEqual(api.updateRequests.count, 1)
        XCTAssertEqual(api.updateRequests.first?.description, "新的简介")
        XCTAssertFalse(viewModel.hasChanges)
    }

    /// 测试保存失败时保留编辑内容并暴露错误。
    func testViewModelSaveFailureKeepsDraft() async throws {
        let api = MockProfileSpaceAPI()
        let uploader = ProfileSpaceMockUploader()
        let viewModel = ProfileSpaceViewModel()
        await viewModel.load(api: api, scenicId: 9)
        viewModel.introduction = "保留草稿"
        api.updateError = APIError.serverCode(500, "保存失败")

        do {
            try await viewModel.save(api: api, uploader: uploader, scenicId: 9)
            XCTFail("保存失败应抛出错误")
        } catch {
            XCTAssertEqual(viewModel.introduction, "保留草稿")
            XCTAssertEqual(viewModel.errorMessage, error.localizedDescription)
            XCTAssertTrue(viewModel.hasChanges)
        }
    }

    /// 测试删除日程会调用 ScheduleAPI 并刷新空间配置。
    func testViewModelDeleteScheduleCallsScheduleAPIAndReloads() async throws {
        let api = MockProfileSpaceAPI()
        let scheduleAPI = MockProfileSpaceScheduleAPI()
        let viewModel = ProfileSpaceViewModel()
        await viewModel.load(api: api, scenicId: 9)
        let item = try XCTUnwrap(viewModel.scheduleMap["2026-06-29"]?.first)

        await viewModel.deleteSchedule(item: item, profileAPI: api, scheduleAPI: scheduleAPI, scenicId: 9)

        XCTAssertEqual(scheduleAPI.deletedIds, [item.id])
        XCTAssertEqual(api.infoRequests, [9, 9])
    }

    /// 测试首页空间设置和基本信息路由拆分正确。
    func testHomeRoutesResolveProfileSpaceAndBasicInfoSeparately() {
        XCTAssertEqual(
            HomeMenuRouter.resolve(uri: "space_settings", title: "空间设置"),
            .destination(.profileSpaceSettings)
        )
        XCTAssertEqual(
            HomeMenuRouter.resolve(uri: "basic_info", title: "基本信息"),
            .destination(.profileSpace)
        )
    }

    fileprivate static var profileInfoEnvelope: Data {
        """
        {
          "code": 100000,
          "msg": "success",
          "data": {
            "id": "1",
            "scenic_id": "9",
            "photog_uid": 88,
            "real_name": "测试摄影师",
            "nickname": "云端摄影师",
            "avatar": "https://cdn.example.com/avatar.jpg",
            "scenic_certification": ["官方认证"],
            "description": "擅长旅拍",
            "attr_label": ["亲和", "构图"],
            "shoot_label": ["夜景"],
            "camera_device": ["Sony A7M4"],
            "business_start_time": "09:00:00",
            "business_end_time": "18:00:00",
            "holiday_start_time": "10:00:00",
            "holiday_end_time": "20:00:00",
            "business_range": [1, 2],
            "accept_order_status": 1,
            "schedule": {
              "2026-06-29": [
                {
                  "id": 31,
                  "name": "上午拍摄",
                  "remark": "东门集合",
                  "start_time": "09:00:00",
                  "end_time": "10:00:00",
                  "schedule_date": "2026-06-29",
                  "order_number": "NO123",
                  "user_phone": "13800000000"
                }
              ]
            }
          }
        }
        """.data(using: .utf8)!
    }

    fileprivate static var emptyEnvelope: Data {
        #"{"code":100000,"msg":"success","data":{}}"#.data(using: .utf8)!
    }

    private static func time(hour: Int, minute: Int) -> Date {
        Calendar.current.date(from: DateComponents(hour: hour, minute: minute)) ?? Date()
    }
}

/// 个人空间 API 测试用 URLSession 替身。
private final class ProfileSpaceURLSession: 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)!
        )
    }
}

@MainActor
/// 个人空间 ViewModel 测试用 API 替身。
private final class MockProfileSpaceAPI: ProfileSpaceServing {
    var updateError: Error?
    private(set) var infoRequests: [Int] = []
    private(set) var updateRequests: [UpdateProfileSpaceRequest] = []
    private(set) var nicknameUpdates: [String?] = []
    private(set) var avatarUpdates: [String] = []

    /// 返回固定个人空间配置。
    func profileSpaceInfo(scenicId: Int) async throws -> ProfileSpaceInfoResponse {
        infoRequests.append(scenicId)
        let envelope = try JSONDecoder().decode(APIEnvelope<ProfileSpaceInfoResponse>.self, from: ProfileSpaceTests.profileInfoEnvelope)
        return try XCTUnwrap(envelope.data)
    }

    /// 记录空间配置更新请求。
    func updateProfileSpaceInfo(_ request: UpdateProfileSpaceRequest) async throws {
        if let updateError { throw updateError }
        updateRequests.append(request)
    }

    /// 记录昵称更新请求。
    func updateUserInfo(nickname: String?, password: String?, avatar: String?) async throws {
        nicknameUpdates.append(nickname)
    }

    /// 记录头像 URL 更新请求。
    func updateUserAvatarURL(_ fileURL: String) async throws {
        avatarUpdates.append(fileURL)
    }
}

@MainActor
/// 个人空间日程测试用 API 替身。
private final class MockProfileSpaceScheduleAPI: ScheduleServing {
    private(set) var deletedIds: [Int] = []

    /// 返回空月标记。
    func monthScheduleDays(scenicId: Int, yearMonth: String) async throws -> [String] {
        []
    }

    /// 返回空日程列表。
    func dayScheduleList(scenicId: Int, date: String) async throws -> [ScheduleItem] {
        []
    }

    /// 新增日程测试不使用。
    func addSchedule(_ request: AddScheduleRequest) async throws {}

    /// 记录删除的日程 ID。
    func deleteSchedule(id: Int) async throws {
        deletedIds.append(id)
    }

    /// 返回空可关联订单。
    func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
        []
    }
}

@MainActor
/// 个人空间上传测试替身。
private final class ProfileSpaceMockUploader: OSSUploadServing {
    /// 上传头像并返回固定 URL。
    func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        onProgress(100)
        return "https://cdn.example.com/new-avatar.jpg"
    }

    /// 实名图片上传测试不使用。
    func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 云盘上传测试不使用。
    func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 相册上传测试不使用。
    func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 任务附件上传测试不使用。
    func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 项目图片上传测试不使用。
    func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 打卡点图片上传测试不使用。
    func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 景区申请图片上传测试不使用。
    func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }

    /// 银行卡图片上传测试不使用。
    func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
}
