//
//  AssetsAPITests.swift
//  suixinkanTests
//
//  Created by Codex on 2026/6/23.
//

import XCTest
@testable import suixinkan

@MainActor
/// 资产 API 测试，覆盖云盘和素材接口请求以及宽松模型解码。
final class AssetsAPITests: XCTestCase {
    /// 测试云盘列表接口使用正确 path 和 query。
    func testCloudFileListUsesExpectedPathAndQuery() async throws {
        let session = AssetsRecordingURLSession(data: Self.cloudListResponse)
        let api = AssetsAPI(client: APIClient(session: session))

        let payload = try await api.cloudFileList(parentFolderId: 9, name: " 照片 ", type: 2, orderBy: 1, page: 0, pageSize: 0)

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/cloud-driver/list")
        let query = assetsQueryItems(from: request)
        XCTAssertEqual(query["parent_folder_id"], "9")
        XCTAssertEqual(query["name"], " 照片 ")
        XCTAssertEqual(query["type"], "2")
        XCTAssertEqual(query["order_by"], "1")
        XCTAssertEqual(query["page"], "1")
        XCTAssertEqual(query["page_size"], "1")
        XCTAssertEqual(payload.total, 2)
        XCTAssertEqual(payload.list.dropFirst().first?.fileSize, 2048)
    }

