//
//  WildPhotographerReportTests.swift
//  suixinkanTests
//

import CoreLocation
import MapKit
import XCTest
@testable import suixinkan

@MainActor
/// 举报摄影师模块测试，覆盖接口类型、提交校验、列表筛选和补充证据。
final class WildPhotographerReportTests: XCTestCase {
    func testReportTypesAPILoadsRemoteOptions() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": {
                "list": [
                    { "value": 1, "label": "私下收款" },
                    { "value": 2, "label": "疑似打野" },
                    { "value": 3, "label": "未戴工牌" },
                    { "value": 4, "label": "其他" }
                ]
            },
            "time": "2026-07-08 14:21:21"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.reportTypes()

        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/types")
        XCTAssertEqual(response.list, [
            WildReportType(value: 1, label: "私下收款"),
            WildReportType(value: 2, label: "疑似打野"),
            WildReportType(value: 3, label: "未戴工牌"),
            WildReportType(value: 4, label: "其他")
        ])
    }

    func testReportCopyAPILoadsNoticeAndRuleGroups() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": {
                "notice": ["需实名登录", "请尽量上传清晰证据"],
                "rule_groups": [
                    {
                        "title": "可举报行为",
                        "items": ["疑似打野摄影师主动揽客"]
                    }
                ]
            },
            "time": "2026-07-09 10:15:17"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.reportCopy()

        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/copy")
        XCTAssertEqual(response.notice, ["需实名登录", "请尽量上传清晰证据"])
        XCTAssertEqual(response.ruleGroups, [
            WildReportRuleGroup(title: "可举报行为", items: ["疑似打野摄影师主动揽客"])
        ])
    }

    func testHomeViewModelLoadsReportCopyAndDisplayRules() async {
        let viewModel = WildPhotographerReportHomeViewModel()
        let api = MockWildPhotographerReportAPI()
        api.reportCopyResponse = WildReportCopyResponse(
            notice: ["需实名登录", "请尽量上传清晰证据"],
            ruleGroups: [WildReportRuleGroup(title: "材料要求", items: ["尽量提供现场图片"])]
        )

        await viewModel.loadReportCopy(api: api)

        XCTAssertEqual(viewModel.noticeItems, ["需实名登录", "请尽量上传清晰证据"])
        XCTAssertEqual(viewModel.ruleGroups, [WildReportRuleGroup(title: "材料要求", items: ["尽量提供现场图片"])])
        XCTAssertTrue(viewModel.shouldShowNoticeModule)
        XCTAssertTrue(viewModel.shouldShowRuleButton)
    }

    func testHomeViewModelHidesNoticeModuleWhenCopyIsEmpty() async {
        let viewModel = WildPhotographerReportHomeViewModel()
        let api = MockWildPhotographerReportAPI()
        api.reportCopyResponse = WildReportCopyResponse(notice: ["  "], ruleGroups: [WildReportRuleGroup(title: " ", items: [])])

        await viewModel.loadReportCopy(api: api)

        XCTAssertTrue(viewModel.noticeItems.isEmpty)
        XCTAssertTrue(viewModel.ruleGroups.isEmpty)
        XCTAssertFalse(viewModel.shouldShowNoticeModule)
        XCTAssertFalse(viewModel.shouldShowRuleButton)
    }

    func testHomeViewModelShowsNoticeWithoutRuleButtonWhenRulesAreEmpty() async {
        let viewModel = WildPhotographerReportHomeViewModel()
        let api = MockWildPhotographerReportAPI()
        api.reportCopyResponse = WildReportCopyResponse(notice: ["需实名登录"], ruleGroups: [])

        await viewModel.loadReportCopy(api: api)

        XCTAssertTrue(viewModel.shouldShowNoticeModule)
        XCTAssertFalse(viewModel.shouldShowRuleButton)
    }

    func testReportListAPILoadsRemoteReports() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": {
                "list": [
                    {
                        "id": 32,
                        "complaint_no": "JB20260708917",
                        "title": "私下收款举报",
                        "report_type": 1,
                        "report_type_text": "私下收款",
                        "desc": "我在",
                        "scenic_id": 128,
                        "scenic_name": "伊犁那拉提景区-5A",
                        "location_name": "伊犁那拉提景区-5A",
                        "latitude": "32.4299428",
                        "longitude": "119.4403263",
                        "handle_status": 0,
                        "handle_status_text": "待处理",
                        "created_at": "2026-07-08 16:06:59"
                    }
                ],
                "total": 1,
                "page": 1,
                "page_size": 10
            },
            "time": "2026-07-08 16:07:30"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.reportList(WildReportListRequest(scenicId: 128, handleStatus: 0, page: 1, pageSize: 10))

        let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/list")
        XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "scenic_id", value: "128")) == true)
        XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "handle_status", value: "0")) == true)
        XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "page", value: "1")) == true)
        XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "page_size", value: "10")) == true)
        XCTAssertEqual(response.total, 1)
        XCTAssertEqual(response.page, 1)
        XCTAssertEqual(response.pageSize, 10)
        XCTAssertEqual(response.list.first?.complaintNo, "JB20260708917")
    }

    func testSubmitReportAPIReturnsComplaintNumber() async throws {
        let json = Data("""
        {
            "code": 100000,
            "data": {
                "complaint_no": "JB20260709886",
                "id": 40
            },
            "msg": "success",
            "time": "2026-07-09 14:09:41"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.submitReport(WildReportSubmitRequest(
            scenicId: 128,
            reportType: 2,
            desc: "测试说明",
            storeUserId: nil,
            contactPhone: "13800138000",
            locationName: "测试景区",
            locationAddress: "测试地址",
            lat: 30.12,
            lng: 120.34,
            evidences: [WildReportEvidenceRequest(fileURL: "https://example.com/scene.jpg", fileType: 1, fileName: "scene.jpg")],
            paymentScreenshots: []
        ))

        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/submit")
        XCTAssertEqual(response.id, 40)
        XCTAssertEqual(response.complaintNo, "JB20260709886")
    }

    func testReportDetailAPILoadsRemoteDetail() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": {
                "id": 32,
                "complaint_no": "JB20260708917",
                "title": "私下收款举报",
                "report_type": 1,
                "report_type_text": "私下收款",
                "desc": "我在观景台附近看到私下收款",
                "supplements": [],
                "report_contents": [
                    {
                        "type": "initial",
                        "id": 0,
                        "desc": "我在观景台附近看到私下收款",
                        "time": "2026-07-08 16:06:59",
                        "time_label": "16:06:59",
                        "evidences": [
                            {
                                "file_type": 1,
                                "file_type_text": "图片",
                                "file_url": "https://example.com/report.jpg",
                                "file_name": "report.jpg"
                            }
                        ]
                    }
                ],
                "scenic_id": 128,
                "scenic_name": "伊犁那拉提景区-5A",
                "store_user_id": 0,
                "contact_phone": "13800138000",
                "location_name": "伊犁那拉提景区-5A",
                "location_address": "江苏省扬州市邗江区瘦西湖路",
                "latitude": "32.4299428",
                "longitude": "119.4403263",
                "handle_status": 0,
                "handle_status_text": "待处理",
                "handle_remark": "",
                "handled_at": "2026-07-09 14:18:40",
                "handler": {
                    "id": 43,
                    "name": "管铜彬",
                    "phone": "132****9413",
                    "started_at": "2026-07-09 14:18:15",
                    "finished_at": "2026-07-09 14:18:40",
                    "remark": "我是处理意见"
                },
                "handler_name": "管铜彬",
                "handler_phone": "132****9413",
                "evidence_summary": { "image_count": 1, "video_count": 1 },
                "evidences": [
                    {
                        "file_type": 1,
                        "file_type_text": "图片",
                        "file_url": "https://example.com/report.jpg",
                        "file_name": "report.jpg"
                    },
                    {
                        "file_type": 2,
                        "file_type_text": "视频",
                        "file_url": "https://example.com/report.mov",
                        "file_name": "report.mov"
                    }
                ],
                "handle_evidences": [
                    {
                        "evidence_kind": 2,
                        "evidence_kind_text": "处理凭证",
                        "file_type": 1,
                        "file_type_text": "图片",
                        "file_url": "http://tmp/handler-one.jpg",
                        "file_name": "handler-one.jpg"
                    },
                    {
                        "evidence_kind": 2,
                        "evidence_kind_text": "处理凭证",
                        "file_type": 1,
                        "file_type_text": "图片",
                        "file_url": "http://tmp/handler-two.png",
                        "file_name": "handler-two.png"
                    }
                ],
                "payment_screenshots": [
                    {
                        "file_type": 5,
                        "file_type_text": "支付截图",
                        "file_url": "https://example.com/payment.jpg",
                        "file_name": "payment.jpg"
                    }
                ],
                "created_at": "2026-07-08 16:06:59",
                "timeline": [
                    { "time": "2026-07-08 16:06:59", "content": "摄影师提交举报" }
                ]
            },
            "time": "2026-07-08 16:31:50"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.reportDetail(id: 32)

        let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/detail")
        XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "id", value: "32")) == true)
        XCTAssertEqual(response.complaintNo, "JB20260708917")
        XCTAssertEqual(response.evidences.count, 2)
        XCTAssertEqual(response.paymentScreenshots.first?.fileName, "payment.jpg")
        XCTAssertEqual(response.timeline.first?.content, "摄影师提交举报")
        XCTAssertEqual(response.handler?.name, "管铜彬")
        XCTAssertEqual(response.handler?.phone, "132****9413")
        XCTAssertEqual(response.handler?.startedAt, "2026-07-09 14:18:15")
        XCTAssertEqual(response.handler?.finishedAt, "2026-07-09 14:18:40")
        XCTAssertEqual(response.handler?.remark, "我是处理意见")
        XCTAssertEqual(response.handledAt, "2026-07-09 14:18:40")
        XCTAssertEqual(response.handleEvidences.count, 2)
        XCTAssertEqual(response.handleEvidences.first?.evidenceKind, 2)
        XCTAssertEqual(response.handleEvidences.first?.evidenceKindText, "处理凭证")
        XCTAssertEqual(response.handleEvidences.first?.fileURL, "http://tmp/handler-one.jpg")
        XCTAssertEqual(response.handleEvidences.last?.fileName, "handler-two.png")
    }

    func testReportShareAPILoadsMiniProgramConfig() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": {
                "userName": "wx8c85189bf3bdda29",
                "path": "pages/scenic/radar/report-detail/index?id=32",
                "title": "私下收款举报",
                "imagePath": "/assets/share/report-share-cover.png",
                "webpageUrl": "https://www.youfuntour.com",
                "withShareTicket": true,
                "miniprogramType": 0,
                "scene": 0
            },
            "time": "2026-07-09 15:42:19"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.reportShare(id: "32")

        let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
        XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/share")
        XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "id", value: "32")) == true)
        XCTAssertEqual(response.userName, "wx8c85189bf3bdda29")
        XCTAssertEqual(response.path, "pages/scenic/radar/report-detail/index?id=32")
        XCTAssertEqual(response.title, "私下收款举报")
        XCTAssertEqual(response.imagePath, "/assets/share/report-share-cover.png")
        XCTAssertEqual(response.webpageURL, "https://www.youfuntour.com")
        XCTAssertTrue(response.withShareTicket)
        XCTAssertEqual(response.miniProgramType, .release)
        XCTAssertEqual(response.scene, .session)
        XCTAssertEqual(response.payload.userName, "wx8c85189bf3bdda29")
        XCTAssertEqual(response.payload.path, "pages/scenic/radar/report-detail/index?id=32")
    }

    func testSupplementReportAPISendsRequestBody() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": null,
            "time": "2026-07-08 17:31:50"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        try await api.supplementReport(WildReportSupplementRequest(
            id: 32,
            desc: "补充说明",
            evidences: [
                WildReportEvidenceRequest(
                    fileURL: "https://example.com/supplement.jpg",
                    fileType: 1,
                    fileName: "supplement.jpg"
                )
            ]
        ))

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "POST")
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/supplement")
        let bodyData = try XCTUnwrap(request.httpBody)
        let body = try XCTUnwrap(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
        XCTAssertEqual(body["id"] as? Int, 32)
        XCTAssertEqual(body["desc"] as? String, "补充说明")
        let evidences = try XCTUnwrap(body["evidences"] as? [[String: Any]])
        XCTAssertEqual(evidences.first?["file_url"] as? String, "https://example.com/supplement.jpg")
        XCTAssertEqual(evidences.first?["file_type"] as? Int, 1)
        XCTAssertEqual(evidences.first?["file_name"] as? String, "supplement.jpg")
    }

    func testRiskMapAPISendsGETQueryAndDecodesMarkers() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": {
                "scenic_id": 128,
                "scenic_name": "伊犁那拉提景区-5A",
                "legend": [
                    { "marker_type": "red", "title": "疑似打野" }
                ],
                "green_marker_tip": "绿色为景区内摄影师",
                "markers": [
                    {
                        "marker_type": "red",
                        "id": 32,
                        "latitude": "43.4068",
                        "longitude": "84.0582",
                        "location_name": "空中草原观景道",
                        "title": "线索 ID：32",
                        "report_type_text": "疑似打野",
                        "handle_status": 0,
                        "distance_m": 320,
                        "created_at": "2026-07-08 16:06:59"
                    },
                    {
                        "marker_type": "yellow",
                        "id": 33,
                        "latitude": "43.3652",
                        "longitude": "84.0385",
                        "location_name": "塔吾萨尼步道",
                        "title": "线索 ID：33",
                        "report_type_text": "处理中",
                        "handle_status": "1",
                        "distance_m": 1280,
                        "created_at": "2026-07-08 16:08:59"
                    }
                ]
            },
            "time": "2026-07-08 18:31:50"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.riskMap(WildReportRiskMapRequest(
            scenicId: 128,
            latitude: 43.3779,
            longitude: 84.0217
        ))

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "GET")
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/risk-map")
        XCTAssertNil(request.httpBody)
        let queryItems = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems ?? []
        XCTAssertEqual(queryItems.first(where: { $0.name == "scenic_id" })?.value, "128")
        XCTAssertEqual(queryItems.first(where: { $0.name == "latitude" })?.value, "43.3779")
        XCTAssertEqual(queryItems.first(where: { $0.name == "longitude" })?.value, "84.0217")
        XCTAssertEqual(response.scenicName, "伊犁那拉提景区-5A")
        XCTAssertEqual(response.greenMarkerTip, "绿色为景区内摄影师")
        XCTAssertEqual(response.markers.map(\.markerType), ["red", "yellow"])
        XCTAssertEqual(response.markers.last?.handleStatus?.displayText, WildReportStatus.processing.rawValue)
    }

    func testMarkerDetailAPISendsGETQueryAndDecodesGreenDetail() async throws {
        let json = Data("""
        {
            "code": 100000,
            "msg": "success",
            "data": {
                "marker_type": "green",
                "id": 901,
                "title": "景区内摄影师",
                "tag": "认证",
                "name": "阿依达娜",
                "avatar": "https://example.com/avatar.jpg",
                "store_name": "空中草原旅拍店",
                "online": true,
                "location_name": "空中草原",
                "latitude": "43.4042",
                "longitude": "84.0186",
                "distance_m": 430,
                "green_marker_tip": "绿色为景区内摄影师",
                "activity_text": "当前在空中草原区域服务游客拍摄。"
            },
            "time": "2026-07-08 18:31:50"
        }
        """.utf8)
        let session = MockURLSession(responses: [json])
        let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))

        let response = try await api.markerDetail(WildReportMarkerDetailRequest(
            scenicId: 128,
            markerType: "green",
            id: 901,
            latitude: 43.3779,
            longitude: 84.0217
        ))

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "GET")
        XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/marker-detail")
        XCTAssertNil(request.httpBody)
        let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false))
        let queryItems = Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value) })
        XCTAssertEqual(queryItems["scenic_id"], "128")
        XCTAssertEqual(queryItems["marker_type"], "green")
        XCTAssertEqual(queryItems["id"], "901")
        XCTAssertEqual(queryItems["latitude"], "43.3779")
        XCTAssertEqual(queryItems["longitude"], "84.0217")
        XCTAssertEqual(response.name, "阿依达娜")
        XCTAssertEqual(response.avatar, "https://example.com/avatar.jpg")
        XCTAssertEqual(response.storeName, "空中草原旅拍店")
        XCTAssertTrue(response.online)
    }

    func testReportListItemMapsToRecord() {
        let item = makeReportListItem(
            complaintNo: "JB20260708917",
            title: "私下收款举报",
            reportType: 1,
            reportTypeText: "私下收款",
            handleStatus: 0,
            handleStatusText: "待处理"
        )

        let record = item.toRecord()

        XCTAssertEqual(record.id, "JB20260708917")
        XCTAssertEqual(record.title, "私下收款举报")
        XCTAssertEqual(record.reportType, WildReportType(value: 1, label: "私下收款"))
        XCTAssertEqual(record.status, .pending)
        XCTAssertEqual(record.location, "伊犁那拉提景区-5A")
        XCTAssertEqual(record.description, "测试说明")
        XCTAssertEqual(record.submitTime, "2026-07-08 16:06:59")
        XCTAssertEqual(record.imageCount, 0)
        XCTAssertEqual(record.videoCount, 0)
        XCTAssertEqual(record.serverID, 32)
    }

    func testReportDetailMapsToRecordAndSupplementData() {
        let detail = makeReportDetailData(handleStatus: 3, handleStatusText: "驳回")

        let record = detail.toRecord()

        XCTAssertEqual(record.serverID, 32)
        XCTAssertEqual(record.id, "JB20260708917")
        XCTAssertEqual(record.reportType, WildReportType(value: 1, label: "私下收款"))
        XCTAssertEqual(record.status, .rejected)
        XCTAssertEqual(record.location, "伊犁那拉提景区-5A · 江苏省扬州市邗江区瘦西湖路")
        XCTAssertEqual(record.description, "正式详情说明")
        XCTAssertEqual(record.phoneNumber, "13800138000")
        XCTAssertEqual(record.imageCount, 1)
        XCTAssertEqual(record.videoCount, 1)
        XCTAssertEqual(detail.supplementRecords.first?.summaryText, "补充说明 · 1张图片")
        XCTAssertEqual(detail.supplementRecords.first?.evidences.first?.fileURL, "https://example.com/supplement.jpg")
    }

    func testSupplementRecordsPreserveMixedMediaOrderAndSummary() throws {
        let evidences = [
            WildReportDetailEvidence(
                fileType: 1,
                fileTypeText: "图片",
                fileURL: "https://example.com/one.jpg",
                fileName: "one.jpg"
            ),
            WildReportDetailEvidence(
                fileType: 2,
                fileTypeText: "视频",
                fileURL: "https://example.com/two.mp4",
                fileName: "two.mp4"
            ),
            WildReportDetailEvidence(
                fileType: 5,
                fileTypeText: "图片",
                fileURL: "https://example.com/three.png",
                fileName: "three.png"
            )
        ]
        let detail = WildReportDetailData(
            supplements: [
                WildReportDetailContent(
                    type: "supplement",
                    id: 7,
                    desc: "补充现场材料",
                    time: "2026-07-13 10:00:00",
                    evidences: evidences
                )
            ]
        )

        let record = try XCTUnwrap(detail.supplementRecords.first)

        XCTAssertEqual(record.evidences.map(\.fileType), [1, 2, 5])
        XCTAssertEqual(record.evidences.map(\.fileURL), [
            "https://example.com/one.jpg",
            "https://example.com/two.mp4",
            "https://example.com/three.png"
        ])
        XCTAssertEqual(record.imageCount, 2)
        XCTAssertEqual(record.videoCount, 1)
        XCTAssertTrue(record.hasMedia)
        XCTAssertEqual(record.summaryText, "补充现场材料 · 2张图片 · 1段视频")
    }

    func testSupplementRecordsFallBackToReportContentsAndKeepMedia() throws {
        let detail = WildReportDetailData(
            supplements: [],
            reportContents: [
                WildReportDetailContent(
                    type: "report",
                    id: 1,
                    desc: "初始举报",
                    time: "2026-07-13 09:00:00"
                ),
                WildReportDetailContent(
                    type: "supplement",
                    id: 2,
                    desc: "补充视频",
                    time: "2026-07-13 10:00:00",
                    evidences: [
                        WildReportDetailEvidence(
                            fileType: 2,
                            fileTypeText: "视频",
                            fileURL: "https://example.com/fallback.mp4",
                            fileName: "fallback.mp4"
                        )
                    ]
                )
            ]
        )

        let record = try XCTUnwrap(detail.supplementRecords.first)

        XCTAssertEqual(detail.supplementRecords.count, 1)
        XCTAssertEqual(record.id, "2")
        XCTAssertEqual(record.evidences.map(\.fileURL), ["https://example.com/fallback.mp4"])
        XCTAssertEqual(record.imageCount, 0)
        XCTAssertEqual(record.videoCount, 1)
        XCTAssertEqual(record.summaryText, "补充视频 · 1段视频")
    }

    func testSupplementRecordSequenceTitleStartsFromDisplayOrder() {
        let titles = (0..<3).map {
            WildReportSupplementDisplayFormatter.sequenceTitle(forDisplayIndex: $0)
        }

        XCTAssertEqual(titles, ["第1次补充", "第2次补充", "第3次补充"])
    }

    func testReportDetailMapsHandlerInfoFromHandlerFields() throws {
        let detail = makeReportDetailData(
            handleStatus: 2,
            handleStatusText: "已处理",
            handleRemark: "顶层处理备注",
            handledAt: "2026-07-09 14:18:40",
            handler: WildReportDetailHandler(
                id: 43,
                name: "管铜彬",
                phone: "132****9413",
                startedAt: "2026-07-09 14:18:15",
                finishedAt: "2026-07-09 14:18:40",
                remark: "我是处理意见"
            ),
            handleEvidences: [
                WildReportDetailEvidence(
                    fileType: 1,
                    fileTypeText: "图片",
                    fileURL: "http://tmp/handler-one.jpg",
                    fileName: "handler-one.jpg",
                    evidenceKind: 2,
                    evidenceKindText: "处理凭证"
                ),
                WildReportDetailEvidence(
                    fileType: 1,
                    fileTypeText: "图片",
                    fileURL: "http://tmp/handler-two.png",
                    fileName: "handler-two.png",
                    evidenceKind: 2,
                    evidenceKindText: "处理凭证"
                )
            ]
        )

        let info = try XCTUnwrap(detail.toRecord().handlerInfo)

        XCTAssertEqual(info.handlerName, "管铜彬")
        XCTAssertEqual(info.handlerPhone, "132****9413")
        XCTAssertEqual(info.processingAt, "2026-07-09 14:18:15")
        XCTAssertEqual(info.processedAt, "2026-07-09 14:18:40")
        XCTAssertEqual(info.processOpinion, "我是处理意见")
        XCTAssertEqual(info.completionMedia.count, 2)
        XCTAssertEqual(info.completionMedia.first?.coverURL, "http://tmp/handler-one.jpg")
        XCTAssertEqual(info.completionMediaSummary, "2张")
    }

    func testReportDetailMapsHandlerInfoFromFallbackFields() throws {
        let detail = makeReportDetailData(
            handleStatus: 2,
            handleStatusText: "已处理",
            handleRemark: "fallback 处理意见",
            handledAt: "2026-07-09 14:18:40",
            handlerName: "管铜彬",
            handlerPhone: "132****9413"
        )

        let info = try XCTUnwrap(detail.toRecord().handlerInfo)

        XCTAssertEqual(info.handlerName, "管铜彬")
        XCTAssertEqual(info.handlerPhone, "132****9413")
        XCTAssertEqual(info.processingAt, "")
        XCTAssertEqual(info.processedAt, "2026-07-09 14:18:40")
        XCTAssertEqual(info.processOpinion, "fallback 处理意见")
    }

    func testReportShareDataDecodesSnakeCaseAndMapsPayload() throws {
        let json = Data("""
        {
            "user_name": "gh_report_original_id",
            "path": "pages/report/detail?id=32",
            "title": "私下收款举报",
            "description": "查看举报处理进度",
            "image_path": "https://example.com/share.png",
            "webpage_url": "https://www.youfuntour.com/report/32",
            "with_share_ticket": true,
            "mini_program_type": 2,
            "scene": 0,
            "disable_forward": true
        }
        """.utf8)

        let data = try JSONDecoder().decode(WildReportMiniProgramShareData.self, from: json)

        XCTAssertEqual(data.userName, "gh_report_original_id")
        XCTAssertEqual(data.webpageURL, "https://www.youfuntour.com/report/32")
        XCTAssertEqual(data.miniProgramType, .preview)
        XCTAssertEqual(data.scene, .session)
        XCTAssertEqual(data.payload.description, "查看举报处理进度")
        XCTAssertTrue(data.payload.withShareTicket)
        XCTAssertTrue(data.payload.disableForward)
    }

    func testReportStatusMapsRejected() {
        XCTAssertEqual(WildReportStatus(handleStatus: 0), .pending)
        XCTAssertEqual(WildReportStatus(handleStatus: 1), .processing)
        XCTAssertEqual(WildReportStatus(handleStatus: 2), .processed)
        XCTAssertEqual(WildReportStatus(handleStatus: 3), .rejected)
    }

    func testReportListAllFilterOmitsHandleStatus() {
        let request = WildReportListRequest(scenicId: 128, handleStatus: nil, page: 1, pageSize: 10)

        XCTAssertFalse(request.queryItems.contains { $0.name == "handle_status" })
        XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "scenic_id", value: "128")))
        XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "page", value: "1")))
        XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "page_size", value: "10")))
    }

    func testSubmitViewModelLoadsReportTypesFromAPI() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.reportTypeResponse = WildReportTypeListResponse(list: [
            WildReportType(value: 1, label: "私下收款"),
            WildReportType(value: 2, label: "疑似打野"),
            WildReportType(value: 4, label: "其他"),
        ])

        await viewModel.loadReportTypes(api: api)

        XCTAssertEqual(viewModel.reportTypes, [
            WildReportType(value: 1, label: "私下收款"),
            WildReportType(value: 2, label: "疑似打野"),
            WildReportType(value: 4, label: "其他")
        ])
        XCTAssertEqual(viewModel.selectedReportType, WildReportType(value: 1, label: "私下收款"))
    }

    func testSubmitViewModelKeepsReportTypesEmptyWhenReportTypesFail() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.error = URLError(.badServerResponse)
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.loadReportTypes(api: api)

        XCTAssertTrue(viewModel.reportTypes.isEmpty)
        XCTAssertNil(viewModel.selectedReportType)
        XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
    }

    func testSubmitLocationOmitsProvinceCityDistrict() {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)

        viewModel.applyResolvedLocation(HomeLocationSnapshot(
            latitude: 32.40,
            longitude: 119.41,
            address: "江苏省扬州市邗江区瘦西湖路"
        ))

        XCTAssertEqual(viewModel.locationTitleText, "当前定位：瘦西湖路")
        XCTAssertEqual(viewModel.reportLocation, "瘦西湖路")
    }

    func testSubmitTextInputUpdatesWithoutRequestingStructuralRefresh() {
        let viewModel = WildPhotographerReportSubmitViewModel(
            homeViewModel: WildPhotographerReportHomeViewModel()
        )
        var stateChangeCount = 0
        viewModel.onStateChange = { stateChangeCount += 1 }

        viewModel.updateDescription("现场说明")
        viewModel.updateContact("wx-test")

        XCTAssertEqual(viewModel.description, "现场说明")
        XCTAssertEqual(viewModel.contact, "wx-test")
        XCTAssertEqual(stateChangeCount, 0)
    }

    func testSubmitFailsWithoutEvidence() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
        viewModel.updateDescription("有说明")
        viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试位置"))

        await viewModel.submitReport(
            api: MockWildPhotographerReportAPI(),
            uploader: MockWildReportAttachmentUploader(),
            scenicId: 9,
            scenicName: "测试景区"
        )

        XCTAssertEqual(viewModel.validationMessage, "请至少上传一项现场证据。")
        XCTAssertNil(viewModel.submitResult)
    }

    func testSubmitFailsWithoutDescription() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
        viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
        viewModel.updateDescription("   ")
        viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试位置"))

        await viewModel.submitReport(
            api: MockWildPhotographerReportAPI(),
            uploader: MockWildReportAttachmentUploader(),
            scenicId: 9,
            scenicName: "测试景区"
        )

        XCTAssertEqual(viewModel.validationMessage, "请填写举报说明。")
        XCTAssertNil(viewModel.submitResult)
    }

    func testSubmitFailsWithoutConfirmedLocation() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
        viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
        viewModel.updateDescription("测试说明")

        await viewModel.submitReport(
            api: MockWildPhotographerReportAPI(),
            uploader: MockWildReportAttachmentUploader(),
            scenicId: 9,
            scenicName: "测试景区"
        )

        XCTAssertEqual(viewModel.validationMessage, "请先更新举报位置后再提交举报。")
        XCTAssertNil(viewModel.submitResult)
    }

    func testSubmitSuccessCreatesRecord() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.submitResponse = WildReportSubmitResponse(id: 40, complaintNo: "JB20260709886")
        let uploader = MockWildReportAttachmentUploader()
        viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
        viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
        viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
        viewModel.updateDescription("测试说明")
        viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "江苏省扬州市邗江区瘦西湖路"))

        await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")

        XCTAssertNil(viewModel.validationMessage)
        XCTAssertNotNil(viewModel.submitResult)
        XCTAssertEqual(viewModel.submitResult?.record.serverID, 40)
        XCTAssertEqual(viewModel.submitResult?.record.id, "JB20260709886")
        XCTAssertEqual(uploader.uploadedFileNames, ["scene.jpg", "payment.jpg"])
        XCTAssertEqual(api.submitRequests.first?.scenicId, 9)
        XCTAssertEqual(api.submitRequests.first?.reportType, 2)
        XCTAssertEqual(api.submitRequests.first?.lat, 30.12)
        XCTAssertEqual(api.submitRequests.first?.lng, 120.34)
        XCTAssertEqual(api.submitRequests.first?.locationName, "测试景区")
        XCTAssertEqual(api.submitRequests.first?.locationAddress, "瘦西湖路")
        XCTAssertEqual(api.submitRequests.first?.evidences.count, 1)
        XCTAssertEqual(api.submitRequests.first?.evidences.first?.fileType, 1)
        XCTAssertEqual(api.submitRequests.first?.paymentScreenshots.count, 1)
        XCTAssertNil(api.submitRequests.first?.storeUserId)
        XCTAssertEqual(viewModel.submitResult?.record.location, "瘦西湖路")
    }

    func testPaymentScreenshotsDoNotReplaceRequiredEvidence() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
        viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
        viewModel.updateDescription("测试说明")
        viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))

        await viewModel.submitReport(
            api: MockWildPhotographerReportAPI(),
            uploader: MockWildReportAttachmentUploader(),
            scenicId: 9,
            scenicName: "测试景区"
        )

        XCTAssertEqual(viewModel.validationMessage, "请至少上传一项现场证据。")
    }

    func testPaymentScreenshotsLimitedToThree() throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)

        viewModel.addPaymentImages([
            try makeAttachment(kind: .payment, fileName: "p1.jpg"),
            try makeAttachment(kind: .payment, fileName: "p2.jpg"),
            try makeAttachment(kind: .payment, fileName: "p3.jpg"),
            try makeAttachment(kind: .payment, fileName: "p4.jpg")
        ])

        XCTAssertEqual(viewModel.paymentImages.count, 3)
    }

    func testUploadFailureSkipsSubmitAPI() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        let uploader = MockWildReportAttachmentUploader()
        uploader.error = OSSUploadError.emptyFile
        viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
        viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
        viewModel.updateDescription("测试说明")
        viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))

        await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")

        XCTAssertEqual(api.submitRequests.count, 0)
        XCTAssertEqual(viewModel.validationMessage, OSSUploadError.emptyFile.localizedDescription)
    }

    func testListInitialLoadUsesAllStatusAndFirstPage() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.reportListResponses = [
            WildReportListResponse(
                list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)],
                total: 1,
                page: 1,
                pageSize: 10
            )
        ]

        await viewModel.loadInitial(api: api, scenicId: 128)

        XCTAssertEqual(api.reportListRequests.first, WildReportListRequest(scenicId: 128, handleStatus: nil, page: 1, pageSize: 10))
        XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001"])
        XCTAssertFalse(viewModel.hasMore)
    }

    func testListFilterReloadsWithMappedStatus() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.reportListResponses = [
            WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 1, page: 1, pageSize: 10),
            WildReportListResponse(list: [makeReportListItem(complaintNo: "JB002", handleStatus: 1)], total: 1, page: 1, pageSize: 10),
            WildReportListResponse(list: [makeReportListItem(complaintNo: "JB003", handleStatus: 2)], total: 1, page: 1, pageSize: 10),
        ]

        await viewModel.selectFilter(.pending, api: api, scenicId: 128)
        await viewModel.selectFilter(.processing, api: api, scenicId: 128)
        await viewModel.selectFilter(.processed, api: api, scenicId: 128)

        XCTAssertEqual(api.reportListRequests.map(\.handleStatus), [0, 1, 2] as [Int?])
        XCTAssertEqual(api.reportListRequests.map(\.page), [1, 1, 1])
        XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB003"])
    }

    func testListLoadMoreAppendsReportsAndUpdatesHasMore() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.reportListResponses = [
            WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 2, page: 1, pageSize: 10),
            WildReportListResponse(list: [makeReportListItem(complaintNo: "JB002", handleStatus: 0)], total: 2, page: 2, pageSize: 10),
        ]

        await viewModel.loadInitial(api: api, scenicId: 128)
        XCTAssertTrue(viewModel.hasMore)
        await viewModel.loadMoreIfNeeded(api: api, scenicId: 128)

        XCTAssertEqual(api.reportListRequests.map(\.page), [1, 2])
        XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001", "JB002"])
        XCTAssertFalse(viewModel.hasMore)
    }

    func testListLoadFailureKeepsExistingReports() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.reportListResponses = [
            WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 2, page: 1, pageSize: 10)
        ]
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.loadInitial(api: api, scenicId: 128)
        api.reportListError = URLError(.badServerResponse)
        await viewModel.loadMoreIfNeeded(api: api, scenicId: 128)

        XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001"])
        XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
    }

    func testListViewModelLoadsMiniProgramSharePayload() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let api = MockWildPhotographerReportAPI()
        api.reportShareResponse = WildReportMiniProgramShareData(
            userName: "gh_report_original_id",
            path: "pages/report/detail?id=32",
            title: "私下收款举报",
            description: "查看举报处理进度",
            webpageURL: "https://www.youfuntour.com/report/32",
            withShareTicket: true,
            miniProgramTypeValue: WeChatMiniProgramType.preview.rawValue
        )

        let maybePayload = await viewModel.shareReport(record, api: api)
        let payload = try XCTUnwrap(maybePayload)

        XCTAssertEqual(api.reportShareRequests, ["32"])
        XCTAssertEqual(payload.title, "私下收款举报")
        XCTAssertEqual(payload.userName, "gh_report_original_id")
        XCTAssertEqual(payload.path, "pages/report/detail?id=32")
        XCTAssertEqual(payload.miniProgramType, .preview)
        XCTAssertTrue(payload.withShareTicket)
    }

    func testListViewModelShareRejectsLocalRecord() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
        let record = WildReportRecord(
            serverID: nil,
            id: "JBLOCAL",
            title: "摄影师举报",
            reportType: WildReportType(value: 2, label: "疑似打野"),
            status: .pending,
            location: "测试景区",
            submitTime: "2026-07-08 16:06:59",
            description: "本地记录",
            wechatID: "",
            phoneNumber: "",
            imageCount: 1,
            videoCount: 0,
            handlerInfo: nil
        )
        let api = MockWildPhotographerReportAPI()
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        let payload = await viewModel.shareReport(record, api: api)

        XCTAssertNil(payload)
        XCTAssertTrue(api.reportShareRequests.isEmpty)
        XCTAssertEqual(messages.last, "当前举报暂未同步到服务器，无法分享")
    }

    func testSupplementEvidenceValidationAndSuccess() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        let uploader = MockWildReportAttachmentUploader()

        await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
        XCTAssertEqual(viewModel.validationMessage, "请至少填写补充说明或上传一项证据。")

        viewModel.updateText("补充说明")
        viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
        viewModel.addVideos([try makeAttachment(kind: .video, fileName: "supplement.mov")])
        await viewModel.submit(api: api, uploader: uploader, scenicId: 128)

        XCTAssertTrue(viewModel.didSubmit)
        XCTAssertEqual(uploader.uploadedFileNames, ["supplement.jpg", "supplement.mov"])
        XCTAssertEqual(api.supplementRequests.first?.id, 32)
        XCTAssertEqual(api.supplementRequests.first?.desc, "补充说明")
        XCTAssertEqual(api.supplementRequests.first?.evidences?.map(\.fileType), [1, 2])
    }

    func testSupplementEvidenceFailsWithoutServerID() async {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let record = WildReportRecord(
            serverID: nil,
            id: "JBLOCAL",
            title: "摄影师举报",
            reportType: WildReportType(value: 2, label: "疑似打野"),
            status: .pending,
            location: "测试景区",
            submitTime: "2026-07-08 16:06:59",
            description: "本地记录",
            wechatID: "",
            phoneNumber: "",
            imageCount: 1,
            videoCount: 0,
            handlerInfo: nil
        )
        let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
        viewModel.updateText("补充说明")

        await viewModel.submit(
            api: MockWildPhotographerReportAPI(),
            uploader: MockWildReportAttachmentUploader(),
            scenicId: 128
        )

        XCTAssertEqual(viewModel.validationMessage, "当前举报暂未同步到服务器，无法补充证据")
        XCTAssertFalse(viewModel.didSubmit)
    }

    func testSupplementEvidenceTextAndAttachmentLimits() throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)

        viewModel.updateText(String(repeating: "测", count: 520))
        XCTAssertEqual(viewModel.text.count, 500)

        let images = try (0..<10).map { try makeAttachment(kind: .image, fileName: "image\($0).jpg") }
        viewModel.addImages(images)
        XCTAssertEqual(viewModel.images.count, 9)

        viewModel.removeAttachment(viewModel.images[0])
        let videos = try (0..<4).map { try makeAttachment(kind: .video, fileName: "video\($0).mov") }
        viewModel.addVideos(videos)
        XCTAssertEqual(viewModel.videos.count, 1)
    }

    func testSupplementEvidenceUploadFailureDoesNotSubmit() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        let uploader = MockWildReportAttachmentUploader()
        uploader.error = URLError(.cannotWriteToFile)
        viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])

        await viewModel.submit(api: api, uploader: uploader, scenicId: 128)

        XCTAssertFalse(viewModel.didSubmit)
        XCTAssertTrue(api.supplementRequests.isEmpty)
        XCTAssertEqual(viewModel.validationMessage, URLError(.cannotWriteToFile).localizedDescription)
    }

    func testSupplementEvidenceAPIErrorDoesNotSucceed() async throws {
        let homeViewModel = WildPhotographerReportHomeViewModel()
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
        let api = MockWildPhotographerReportAPI()
        api.supplementError = URLError(.badServerResponse)
        viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])

        await viewModel.submit(
            api: api,
            uploader: MockWildReportAttachmentUploader(),
            scenicId: 128
        )

        XCTAssertFalse(viewModel.didSubmit)
        XCTAssertEqual(api.supplementRequests.count, 0)
        XCTAssertEqual(viewModel.validationMessage, URLError(.badServerResponse).localizedDescription)
    }

    func testRiskMapViewModelRejectsInvalidScenicID() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()

        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
            scenicId: 0
        )

        XCTAssertTrue(api.riskMapRequests.isEmpty)
        XCTAssertEqual(viewModel.errorMessage, "请先选择景区后再查看风险地图")
    }

    func testRiskMapViewModelLoadsWithLocationAndMapsMarkerTypes() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]
        let locationProvider = MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217))

        await viewModel.loadInitial(
            api: api,
            locationProvider: locationProvider,
            scenicId: 128,
            scenicName: "测试景区"
        )

        XCTAssertEqual(api.riskMapRequests.count, 1)
        XCTAssertEqual(api.riskMapRequests.first?.scenicId, 128)
        XCTAssertEqual(api.riskMapRequests.first?.latitude, 43.3779)
        XCTAssertEqual(api.riskMapRequests.first?.longitude, 84.0217)
        XCTAssertEqual(locationProvider.requestedAccuracies.last, kCLLocationAccuracyHundredMeters)
        XCTAssertEqual(viewModel.scenicAreaName, "伊犁那拉提景区-5A")
        XCTAssertTrue(viewModel.markers.contains { $0.kind == .selfLocation })
        XCTAssertTrue(viewModel.markers.contains { $0.kind == .wildPhotographer })
        XCTAssertTrue(viewModel.markers.contains { $0.kind == .processingClue })
        XCTAssertTrue(viewModel.markers.contains { $0.kind == .peerPhotographer })
        XCTAssertEqual(viewModel.selectedMarker?.kind, .selfLocation)
        let yellow = viewModel.markers.first { $0.kind == .processingClue }
        XCTAssertEqual(yellow.flatMap { marker in
            if case .activeClue(let clue) = marker.detail { return clue.distanceText }
            return nil
        }, "1.3公里")
    }

    func testRiskMapViewModelLoadsWithoutLocationWhenLocationFails() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]

        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.failure),
            scenicId: 128
        )

        XCTAssertEqual(api.riskMapRequests.first?.scenicId, 128)
        XCTAssertNil(api.riskMapRequests.first?.latitude)
        XCTAssertNil(api.riskMapRequests.first?.longitude)
        XCTAssertFalse(viewModel.markers.contains { $0.kind == .selfLocation })
    }

    func testRiskMapViewModelFocusCurrentLocationSelectsSelfMarker() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]
        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
            scenicId: 128
        )
        let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
        await viewModel.selectMarker(redMarker)

        let didFocus = viewModel.focusCurrentLocation()

        XCTAssertTrue(didFocus)
        XCTAssertEqual(viewModel.selectedMarker?.kind, .selfLocation)
        XCTAssertEqual(viewModel.region.center.latitude, 43.3779, accuracy: 0.000001)
        XCTAssertEqual(viewModel.region.center.longitude, 84.0217, accuracy: 0.000001)
        XCTAssertTrue(api.markerDetailRequests.isEmpty)
    }

    func testRiskMapViewModelLoadsMarkerDetailAndUpdatesSelectedClue() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]
        api.markerDetailResponses = [
            WildReportMarkerDetailResponse(
                markerType: "red",
                id: 32,
                title: "线索 ID：32",
                locationName: "空中草原观景道",
                latitude: "43.4068",
                longitude: "84.0582",
                distanceM: 320,
                reportTypeText: "疑似打野",
                handleStatus: WildReportDecodedHandleStatus(intValue: 0),
                desc: "多人结伴揽客",
                reporterName: "张三",
                reporterPhone: "13333333333",
                createdAt: "2026-07-08 16:06:59"
            )
        ]
        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
            scenicId: 128
        )
        let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })

        await viewModel.selectMarker(redMarker, api: api, scenicId: 128)

        XCTAssertEqual(api.markerDetailRequests.first?.markerType, "red")
        XCTAssertEqual(api.markerDetailRequests.first?.id, 32)
        XCTAssertEqual(viewModel.selectedClue?.reporterName, "张三")
        XCTAssertEqual(viewModel.selectedClue?.maskedReporterPhone, "133****3333")
        XCTAssertEqual(viewModel.selectedClue?.detail, "多人结伴揽客")
    }

    func testRiskMapViewModelMapsProcessingMarkerDetailForBottomCard() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]
        api.markerDetailResponses = [
            WildReportMarkerDetailResponse(
                markerType: "yellow",
                id: 33,
                clueId: "2026年7月9日-0039",
                title: "疑似打野摄影师举报",
                statusTag: "正在处理中",
                latitude: "32.3774100",
                longitude: "119.3979350",
                distanceM: 7068,
                reportTypeText: "疑似打野",
                desc: "色地方噶是的法规",
                reporterName: "游客",
                reporterPhone: "121****1212",
                createdAt: "2026-07-09 11:37:26",
                evidences: [
                    WildReportDetailEvidence(fileType: 1, fileTypeText: "图片", fileURL: "https://example.com/one.jpg"),
                    WildReportDetailEvidence(fileType: 1, fileTypeText: "图片", fileURL: "https://example.com/two.jpg")
                ]
            )
        ]

        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.success(latitude: 32.429943033854165, longitude: 119.44032552083333)),
            scenicId: 128
        )
        let yellowMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .processingClue })

        await viewModel.selectMarker(yellowMarker, api: api, scenicId: 128)

        XCTAssertEqual(api.markerDetailRequests.first?.markerType, "yellow")
        XCTAssertEqual(viewModel.selectedClue?.id, "2026年7月9日-0039")
        XCTAssertEqual(viewModel.selectedClue?.statusText, "正在处理中")
        XCTAssertEqual(viewModel.selectedClue?.reporterName, "游客")
        XCTAssertEqual(viewModel.selectedClue?.reporterPhone, "121****1212")
        XCTAssertEqual(viewModel.selectedClue?.reportContents.first?.content, "色地方噶是的法规")
        XCTAssertEqual(viewModel.selectedClue?.images, ["https://example.com/one.jpg", "https://example.com/two.jpg"])
        XCTAssertEqual(viewModel.selectedClue?.processingStarted, true)
    }

    func testRiskMapViewModelMarkerDetailFailureKeepsSummary() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]
        api.markerDetailError = URLError(.badServerResponse)
        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
            scenicId: 128
        )
        let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })

        await viewModel.selectMarker(redMarker, api: api, scenicId: 128)

        XCTAssertEqual(viewModel.selectedClue?.summary, "疑似打野")
        XCTAssertEqual(viewModel.errorMessage, URLError(.badServerResponse).localizedDescription)
    }

    func testRiskMapViewModelSharesSelectedReportMarker() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]
        api.reportShareResponse = WildReportMiniProgramShareData(
            userName: "gh_test",
            path: "/pages/report/detail?id=32",
            title: "举报线索",
            description: "请协助处理",
            webpageURL: "https://example.com/report/32",
            miniProgramTypeValue: WeChatMiniProgramType.preview.rawValue
        )

        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
            scenicId: 128
        )
        let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
        await viewModel.selectMarker(redMarker)

        let payload = await viewModel.shareSelectedClue(api: api)

        XCTAssertEqual(api.reportShareRequests, ["32"])
        XCTAssertEqual(payload?.title, "举报线索")
        XCTAssertEqual(payload?.path, "/pages/report/detail?id=32")
        XCTAssertEqual(payload?.miniProgramType, .preview)
    }

    func testRiskMapViewModelDoesNotShareCurrentLocationMarker() async {
        let viewModel = WildReportRiskMapViewModel()
        let api = MockWildPhotographerReportAPI()
        api.riskMapResponses = [makeRiskMapResponse()]

        await viewModel.loadInitial(
            api: api,
            locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
            scenicId: 128
        )

        let payload = await viewModel.shareSelectedClue(api: api)

        XCTAssertNil(payload)
        XCTAssertTrue(api.reportShareRequests.isEmpty)
        XCTAssertEqual(viewModel.errorMessage, "请选择可分享的举报线索")
    }

    func testDetailViewModelLoadsRemoteDetailWhenServerIDExists() async {
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let viewModel = WildPhotographerReportDetailViewModel(record: record)
        let api = MockWildPhotographerReportAPI()
        api.reportDetailResponses = [
            makeReportDetailData(
                handleStatus: 2,
                handleStatusText: "已处理",
                handler: WildReportDetailHandler(
                    id: 43,
                    name: "管铜彬",
                    phone: "132****9413",
                    startedAt: "2026-07-09 14:18:15",
                    finishedAt: "2026-07-09 14:18:40",
                    remark: "我是处理意见"
                ),
                handleEvidences: [
                    WildReportDetailEvidence(
                        fileType: 1,
                        fileTypeText: "图片",
                        fileURL: "http://tmp/handler-one.jpg",
                        fileName: "handler-one.jpg"
                    )
                ]
            )
        ]

        await viewModel.loadDetailIfNeeded(api: api)

        XCTAssertEqual(api.reportDetailRequests, [32])
        XCTAssertEqual(viewModel.record.description, "正式详情说明")
        XCTAssertEqual(viewModel.reporterMediaItems.count, 2)
        XCTAssertEqual(viewModel.paymentScreenshotItems.count, 1)
        XCTAssertEqual(viewModel.progressSteps.first?.title, "摄影师提交举报")
        XCTAssertTrue(viewModel.showHandlerInfo)
        XCTAssertEqual(viewModel.handlerInfo?.handlerName, "管铜彬")
        XCTAssertEqual(viewModel.handlerInfo?.processingAt, "2026-07-09 14:18:15")
        XCTAssertEqual(viewModel.handlerInfo?.completionMedia.first?.coverURL, "http://tmp/handler-one.jpg")
    }

    func testDetailViewModelFailureKeepsInitialRecord() async {
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let viewModel = WildPhotographerReportDetailViewModel(record: record)
        let api = MockWildPhotographerReportAPI()
        api.reportDetailError = URLError(.badServerResponse)
        var messages: [String] = []
        viewModel.onShowMessage = { messages.append($0) }

        await viewModel.loadDetailIfNeeded(api: api)

        XCTAssertEqual(viewModel.record.description, "测试说明")
        XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
    }

    func testDetailViewModelLoadsMiniProgramSharePayload() async throws {
        let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
        let viewModel = WildPhotographerReportDetailViewModel(record: record)
        let api = MockWildPhotographerReportAPI()
        api.reportShareResponse = WildReportMiniProgramShareData(
            userName: "gh_report_original_id",
            path: "pages/report/detail?id=32",
            title: "私下收款举报",
            webpageURL: "https://www.youfuntour.com/report/32",
            miniProgramTypeValue: WeChatMiniProgramType.test.rawValue
        )

        let maybePayload = await viewModel.shareReport(api: api)
        let payload = try XCTUnwrap(maybePayload)

        XCTAssertEqual(api.reportShareRequests, ["32"])
        XCTAssertEqual(payload.webpageURL, "https://www.youfuntour.com/report/32")
        XCTAssertEqual(payload.userName, "gh_report_original_id")
        XCTAssertEqual(payload.miniProgramType, .test)
    }

    func testDetailViewModelWithoutServerIDSkipsRequest() async {
        let record = WildReportRecord(
            id: "JBLOCAL",
            title: "摄影师举报",
            reportType: WildReportType(value: 2, label: "疑似打野"),
            status: .pending,
            location: "测试景区",
            submitTime: "2026-07-08 16:06:59",
            description: "本地提交记录",
            wechatID: "",
            phoneNumber: "",
            imageCount: 1,
            videoCount: 0,
            handlerInfo: nil
        )
        let viewModel = WildPhotographerReportDetailViewModel(record: record)
        let api = MockWildPhotographerReportAPI()

        await viewModel.loadDetailIfNeeded(api: api)

        XCTAssertTrue(api.reportDetailRequests.isEmpty)
        XCTAssertEqual(viewModel.record.description, "本地提交记录")
    }
}

@MainActor
private final class MockWildPhotographerReportAPI: WildPhotographerReportServing {
    var reportTypeResponse = WildReportTypeListResponse(list: [])
    var reportCopyResponse = WildReportCopyResponse()
    var reportCopyError: Error?
    var error: Error?
    var submitResponse = WildReportSubmitResponse()
    var submitRequests: [WildReportSubmitRequest] = []
    var reportListResponses: [WildReportListResponse] = []
    var reportListRequests: [WildReportListRequest] = []
    var reportListError: Error?
    var reportDetailResponses: [WildReportDetailData] = []
    var reportDetailRequests: [Int] = []
    var reportDetailError: Error?
    var reportShareResponse = WildReportMiniProgramShareData()
    var reportShareRequests: [String] = []
    var reportShareError: Error?
    var supplementRequests: [WildReportSupplementRequest] = []
    var supplementError: Error?
    var riskMapResponses: [WildReportRiskMapResponse] = []
    var riskMapRequests: [WildReportRiskMapRequest] = []
    var riskMapError: Error?
    var markerDetailResponses: [WildReportMarkerDetailResponse] = []
    var markerDetailRequests: [WildReportMarkerDetailRequest] = []
    var markerDetailError: Error?

    func reportTypes() async throws -> WildReportTypeListResponse {
        if let error { throw error }
        return reportTypeResponse
    }

    func reportCopy() async throws -> WildReportCopyResponse {
        if let reportCopyError { throw reportCopyError }
        return reportCopyResponse
    }

    func submitReport(_ request: WildReportSubmitRequest) async throws -> WildReportSubmitResponse {
        if let error { throw error }
        submitRequests.append(request)
        return submitResponse
    }

    func supplementReport(_ request: WildReportSupplementRequest) async throws {
        if let supplementError { throw supplementError }
        supplementRequests.append(request)
    }

    func riskMap(_ request: WildReportRiskMapRequest) async throws -> WildReportRiskMapResponse {
        if let riskMapError { throw riskMapError }
        riskMapRequests.append(request)
        guard !riskMapResponses.isEmpty else {
            return WildReportRiskMapResponse(scenicId: request.scenicId, scenicName: "测试景区")
        }
        return riskMapResponses.removeFirst()
    }

    func markerDetail(_ request: WildReportMarkerDetailRequest) async throws -> WildReportMarkerDetailResponse {
        if let markerDetailError { throw markerDetailError }
        markerDetailRequests.append(request)
        guard !markerDetailResponses.isEmpty else {
            return WildReportMarkerDetailResponse(markerType: request.markerType, id: request.id)
        }
        return markerDetailResponses.removeFirst()
    }

    func reportList(_ request: WildReportListRequest) async throws -> WildReportListResponse {
        if let reportListError { throw reportListError }
        reportListRequests.append(request)
        guard !reportListResponses.isEmpty else {
            return WildReportListResponse(list: [], total: 0, page: request.page, pageSize: request.pageSize)
        }
        return reportListResponses.removeFirst()
    }

    func reportDetail(id: Int) async throws -> WildReportDetailData {
        if let reportDetailError { throw reportDetailError }
        reportDetailRequests.append(id)
        guard !reportDetailResponses.isEmpty else { return WildReportDetailData(id: id) }
        return reportDetailResponses.removeFirst()
    }

    func reportShare(id: String) async throws -> WildReportMiniProgramShareData {
        if let reportShareError { throw reportShareError }
        reportShareRequests.append(id)
        return reportShareResponse
    }
}

private enum MockRiskMapLocationResult: Sendable {
    case success(latitude: Double, longitude: Double)
    case failure
}

private final class MockRiskMapLocationProvider: LocationProviding, @unchecked Sendable {
    private let result: MockRiskMapLocationResult
    private let onCoordinateRequest: (@Sendable (CLLocationAccuracy) async -> Void)?
    private let lock = NSLock()
    private var recordedAccuracies: [CLLocationAccuracy] = []

    var requestedAccuracies: [CLLocationAccuracy] {
        lock.withLock { recordedAccuracies }
    }

    init(
        _ result: MockRiskMapLocationResult,
        onCoordinateRequest: (@Sendable (CLLocationAccuracy) async -> Void)? = nil
    ) {
        self.result = result
        self.onCoordinateRequest = onCoordinateRequest
    }

    func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
        switch result {
        case .success(let latitude, let longitude):
            return HomeLocationSnapshot(latitude: latitude, longitude: longitude, address: "测试地址")
        case .failure:
            throw LocationProviderError.locationFailed
        }
    }

    func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
        lock.withLock {
            recordedAccuracies.append(desiredAccuracy)
        }
        await onCoordinateRequest?(desiredAccuracy)
        switch result {
        case .success(let latitude, let longitude):
            return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
        case .failure:
            throw LocationProviderError.locationFailed
        }
    }

    func reverseGeocode(latitude: Double, longitude: Double) async -> String {
        "测试地址"
    }
}

@MainActor
private final class MockWildReportAttachmentUploader: WildReportAttachmentUploading {
    var uploadedFileNames: [String] = []
    var error: Error?

    func uploadWildReportAttachment(
        data: Data,
        fileName: String,
        fileType: Int,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String {
        if let error { throw error }
        onProgress(100)
        uploadedFileNames.append(fileName)
        return "https://oss.example.com/\(fileName)"
    }
}

private func makeAttachment(kind: WildReportAttachmentKind, fileName: String) throws -> WildReportAttachment {
    let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName)
    try Data("mock".utf8).write(to: url)
    return WildReportAttachment(
        kind: kind,
        title: fileName,
        localFileURL: url,
        fileName: fileName
    )
}

private func makeRiskMapResponse() -> WildReportRiskMapResponse {
    WildReportRiskMapResponse(
        scenicId: 128,
        scenicName: "伊犁那拉提景区-5A",
        legend: [WildReportRiskLegendItem(markerType: "red", title: "疑似打野")],
        greenMarkerTip: "绿色为景区内摄影师",
        markers: [
            WildReportRiskMapMarkerDTO(
                markerType: "red",
                id: 32,
                latitude: "43.4068",
                longitude: "84.0582",
                locationName: "空中草原观景道",
                title: "线索 ID：32",
                reportTypeText: "疑似打野",
                handleStatus: WildReportDecodedHandleStatus(intValue: 0),
                distanceM: 320,
                createdAt: "2026-07-08 16:06:59"
            ),
            WildReportRiskMapMarkerDTO(
                markerType: "yellow",
                id: 33,
                latitude: "43.3652",
                longitude: "84.0385",
                locationName: "塔吾萨尼步道",
                title: "线索 ID：33",
                reportTypeText: "处理中",
                handleStatus: WildReportDecodedHandleStatus(intValue: 1),
                distanceM: 1280,
                createdAt: "2026-07-08 16:08:59"
            ),
            WildReportRiskMapMarkerDTO(
                markerType: "green",
                id: 901,
                latitude: "43.4042",
                longitude: "84.0186",
                locationName: "空中草原",
                title: "景区内摄影师",
                distanceM: 430,
                createdAt: "2026-07-08 16:09:59",
                name: "阿依达娜",
                storeName: "空中草原旅拍店",
                online: true,
                activityText: "当前在空中草原区域服务游客拍摄。"
            )
        ]
    )
}

private func makeReportListItem(
    id: Int = 32,
    complaintNo: String,
    title: String = "疑似打野摄影师举报",
    reportType: Int = 2,
    reportTypeText: String = "疑似打野",
    handleStatus: Int,
    handleStatusText: String = "待处理"
) -> WildReportListItem {
    WildReportListItem(
        id: id,
        complaintNo: complaintNo,
        title: title,
        reportType: reportType,
        reportTypeText: reportTypeText,
        desc: "测试说明",
        scenicId: 128,
        scenicName: "伊犁那拉提景区-5A",
        locationName: "伊犁那拉提景区-5A",
        latitude: "32.4299428",
        longitude: "119.4403263",
        handleStatus: handleStatus,
        handleStatusText: handleStatusText,
        createdAt: "2026-07-08 16:06:59"
    )
}

private func makeReportDetailData(
    handleStatus: Int = 0,
    handleStatusText: String = "待处理",
    handleRemark: String = "处理备注",
    handledAt: String = "",
    handler: WildReportDetailHandler? = nil,
    handlerName: String = "",
    handlerPhone: String = "",
    handleEvidences: [WildReportDetailEvidence] = []
) -> WildReportDetailData {
    WildReportDetailData(
        id: 32,
        complaintNo: "JB20260708917",
        title: "私下收款举报",
        reportType: 1,
        reportTypeText: "私下收款",
        desc: "正式详情说明",
        supplements: [
            WildReportDetailContent(
                type: "supplement",
                id: 7,
                desc: "补充说明",
                time: "2026-07-08 17:06:59",
                timeLabel: "17:06:59",
                evidences: [
                    WildReportDetailEvidence(
                        fileType: 1,
                        fileTypeText: "图片",
                        fileURL: "https://example.com/supplement.jpg",
                        fileName: "supplement.jpg"
                    )
                ]
            )
        ],
        reportContents: [],
        scenicId: 128,
        scenicName: "伊犁那拉提景区-5A",
        storeUserId: 0,
        contactPhone: "13800138000",
        locationName: "伊犁那拉提景区-5A",
        locationAddress: "江苏省扬州市邗江区瘦西湖路",
        latitude: "32.4299428",
        longitude: "119.4403263",
        handleStatus: handleStatus,
        handleStatusText: handleStatusText,
        handleRemark: handleRemark,
        handledAt: handledAt,
        handler: handler,
        handlerName: handlerName,
        handlerPhone: handlerPhone,
        evidenceSummary: WildReportEvidenceSummary(imageCount: 1, videoCount: 1),
        evidences: [
            WildReportDetailEvidence(
                fileType: 1,
                fileTypeText: "图片",
                fileURL: "https://example.com/report.jpg",
                fileName: "report.jpg"
            ),
            WildReportDetailEvidence(
                fileType: 2,
                fileTypeText: "视频",
                fileURL: "https://example.com/report.mov",
                fileName: "report.mov"
            )
        ],
        handleEvidences: handleEvidences,
        paymentScreenshots: [
            WildReportDetailEvidence(
                fileType: 5,
                fileTypeText: "支付截图",
                fileURL: "https://example.com/payment.jpg",
                fileName: "payment.jpg"
            )
        ],
        timeline: [
            WildReportDetailTimelineItem(time: "2026-07-08 16:06:59", content: "摄影师提交举报")
        ],
        createdAt: "2026-07-08 16:06:59"
    )
}
