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

import XCTest
@testable import suixinkan

@MainActor
/// 打卡点 API 测试，覆盖旧工程接口路径、参数和宽松解码。
final class PunchPointAPITests: XCTestCase {
    /// 测试打卡点列表接口使用正确 path 和 query。
    func testPunchPointListUsesExpectedPathAndQuery() async throws {
        let session = PunchPointRecordingSession(data: Self.listResponse)
        let api = PunchPointAPI(client: APIClient(session: session))

        let payload = try await api.punchPointList(scenicId: 88, status: -1, page: 0, pageSize: 0)

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/scenic-spot/list")
        let query = queryItems(from: request)
        XCTAssertEqual(query["scenic_area_id"], "88")
        XCTAssertEqual(query["status"], "0")
        XCTAssertEqual(query["page"], "1")
        XCTAssertEqual(query["page_size"], "1")
        XCTAssertEqual(payload.total, 1)
        XCTAssertEqual(payload.list.first?.id, 7)
        XCTAssertEqual(payload.list.first?.region?.lat, 30.1)
    }

    /// 测试详情、新增、编辑、删除接口路径和请求体。
    func testPunchPointMutationRequestsUseExpectedBodies() async throws {
        let session = PunchPointRecordingSession(responses: [Self.infoResponse, Self.emptyResponse, Self.emptyResponse, Self.emptyResponse])
        let api = PunchPointAPI(client: APIClient(session: session))
        let region = PunchPointRegion(lat: 30.1, lot: 120.2, address: "入口", scenicSpotStr: "入口点")

        _ = try await api.punchPointInfo(id: 7)
        try await api.addPunchPoint(AddPunchPointRequest(scenicAreaId: "88", name: "入口", description: "描述", region: region, scenicSpotStr: "入口点", guideImages: ["https://cdn/a.jpg"]))
        try await api.editPunchPoint(EditPunchPointRequest(id: 7, scenicAreaId: "88", name: "入口2", description: "描述2", region: region, scenicSpotStr: "入口点", guideImages: []))
        try await api.deletePunchPoint(id: 7)

        XCTAssertEqual(session.requests.map { $0.url?.path }, [
            "/api/yf-handset-app/photog/scenic-spot/info",
            "/api/yf-handset-app/photog/scenic-spot/add",
            "/api/yf-handset-app/photog/scenic-spot/edit",
            "/api/yf-handset-app/photog/scenic-spot/delete"
        ])
        XCTAssertEqual(queryItems(from: session.requests[0])["id"], "7")
        let addBody = try bodyObject(from: session.requests[1])
        XCTAssertEqual(addBody["scenic_area_id"] as? String, "88")
        XCTAssertEqual(addBody["name"] as? String, "入口")
        XCTAssertEqual((addBody["guide_imgs"] as? [String])?.first, "https://cdn/a.jpg")
        let deleteBody = try bodyObject(from: session.requests[3])
        XCTAssertEqual(deleteBody["id"] as? Int, 7)
    }