    /// 测试云盘写操作使用正确 path 和 body。
    func testCloudMutationRequestsUseExpectedBodies() async throws {
        let session = AssetsRecordingURLSession(data: Self.emptyResponse)
        let api = AssetsAPI(client: APIClient(session: session))

        try await api.cloudFolderCreate(CloudFolderCreateRequest(parentFolderId: 1, name: "新文件夹"))
        try await api.cloudFileUpload(CloudFileUploadRequest(parentFolderId: 1, fileUrl: "https://cdn/a.jpg", fileName: "a.jpg"))
        try await api.cloudFileDelete(CloudFileDeleteRequest(list: [CloudFileActionItem(id: 2, type: 99)]))
        try await api.cloudFileMove(CloudFileMoveRequest(targetFolderId: 3, list: [CloudFileActionItem(id: 2, type: 2)]))
        try await api.cloudFolderEdit(CloudFolderModifyRequest(id: 2, name: "改名"))
        try await api.cloudFileEdit(CloudFileModifyRequest(id: 4, fileName: "b.jpg"))

        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/yf-handset-app/photog/cloud-driver/folder-create",
            "/api/yf-handset-app/photog/cloud-driver/file-upload",
            "/api/yf-handset-app/photog/cloud-driver/delete",
            "/api/yf-handset-app/photog/cloud-driver/move",
            "/api/yf-handset-app/photog/cloud-driver/folder-edit",
            "/api/yf-handset-app/photog/cloud-driver/file-edit"
        ])
        let uploadBody = try bodyObject(from: session.requests[1])
        XCTAssertEqual(uploadBody["parent_folder_id"] as? Int, 1)
        XCTAssertEqual(uploadBody["file_url"] as? String, "https://cdn/a.jpg")
        let moveBody = try bodyObject(from: session.requests[3])
        XCTAssertEqual(moveBody["target_folder_id"] as? Int, 3)
    }

    /// 测试云盘上传权限接口兼容字符串布尔值。
    func testCloudUploadPermissionDecodesLossyFields() async throws {
        let session = AssetsRecordingURLSession(data: Self.permissionResponse)
        let api = AssetsAPI(client: APIClient(session: session))

        let permission = try await api.checkCloudUploadPermission()

        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/cloud-driver/check-upload-permission")
        XCTAssertFalse(permission.canUpload)
        XCTAssertEqual(permission.reason, "空间不足")
    }

    /// 测试素材列表和详情接口使用正确 path 并宽松解码字段。
    func testMediaListAndDetailDecodeLossyFields() async throws {
        let session = AssetsRecordingURLSession(responses: [Self.mediaListResponse, Self.mediaDetailResponse])
        let api = AssetsAPI(client: APIClient(session: session))

        let list = try await api.mediaAlbumList(kind: .material, keyword: "航拍", status: 1, page: 0, pageSize: 0)
        let detail = try await api.mediaAlbumDetail(id: 7, kind: .material)

        XCTAssertEqual(session.requests.first?.url?.path, "/api/app/media-album/list")
        let query = assetsQueryItems(from: try XCTUnwrap(session.requests.first))
        XCTAssertEqual(query["type"], "1")
        XCTAssertEqual(query["status"], "1")
        XCTAssertEqual(query["page"], "1")
        XCTAssertEqual(query["page_size"], "1")
        XCTAssertEqual(list.order?.totalNum, 12)
        XCTAssertEqual(list.list.first?.likesCount, 3)
        XCTAssertEqual(session.requests.last?.url?.path, "/api/app/media-album/detail")
        XCTAssertEqual(assetsQueryItems(from: try XCTUnwrap(session.requests.last))["type"], "1")
        XCTAssertEqual(detail.mediaList.first?.size, 4096)
        XCTAssertTrue(detail.mediaList.first?.isVideo == true)
    }

    /// 测试素材写操作 path 和请求体。
    func testMediaMutationRequestsUseExpectedBodies() async throws {
        let session = AssetsRecordingURLSession(data: Self.emptyResponse)
        let api = AssetsAPI(client: APIClient(session: session))
        let upload = MediaAlbumUploadRequest(
            name: "素材",
            type: 1,
            mediaType: 2,
            mediaList: [MediaAlbumUploadItem(originalName: "a.jpg", ossUrl: "https://cdn/a.jpg", size: 10, fileWidthSize: MediaAlbumFileSize(width: 100, height: 80))],
            coverUrl: "https://cdn/c.jpg",
            coverSize: MediaAlbumFileSize(width: 100, height: 80),
            scenicSpotId: 6,
            description: "描述",
            materialTag: "航拍",
            projectId: 0
        )

        try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: 1, type: 1, listingStatus: 1))
        try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 1, type: 1))
        try await api.mediaAlbumAddTag(MediaAlbumAddTagRequest(name: "航拍"))
        try await api.mediaAlbumUpload(upload)
        try await api.mediaAlbumEdit(MediaAlbumEditRequest(id: 2, name: "素材", type: 1, mediaType: 2, mediaList: upload.mediaList, coverUrl: upload.coverUrl, coverSize: upload.coverSize, scenicSpotId: 6, description: "描述", materialTag: "航拍"))

        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/app/media-album/operation",
            "/api/app/media-album/delete",
            "/api/app/media-album/add-tag",
            "/api/app/media-album/upload",
            "/api/app/media-album/edit"
        ])
        let body = try bodyObject(from: session.requests[3])
        XCTAssertEqual(body["scenic_spot_id"] as? Int, 6)
        XCTAssertEqual((body["media_list"] as? [[String: Any]])?.first?["oss_url"] as? String, "https://cdn/a.jpg")
    }

    /// 测试样片请求携带 type=2，项目列表接口和项目字段宽松解码正确。
    func testSampleRequestsAndProjectListUseExpectedTypeAndQuery() async throws {
        let session = AssetsRecordingURLSession(responses: [
            Self.mediaListResponse,
            Self.mediaDetailResponse,
            Self.emptyResponse,
            Self.emptyResponse,
            Self.projectListResponse,
            Self.emptyResponse
        ])
        let api = AssetsAPI(client: APIClient(session: session))
        let upload = MediaAlbumUploadRequest(
            name: "亲子样片",
            type: 2,
            mediaType: 2,
            mediaList: [MediaAlbumUploadItem(originalName: "sample.jpg", ossUrl: "https://cdn/sample.jpg", size: 10, fileWidthSize: MediaAlbumFileSize(width: 100, height: 80))],
            coverUrl: "https://cdn/cover.jpg",
            coverSize: MediaAlbumFileSize(width: 100, height: 80),
            scenicSpotId: 6,
            description: "描述",
            materialTag: "",
            projectId: 88
        )

        _ = try await api.mediaAlbumList(kind: .sample, keyword: "样片", status: nil, page: 1, pageSize: 10)
        _ = try await api.mediaAlbumDetail(id: 7, kind: .sample)
        try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: 7, type: 2, listingStatus: 1))
        try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: 7, type: 2))
        let projects = try await api.projectList(scenicId: 9, name: "亲子", page: 0, pageSize: 0)
        try await api.mediaAlbumUpload(upload)

        XCTAssertEqual(assetsQueryItems(from: session.requests[0])["type"], "2")
        XCTAssertEqual(assetsQueryItems(from: session.requests[1])["type"], "2")
        XCTAssertEqual((try bodyObject(from: session.requests[2]))["type"] as? Int, 2)
        XCTAssertEqual((try bodyObject(from: session.requests[3]))["type"] as? Int, 2)
        XCTAssertEqual(session.requests[4].url?.path, "/api/yf-handset-app/photog/project/list")
        let projectQuery = assetsQueryItems(from: session.requests[4])
        XCTAssertEqual(projectQuery["scenic_id"], "9")
        XCTAssertEqual(projectQuery["name"], "亲子")
        XCTAssertEqual(projectQuery["page"], "1")
        XCTAssertEqual(projectQuery["page_size"], "1")
        XCTAssertEqual(projects.list.first?.id, 88)
        XCTAssertEqual(projects.list.first?.name, "亲子旅拍")
        XCTAssertEqual((try bodyObject(from: session.requests[5]))["project_id"] as? Int, 88)
    }

    /// 测试相册接口使用旧工程路径、query 和请求体。
    func testAlbumRequestsUseExpectedPathQueryAndBodies() async throws {
        let session = AssetsRecordingURLSession(responses: [
            Self.albumFolderListResponse,
            Self.albumFolderInfoResponse,
            Self.albumFileListResponse,
            Self.emptyResponse,
            Self.emptyResponse,
            Self.emptyResponse,
            Self.emptyResponse
        ])
        let api = AssetsAPI(client: APIClient(session: session))

        let folders = try await api.albumFolderList(
            scenicId: 9,
            page: 0,
            pageSize: 0,
            name: "旅拍",
            startTime: "2026-06-01",
            endTime: "2026-06-24"
        )
        let info = try await api.albumFolderInfo(id: 3)
        let files = try await api.albumFileList(scenicId: 9, folderId: 3, fileType: 2, page: 0, pageSize: 0)
        try await api.addAlbumFolder(scenicId: 9, name: "新相册", remark: "备注")
        try await api.editAlbumFolder(folderId: 3, coverFileId: 11, name: "改名", remark: nil)
        try await api.deleteAlbumFiles(folderId: 3, idList: [11, 12])
        try await api.albumFileUploadURL(scenicId: 9, fileURL: "https://cdn/a.jpg", folderId: 3)

        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/yf-handset-app/photog/album/folder-list",
            "/api/yf-handset-app/photog/album/folder-info",
            "/api/yf-handset-app/photog/album/file-list",
            "/api/yf-handset-app/photog/album/folder-add",
            "/api/yf-handset-app/photog/album/folder-edit",
            "/api/yf-handset-app/photog/album/file-delete",
            "/api/yf-handset-app/photog/album/file-upload-url"
        ])
        let folderQuery = assetsQueryItems(from: session.requests[0])
        XCTAssertEqual(folderQuery["scenic_id"], "9")
        XCTAssertEqual(folderQuery["page"], "1")
        XCTAssertEqual(folderQuery["page_size"], "1")
        XCTAssertEqual(folderQuery["cloud_folder_type"], "1")
        XCTAssertEqual(folderQuery["name"], "旅拍")
        XCTAssertEqual(folderQuery["start_time"], "2026-06-01")
        XCTAssertEqual(folderQuery["end_time"], "2026-06-24")
        XCTAssertEqual(folders.list.first?.countImage, 4)
        XCTAssertEqual(info.coverFile?.fileSize, 2048)
        XCTAssertTrue(files.list.first?.isImage == true)

        let addBody = try bodyObject(from: session.requests[3])
        XCTAssertEqual(addBody["scenic_id"] as? String, "9")
        XCTAssertEqual(addBody["cloud_folder_type"] as? Int, 1)
        let editBody = try bodyObject(from: session.requests[4])
        XCTAssertEqual(editBody["cover_file_id"] as? Int, 11)
        let deleteBody = try bodyObject(from: session.requests[5])
        XCTAssertEqual(deleteBody["folder_id"] as? Int, 3)
        XCTAssertEqual(deleteBody["id_list"] as? [Int], [11, 12])
        let uploadQuery = assetsQueryItems(from: session.requests[6])
        XCTAssertEqual(uploadQuery["file_url"], "https://cdn/a.jpg")
    }

    fileprivate static let emptyResponse = #"{"code":100000,"msg":"success","data":{}}"#.data(using: .utf8)!
    fileprivate static let permissionResponse = #"{"code":100000,"msg":"success","data":{"can_upload":"0","reason":"空间不足"}}"#.data(using: .utf8)!
    fileprivate static let cloudListResponse = """
    {"code":100000,"msg":"success","data":{"total":"2","list":[
    {"id":"1","parent_folder_id":0,"file_url":"","cover_url":"","updated_at":"2026","name":"文件夹","created_at":"2026","child_num":"3","type":"99","file_size":"0"},
    {"id":2,"parent_folder_id":"0","file_url":"https://cdn/a.jpg","cover_url":"","updated_at":"2026","name":"a.jpg","created_at":"2026","child_num":0,"type":2,"file_size":"2048"}
    ]}}
    """.data(using: .utf8)!
    fileprivate static let mediaListResponse = """
    {"code":100000,"msg":"success","data":{"total":"1","order":{"total_num":"12","avg_order_amount":"8.5","refund_total":1,"avg_change":"0.2"},"list":[
    {"id":"7","name":"航拍","cover_url":"https://cdn/c.jpg","created_at":"2026","status":"1","download_count":"2","likes_count":"3","collect_count":"4","share_count":"5","scenic_spot_name":"东门","project_name":"项目","listing_status":"1"}
    ]}}
    """.data(using: .utf8)!
    fileprivate static let mediaDetailResponse = """
    {"code":100000,"msg":"success","data":{"id":"7","name":"航拍","cover_url":"https://cdn/c.jpg","description":"描述","status":"1","listing_status":"1","likes_count":"3","download_count":"2","collect_count":"4","share_count":"5","uploader_name":"摄影师","created_at":"2026","scenic_name":"景区","project_name":"项目","media_list":[
    {"id":"9","original_name":"v.mp4","oss_url":"https://cdn/v.mp4","thumbnail_url":"","type":"1","size":"4096","show_url":"https://cdn/v.mp4"}
    ]}}
    """.data(using: .utf8)!
    fileprivate static let projectListResponse = """
    {"code":100000,"msg":"success","data":{"total":"1","list":[
    {"id":"88","type":"19","type_name":"多点旅拍","status":"1","status_name":"上架","name":"亲子旅拍","cover_project":"https://cdn/p.jpg","cover_video":"","price":"299","ot_price":"399","price_deposit":"99"}
    ]}}
    """.data(using: .utf8)!
    fileprivate static let albumFolderListResponse = """
    {"code":100000,"msg":"success","data":{"total":"1","list":[
    {"id":"3","name":"旅拍","cover_file_id":"11","count_video":"2","count_image":"4","cover":{"id":"11","file_name":"a.jpg","file_type":"2","file_url":"https://cdn/a.jpg","cover_url":"","file_size":"2048","file_size_human":"2KB","remark":""},"created_at":"2026","remark":"备注"}
    ]}}
    """.data(using: .utf8)!
    fileprivate static let albumFolderInfoResponse = """
    {"code":100000,"msg":"success","data":{"id":"3","name":"旅拍","cover_file_id":"11","count_video":"2","count_image":"4","cover":{"id":"11","file_name":"a.jpg","file_type":"2","file_url":"https://cdn/a.jpg","cover_url":"","file_size":"2048","file_size_human":"2KB","remark":""},"created_at":"2026","remark":"备注"}}
    """.data(using: .utf8)!
    fileprivate static let albumFileListResponse = """
    {"code":100000,"msg":"success","data":{"total":"1","list":[
    {"id":"11","file_name":"a.jpg","file_type":"2","file_url":"https://cdn/a.jpg","cover_url":"","file_size":"2048","file_size_human":"2KB","remark":""}
    ]}}
    """.data(using: .utf8)!
}

/// 资产 API 测试用 URLSession，记录请求并返回固定响应。
private final class AssetsRecordingURLSession: URLSessionProtocol {
    private var responses: [Data]
    private(set) var requests: [URLRequest] = []

    /// 初始化单一响应。
    init(data: Data) {
        responses = [data]
    }

    /// 初始化顺序响应。
    init(responses: [Data]) {
        self.responses = responses
    }

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

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

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