    private static let emptyResponse = Data(#"{"code":100000,"msg":"ok","data":{}}"#.utf8)
    private static let listResponse = Data(#"{"code":100000,"msg":"ok","data":{"total":"1","list":[{"id":"7","scenic_area_id":"88","name":100,"status":"1","status_label":"运营中","description":"描述","region":{"lat":"30.1","lot":120.2,"address":"入口","scenic_spot_str":"入口点"},"scenic_spot_str":"入口点","guide_imgs":["https://cdn/a.jpg"],"mp_qrcode":"https://cdn/qr.png","created_at":"2026-06-24"}]}}"#.utf8)
    private static let infoResponse = Data(#"{"code":100000,"msg":"ok","data":{"id":7,"name":"入口","status":1,"region":{"lat":30.1,"lot":"120.2","address":"入口"},"guide_imgs":[]}}"#.utf8)
}

@MainActor
/// 打卡点 ViewModel 测试，覆盖分页、删除和图片上传提交。
final class PunchPointViewModelTests: XCTestCase {
    /// 测试无景区时清空列表且不请求接口。
    func testListClearsWhenScenicMissing() async {
        let api = MockPunchPointService()
        let viewModel = PunchPointListViewModel()
        viewModel.items = [PunchPointItem(id: 1, name: "旧数据")]

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

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

    /// 测试列表分页和最后一页停止请求。
    func testListPaginationStopsAtLastPage() async {
        let api = MockPunchPointService()
        api.pages = [
            ListPayload(total: 2, list: [PunchPointItem(id: 1, name: "A")]),
            ListPayload(total: 2, list: [PunchPointItem(id: 2, name: "B")])
        ]
        let viewModel = PunchPointListViewModel()

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

        XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
        XCTAssertEqual(api.listRequests.map(\.page), [1, 2])
    }

    /// 测试删除成功后刷新列表。
    func testDeleteRefreshesList() async {
        let api = MockPunchPointService()
        let viewModel = PunchPointListViewModel()

        let success = await viewModel.delete(PunchPointItem(id: 9, name: "A"), scenicId: 88, api: api)

        XCTAssertTrue(success)
        XCTAssertEqual(api.deletedIds, [9])
        XCTAssertEqual(api.listRequests.count, 1)
    }

    /// 测试新建打卡点会先上传本地图片，再提交最终 OSS URL。
    func testEditorUploadsImagesBeforeSubmit() async {
        let api = MockPunchPointService()
        let uploader = MockPunchPointUploader()
        let viewModel = PunchPointEditorViewModel()
        viewModel.name = "入口"
        viewModel.applyLocation(latitude: 30.1, longitude: 120.2, address: "入口")
        viewModel.addLocalImages([PunchPointLocalImage(data: Data([1]), fileName: "a.jpg")])

        let success = await viewModel.submit(scenicId: 88, api: api, uploadService: uploader)

        XCTAssertTrue(success)
        XCTAssertEqual(uploader.uploadedFileNames, ["a.jpg"])
        XCTAssertEqual(api.addRequests.first?.guideImages.first, "https://cdn.example.com/a.jpg")
    }

    /// 测试上传失败时不提交打卡点接口。
    func testEditorUploadFailureDoesNotSubmit() async {
        let api = MockPunchPointService()
        let uploader = MockPunchPointUploader()
        uploader.shouldFail = true
        let viewModel = PunchPointEditorViewModel()
        viewModel.name = "入口"
        viewModel.applyLocation(latitude: 30.1, longitude: 120.2, address: "入口")
        viewModel.addLocalImages([PunchPointLocalImage(data: Data([1]), fileName: "a.jpg")])

        let success = await viewModel.submit(scenicId: 88, api: api, uploadService: uploader)

        XCTAssertFalse(success)
        XCTAssertEqual(api.addRequests.count, 0)
    }
}

/// 打卡点 API 测试用 URLSession。
private final class PunchPointRecordingSession: URLSessionProtocol {
    private var responses: [Data]
    private(set) var requests: [URLRequest] = []

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

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

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

@MainActor
/// 打卡点服务测试替身。
private final class MockPunchPointService: PunchPointServing {
    var pages: [ListPayload<PunchPointItem>] = [ListPayload(total: 0, list: [])]
    var listRequests: [(scenicId: Int, status: Int, page: Int, pageSize: Int)] = []
    var addRequests: [AddPunchPointRequest] = []
    var editRequests: [EditPunchPointRequest] = []
    var deletedIds: [Int] = []

    func punchPointList(scenicId: Int, status: Int, page: Int, pageSize: Int) async throws -> ListPayload<PunchPointItem> {
        listRequests.append((scenicId, status, page, pageSize))
        return pages.isEmpty ? ListPayload(total: 0, list: []) : pages.removeFirst()
    }

    func punchPointInfo(id: Int) async throws -> PunchPointItem {
        PunchPointItem(id: id, name: "详情")
    }

    func addPunchPoint(_ request: AddPunchPointRequest) async throws {
        addRequests.append(request)
    }

    func editPunchPoint(_ request: EditPunchPointRequest) async throws {
        editRequests.append(request)
    }

    func deletePunchPoint(id: Int) async throws {
        deletedIds.append(id)
    }
}

@MainActor
/// OSS 上传测试替身。
private final class MockPunchPointUploader: OSSUploadServing {
    var shouldFail = false
    var uploadedFileNames: [String] = []

    func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
        if shouldFail { throw NSError(domain: "upload", code: 1) }
        uploadedFileNames.append(fileName)
        onProgress(100)
        return "https://cdn.example.com/\(fileName)"
    }

    func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
    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 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 { "" }
}

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

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