from __future__ import annotations

import io
import json
import threading
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from pathlib import Path

import pytest


PDF_BYTES = b"%PDF-1.4\n1 0 obj\n<<>>\nendobj\n%%EOF\n"


def _headers(token: str) -> dict[str, str]:
    return {"Authorization": f"Bearer {token}"}


def _category_id(app) -> str:
    from dms.extensions import db
    from dms.models import Category

    with app.app_context():
        category = db.session.scalar(
            db.select(Category).where(Category.category_code == "CHILD_A1")
        )
        assert category is not None
        return str(category.id)


def _upload_plan(
    client,
    token: str,
    category_id: str,
    name: str,
    *,
    document_type: str = "MAIN",
    parent_id: str | None = None,
    visibility: str = "ALL_AUTHENTICATED",
):
    metadata = {
        "documentName": name,
        "documentType": document_type,
        "summary": f"{name}摘要",
        "securityLevel": "INTERNAL",
        "tags": ["B8"],
    }
    if document_type == "MAIN":
        metadata["categoryId"] = category_id
    if document_type == "SUB_PLAN" and parent_id is not None:
        metadata["parentDocumentId"] = parent_id
    response = client.post(
        "/api/v1/documents",
        data={
            "file": (io.BytesIO(PDF_BYTES), f"{name}.pdf"),
            "metadata": json.dumps(metadata, ensure_ascii=False),
        },
        headers=_headers(token),
        content_type="multipart/form-data",
    )
    assert response.status_code == 201, response.get_json()
    document = response.get_json()["data"]
    if document_type == "MAIN" and visibility == "ALL_AUTHENTICATED":
        permission = client.put(
            f"/api/v1/documents/{document['id']}/permissions",
            json={
                "visibilityType": "ALL_AUTHENTICATED",
                "documentRowVersion": document["rowVersion"],
                "entries": [],
            },
            headers=_headers(token),
        )
        assert permission.status_code == 200, permission.get_json()
        document["rowVersion"] = permission.get_json()["data"][
            "documentRowVersion"
        ]
        document["visibilityType"] = "ALL_AUTHENTICATED"
    return document


def _upload_attachment(client, token: str, name: str):
    response = client.post(
        "/api/v1/attachments",
        data={
            "file": (io.BytesIO(PDF_BYTES), f"{name}.pdf"),
            "metadata": json.dumps(
                {
                    "documentName": name,
                    "attachmentType": "OTHER",
                    "summary": f"{name}摘要",
                    "tags": ["B8"],
                },
                ensure_ascii=False,
            ),
        },
        headers=_headers(token),
        content_type="multipart/form-data",
    )
    assert response.status_code == 201, response.get_json()
    return response.get_json()["data"]


def _delete(client, token: str, document: dict, *, attachment: bool = False):
    prefix = "/api/v1/attachments" if attachment else "/api/v1/documents"
    response = client.delete(
        f"{prefix}/{document['id']}?rowVersion={document['rowVersion']}",
        headers=_headers(token),
    )
    assert response.status_code == 200, response.get_json()
    return response.get_json()["data"]


def _deleted_document(app, document_id: str):
    from dms.extensions import db
    from dms.models import Document

    with app.app_context():
        document = db.session.get(Document, int(document_id))
        assert document is not None and document.is_deleted
        return {
            "rowVersion": document.row_version,
            "deletedAt": document.deleted_at,
            "fileRelativePath": document.file_relative_path,
            "viewCount": document.view_count,
        }


def _restore(client, token: str, document_id: str, row_version: int):
    return client.post(
        f"/api/v1/recycle-bin/documents/{document_id}/restore",
        json={"rowVersion": row_version},
        headers=_headers(token),
    )


def test_recycle_bin_auth_query_filters_and_deleted_by(
    b3_app, b3_client, login_token, token_for
):
    category_id = _category_id(b3_app)
    normal = _upload_plan(
        b3_client, login_token, category_id, "B8正常文档"
    )
    deleted = _upload_plan(
        b3_client, login_token, category_id, "B8_百分号%_下划线_"
    )
    _delete(b3_client, login_token, deleted)

    assert b3_client.get("/api/v1/recycle-bin/documents").status_code == 401
    for username in ("user",):
        response = b3_client.get(
            "/api/v1/recycle-bin/documents",
            headers=_headers(token_for(username)),
        )
        assert response.status_code == 403
        assert response.get_json()["code"] == "FORBIDDEN"

    response = b3_client.get(
        "/api/v1/recycle-bin/documents",
        query_string={
            "keyword": "百分号%",
            "documentType": "MAIN",
            "categoryId": category_id,
            "page": "1",
            "pageSize": "20",
            "sortField": "documentName",
            "sortDirection": "asc",
        },
        headers=_headers(login_token),
    )
    assert response.status_code == 200, response.get_json()
    body = response.get_json()
    assert body["data"]["total"] == 1
    item = body["data"]["items"][0]
    assert item["id"] == deleted["id"]
    assert item["id"] != normal["id"]
    assert item["documentStatus"] == "PUBLISHED"
    assert item["deletedBy"]["username"] == "admin"
    assert "fileRelativePath" not in item
    assert "fileHash" not in item
    assert isinstance(item["id"], str)


@pytest.mark.parametrize(
    ("query", "message_part"),
    [
        ("unknown=x", "未知"),
        ("page=1&page=2", "重复"),
        ("documentType=OTHER", "documentType"),
        ("categoryId=0", "categoryId"),
        ("deletedFrom=2026-01-01T00:00:00%2B08:00", "UTC Z"),
        ("deletedFrom=2026-01-02T00:00:00Z&deletedTo=2026-01-01T00:00:00Z", "早于"),
        ("pageSize=101", "100"),
        ("sortField=id", "sortField"),
        ("sortDirection=sideways", "sortDirection"),
    ],
)
def test_recycle_bin_rejects_invalid_query(
    b3_client, login_token, query, message_part
):
    response = b3_client.get(
        f"/api/v1/recycle-bin/documents?{query}",
        headers=_headers(login_token),
    )
    assert response.status_code == 400
    assert response.get_json()["code"] == "INVALID_ARGUMENT"
    assert message_part in response.get_json()["message"]


def test_restore_main_restores_delete_batch_acl_binding_counts_and_audit(
    b3_app, b3_client, login_token
):
    from dms.extensions import db
    from dms.models import (
        AttachmentBinding,
        AuditLog,
        Category,
        Document,
        Organization,
        Permission,
    )

    category_id = _category_id(b3_app)
    main = _upload_plan(
        b3_client,
        login_token,
        category_id,
        "B8关系主案",
        visibility="ORGANIZATION",
    )
    attachment = _upload_attachment(b3_client, login_token, "B8关系附件")
    with b3_app.app_context():
        organization = db.session.scalar(
            db.select(Organization).where(Organization.org_code == "ORG_ROOT")
        )
        assert organization is not None
        organization_id = str(organization.id)

    permission_response = b3_client.put(
        f"/api/v1/documents/{main['id']}/permissions",
        json={
            "visibilityType": "ORGANIZATION",
            "documentRowVersion": main["rowVersion"],
            "entries": [
                {
                    "subjectType": "ORG",
                    "subjectId": organization_id,
                    "canView": True,
                    "canDownload": True,
                    "canEdit": False,
                    "canManagePermission": False,
                    "canDelete": False,
                }
            ],
        },
        headers=_headers(login_token),
    )
    assert permission_response.status_code == 200, permission_response.get_json()
    main_version = permission_response.get_json()["data"]["documentRowVersion"]
    bind_response = b3_client.post(
        f"/api/v1/main-plans/{main['id']}/attachments/bind",
        json={
            "attachmentIds": [attachment["id"]],
            "mainPlanRowVersion": main_version,
        },
        headers=_headers(login_token),
    )
    assert bind_response.status_code == 200, bind_response.get_json()
    main["rowVersion"] = bind_response.get_json()["data"]["mainPlanRowVersion"]
    _delete(b3_client, login_token, main)
    deleted = _deleted_document(b3_app, main["id"])

    response = _restore(
        b3_client, login_token, main["id"], deleted["rowVersion"]
    )
    assert response.status_code == 200, response.get_json()
    data = response.get_json()["data"]
    assert data["restoredPermissionCount"] == 1
    assert data["skippedPermissionCount"] == 0
    assert data["restoredBindingCount"] == 1
    assert data["skippedBindingCount"] == 0
    assert data["document"]["attachmentCount"] == 1
    assert data["document"]["viewCount"] == deleted["viewCount"]
    assert "fileHash" not in data["document"]

    with b3_app.app_context():
        document = db.session.get(Document, int(main["id"]))
        category = db.session.get(Category, int(category_id))
        permission = db.session.scalar(
            db.select(Permission).where(
                Permission.document_id == int(main["id"]),
                Permission.is_deleted.is_(False),
            )
        )
        binding = db.session.scalar(
            db.select(AttachmentBinding).where(
                AttachmentBinding.main_document_id == int(main["id"]),
                AttachmentBinding.is_deleted.is_(False),
            )
        )
        audit = db.session.scalar(
            db.select(AuditLog)
            .where(
                AuditLog.action_type == "RESTORE_DOCUMENT",
                AuditLog.target_id == int(main["id"]),
            )
            .order_by(AuditLog.id.desc())
        )
        view_audits = db.session.scalar(
            db.select(db.func.count(AuditLog.id)).where(
                AuditLog.action_type == "VIEW_DOCUMENT",
                AuditLog.target_id == int(main["id"]),
            )
        )
        assert document is not None and not document.is_deleted
        assert document.row_version == deleted["rowVersion"] + 1
        assert category is not None and category.document_count >= 1
        assert permission is not None and permission.row_version == 2
        assert binding is not None and binding.row_version == 2
        assert audit is not None
        assert audit.operation_detail["restoredPermissionCount"] == 1
        assert "fileHash" not in audit.operation_detail
        assert view_audits == 0


def test_restore_sub_plan_recounts_parent_and_inherits_current_acl(
    b3_app, b3_client, login_token
):
    from dms.extensions import db
    from dms.models import Document, Permission

    category_id = _category_id(b3_app)
    main = _upload_plan(
        b3_client, login_token, category_id, "B8子案父主案"
    )
    sub = _upload_plan(
        b3_client,
        login_token,
        category_id,
        "B8待恢复子案",
        document_type="SUB_PLAN",
        parent_id=main["id"],
    )
    _delete(b3_client, login_token, sub)
    deleted = _deleted_document(b3_app, sub["id"])
    with b3_app.app_context():
        parent_before = db.session.get(Document, int(main["id"])).row_version

    response = _restore(
        b3_client, login_token, sub["id"], deleted["rowVersion"]
    )
    assert response.status_code == 200, response.get_json()
    data = response.get_json()["data"]
    assert data["restoredPermissionCount"] == 0
    with b3_app.app_context():
        restored = db.session.get(Document, int(sub["id"]))
        parent = db.session.get(Document, int(main["id"]))
        acl_count = db.session.scalar(
            db.select(db.func.count(Permission.id)).where(
                Permission.document_id == restored.id,
                Permission.is_deleted.is_(False),
            )
        )
        assert restored.row_version == deleted["rowVersion"] + 1
        assert parent.child_count == 1
        assert parent.row_version == parent_before + 1
        assert acl_count == 0


def test_restore_attachment_does_not_restore_historical_binding(
    b3_app, b3_client, login_token
):
    from dms.extensions import db
    from dms.models import AttachmentBinding, Document

    category_id = _category_id(b3_app)
    main = _upload_plan(
        b3_client, login_token, category_id, "B8附件挂载主案"
    )
    attachment = _upload_attachment(b3_client, login_token, "B8待恢复附件")
    bind = b3_client.post(
        f"/api/v1/main-plans/{main['id']}/attachments/bind",
        json={
            "attachmentIds": [attachment["id"]],
            "mainPlanRowVersion": main["rowVersion"],
        },
        headers=_headers(login_token),
    ).get_json()["data"]
    unbind = b3_client.delete(
        f"/api/v1/main-plans/{main['id']}/attachments/{attachment['id']}"
        f"?mainPlanRowVersion={bind['mainPlanRowVersion']}",
        headers=_headers(login_token),
    )
    assert unbind.status_code == 200
    _delete(b3_client, login_token, attachment, attachment=True)
    deleted = _deleted_document(b3_app, attachment["id"])
    response = _restore(
        b3_client, login_token, attachment["id"], deleted["rowVersion"]
    )
    assert response.status_code == 200, response.get_json()
    data = response.get_json()["data"]
    assert data["restoredPermissionCount"] == 0
    assert data["restoredBindingCount"] == 0
    assert data["document"]["mountedPlanCount"] == 0
    with b3_app.app_context():
        restored = db.session.get(Document, int(attachment["id"]))
        binding = db.session.scalar(
            db.select(AttachmentBinding).where(
                AttachmentBinding.attachment_document_id == restored.id
            )
        )
        assert restored.security_level == "PUBLIC"
        assert restored.visibility_type == "ALL_AUTHENTICATED"
        assert restored.category_id is None
        assert binding is not None and binding.is_deleted


def test_restore_validates_body_state_and_old_path_stays_404(
    b3_app, b3_client, login_token
):
    category_id = _category_id(b3_app)
    document = _upload_plan(
        b3_client, login_token, category_id, "B8请求校验"
    )
    response = _restore(
        b3_client, login_token, document["id"], document["rowVersion"]
    )
    assert response.status_code == 409
    assert response.get_json()["code"] == "DOCUMENT_NOT_DELETED"
    assert (
        b3_client.post(
            f"/api/v1/documents/{document['id']}/restore",
            json={"rowVersion": document["rowVersion"]},
            headers=_headers(login_token),
        ).status_code
        == 404
    )
    _delete(b3_client, login_token, document)
    deleted = _deleted_document(b3_app, document["id"])
    for payload in (
        {},
        {"rowVersion": True},
        {"rowVersion": "1"},
        {"rowVersion": 1.5},
        {"rowVersion": -1},
        {"rowVersion": deleted["rowVersion"], "extra": 1},
    ):
        invalid = b3_client.post(
            f"/api/v1/recycle-bin/documents/{document['id']}/restore",
            json=payload,
            headers=_headers(login_token),
        )
        assert invalid.status_code == 400
        assert invalid.get_json()["code"] == "INVALID_ARGUMENT"
    conflict = _restore(
        b3_client, login_token, document["id"], deleted["rowVersion"] - 1
    )
    assert conflict.status_code == 409
    assert conflict.get_json()["code"] == "DATA_VERSION_CONFLICT"
    assert (
        conflict.get_json()["details"]["currentRowVersion"]
        == deleted["rowVersion"]
    )


@pytest.mark.parametrize(
    ("mutation", "expected_code", "expected_status"),
    [
        ("missing", "FILE_NOT_FOUND", 404),
        ("size", "FILE_INTEGRITY_MISMATCH", 409),
        ("hash", "FILE_INTEGRITY_MISMATCH", 409),
        ("path", "FILE_PATH_INVALID", 500),
        ("type", "FILE_INTEGRITY_MISMATCH", 409),
    ],
)
def test_restore_file_failures_are_safe_and_rollback(
    b3_app, b3_client, login_token, mutation, expected_code, expected_status
):
    from dms.extensions import db
    from dms.models import Document
    from dms.storage.paths import resolve_storage_path

    category_id = _category_id(b3_app)
    document = _upload_plan(
        b3_client, login_token, category_id, f"B8文件失败{mutation}"
    )
    _delete(b3_client, login_token, document)
    deleted = _deleted_document(b3_app, document["id"])
    with b3_app.app_context():
        row = db.session.get(Document, int(document["id"]))
        path = resolve_storage_path(
            row.file_relative_path, b3_app.config["DMS_STORAGE_ROOT"]
        )
        if mutation == "missing":
            path.unlink()
        elif mutation == "size":
            path.write_bytes(PDF_BYTES + b"x")
        elif mutation == "hash":
            changed = bytearray(PDF_BYTES)
            changed[-2] = ord("X")
            path.write_bytes(changed)
        elif mutation == "path":
            row.file_relative_path = "../outside.pdf"
            db.session.commit()
        elif mutation == "type":
            path.write_bytes(b"NOT_A_PDF" + PDF_BYTES)

    response = _restore(
        b3_client, login_token, document["id"], deleted["rowVersion"]
    )
    assert response.status_code == expected_status
    body = response.get_json()
    assert body["code"] == expected_code
    serialized = json.dumps(body, ensure_ascii=False)
    assert str(Path(b3_app.config["DMS_STORAGE_ROOT"])) not in serialized
    assert "a" * 64 not in serialized
    with b3_app.app_context():
        assert db.session.get(Document, int(document["id"])).is_deleted


def test_restore_file_recheck_and_audit_failure_roll_back(
    b3_app, b3_client, login_token, monkeypatch
):
    from dms.common.errors import RestoreFileChangedError
    from dms.extensions import db
    from dms.models import AuditLog, Document
    from dms.services import recycle_bin_service

    category_id = _category_id(b3_app)
    first = _upload_plan(
        b3_client, login_token, category_id, "B8文件竞态"
    )
    _delete(b3_client, login_token, first)
    first_deleted = _deleted_document(b3_app, first["id"])
    monkeypatch.setattr(
        recycle_bin_service,
        "_recheck_file",
        lambda *_args: (_ for _ in ()).throw(RestoreFileChangedError()),
    )
    response = _restore(
        b3_client, login_token, first["id"], first_deleted["rowVersion"]
    )
    assert response.status_code == 409
    assert response.get_json()["code"] == "RESTORE_FILE_CHANGED"
    with b3_app.app_context():
        assert db.session.get(Document, int(first["id"])).is_deleted
    monkeypatch.undo()

    second = _upload_plan(
        b3_client, login_token, category_id, "B8审计回滚"
    )
    _delete(b3_client, login_token, second)
    second_deleted = _deleted_document(b3_app, second["id"])

    def fail_audit(**_kwargs):
        raise RuntimeError("audit failed")

    monkeypatch.setattr(recycle_bin_service, "business_audit", fail_audit)
    audit_failure = _restore(
        b3_client, login_token, second["id"], second_deleted["rowVersion"]
    )
    assert audit_failure.status_code == 500
    assert audit_failure.get_json()["code"] == "INTERNAL_ERROR"
    with b3_app.app_context():
        assert db.session.get(Document, int(second["id"])).is_deleted
        count = db.session.scalar(
            db.select(db.func.count(AuditLog.id)).where(
                AuditLog.action_type == "RESTORE_DOCUMENT",
                AuditLog.target_id == int(second["id"]),
            )
        )
        assert count == 0


def test_restore_invalid_category_and_parent_roll_back(
    b3_app, b3_client, login_token
):
    from dms.extensions import db
    from dms.models import Category, Document

    category_id = _category_id(b3_app)
    main = _upload_plan(
        b3_client, login_token, category_id, "B8失效分类"
    )
    _delete(b3_client, login_token, main)
    deleted_main = _deleted_document(b3_app, main["id"])
    with b3_app.app_context():
        category = db.session.get(Category, int(category_id))
        category.status = "DISABLED"
        db.session.commit()
    invalid_category = _restore(
        b3_client, login_token, main["id"], deleted_main["rowVersion"]
    )
    assert invalid_category.status_code == 409
    assert invalid_category.get_json()["code"] == "RESTORE_CATEGORY_INVALID"
    with b3_app.app_context():
        category = db.session.get(Category, int(category_id))
        category.status = "ENABLED"
        db.session.commit()

    parent = _upload_plan(
        b3_client, login_token, category_id, "B8失效父主案"
    )
    sub = _upload_plan(
        b3_client,
        login_token,
        category_id,
        "B8父级失效子案",
        document_type="SUB_PLAN",
        parent_id=parent["id"],
    )
    _delete(b3_client, login_token, sub)
    deleted_sub = _deleted_document(b3_app, sub["id"])
    with b3_app.app_context():
        parent_row = db.session.get(Document, int(parent["id"]))
        parent_row.is_deleted = True
        parent_row.deleted_at = datetime.now(timezone.utc).replace(tzinfo=None)
        db.session.commit()
    invalid_parent = _restore(
        b3_client, login_token, sub["id"], deleted_sub["rowVersion"]
    )
    assert invalid_parent.status_code == 409
    assert invalid_parent.get_json()["code"] == "RESTORE_PARENT_INVALID"


def test_restore_custom_skips_disabled_subject_but_organization_rejects(
    b3_app, b3_client, login_token
):
    from dms.extensions import db
    from dms.models import Organization, User

    category_id = _category_id(b3_app)
    with b3_app.app_context():
        user = db.session.scalar(db.select(User).where(User.username == "user"))
        organization = db.session.scalar(
            db.select(Organization).where(Organization.org_code == "ORG_ROOT")
        )
        assert user is not None and organization is not None
        user_id = str(user.id)
        organization_id = str(organization.id)

    def permission(document, visibility, subject_type, subject_id):
        response = b3_client.put(
            f"/api/v1/documents/{document['id']}/permissions",
            json={
                "visibilityType": visibility,
                "documentRowVersion": document["rowVersion"],
                "entries": [
                    {
                        "subjectType": subject_type,
                        "subjectId": subject_id,
                        "canView": True,
                        "canDownload": False,
                        "canEdit": False,
                        "canManagePermission": False,
                        "canDelete": False,
                    }
                ],
            },
            headers=_headers(login_token),
        )
        assert response.status_code == 200
        document["rowVersion"] = response.get_json()["data"]["documentRowVersion"]

    custom = _upload_plan(
        b3_client,
        login_token,
        category_id,
        "B8自定义跳过",
        visibility="CUSTOM",
    )
    permission(custom, "CUSTOM", "USER", user_id)
    _delete(b3_client, login_token, custom)
    custom_deleted = _deleted_document(b3_app, custom["id"])
    with b3_app.app_context():
        user = db.session.get(User, int(user_id))
        user.status = "DISABLED"
        db.session.commit()
    custom_restore = _restore(
        b3_client, login_token, custom["id"], custom_deleted["rowVersion"]
    )
    assert custom_restore.status_code == 200, custom_restore.get_json()
    assert custom_restore.get_json()["data"]["skippedPermissionCount"] == 1

    organization_main = _upload_plan(
        b3_client,
        login_token,
        category_id,
        "B8组织权限拒绝",
        visibility="ORGANIZATION",
    )
    permission(
        organization_main, "ORGANIZATION", "ORG", organization_id
    )
    _delete(b3_client, login_token, organization_main)
    organization_deleted = _deleted_document(
        b3_app, organization_main["id"]
    )
    with b3_app.app_context():
        organization = db.session.get(Organization, int(organization_id))
        organization.status = "DISABLED"
        db.session.commit()
    rejected = _restore(
        b3_client,
        login_token,
        organization_main["id"],
        organization_deleted["rowVersion"],
    )
    assert rejected.status_code == 409
    assert rejected.get_json()["code"] == "RESTORE_PERMISSION_INVALID"


def test_deleted_by_is_null_without_reliable_audit_and_query_has_no_n_plus_one(
    b3_app, b3_client, login_token
):
    from sqlalchemy import event

    from dms.extensions import db
    from dms.models import AuditLog

    category_id = _category_id(b3_app)
    documents = [
        _upload_plan(
            b3_client, login_token, category_id, f"B8批量删除人{i}"
        )
        for i in range(3)
    ]
    for document in documents:
        _delete(b3_client, login_token, document)
    with b3_app.app_context():
        db.session.execute(
            db.delete(AuditLog).where(
                AuditLog.action_type == "DELETE_DOCUMENT",
                AuditLog.target_id == int(documents[0]["id"]),
            )
        )
        db.session.commit()

    select_count = 0

    def count_selects(
        _conn, _cursor, statement, _parameters, _context, _executemany
    ):
        nonlocal select_count
        if statement.lstrip().upper().startswith("SELECT"):
            select_count += 1

    with b3_app.app_context():
        event.listen(db.engine, "before_cursor_execute", count_selects)
    try:
        response = b3_client.get(
            "/api/v1/recycle-bin/documents?pageSize=100",
            headers=_headers(login_token),
        )
    finally:
        with b3_app.app_context():
            event.remove(db.engine, "before_cursor_execute", count_selects)
    assert response.status_code == 200
    items = {
        item["id"]: item for item in response.get_json()["data"]["items"]
    }
    assert items[documents[0]["id"]]["deletedBy"] is None
    assert items[documents[1]["id"]]["deletedBy"]["username"] == "admin"
    # Token校验1次、分页count 1次、文档页1次、窗口审计1次。
    assert select_count <= 4


def test_restore_relation_conflicts_and_all_authenticated_acl_roll_back(
    b3_app, b3_client, login_token
):
    from dms.extensions import db
    from dms.models import Document, Organization, Permission

    category_id = _category_id(b3_app)
    with b3_app.app_context():
        organization = db.session.scalar(
            db.select(Organization).where(Organization.org_code == "ORG_ROOT")
        )
        assert organization is not None
        organization_id = str(organization.id)

    main = _upload_plan(
        b3_client,
        login_token,
        category_id,
        "B8权限关系冲突",
        visibility="ORGANIZATION",
    )
    permission = b3_client.put(
        f"/api/v1/documents/{main['id']}/permissions",
        json={
            "visibilityType": "ORGANIZATION",
            "documentRowVersion": main["rowVersion"],
            "entries": [
                {
                    "subjectType": "ORG",
                    "subjectId": organization_id,
                    "canView": True,
                    "canDownload": False,
                    "canEdit": False,
                    "canManagePermission": False,
                    "canDelete": False,
                }
            ],
        },
        headers=_headers(login_token),
    )
    main["rowVersion"] = permission.get_json()["data"]["documentRowVersion"]
    _delete(b3_client, login_token, main)
    deleted = _deleted_document(b3_app, main["id"])
    with b3_app.app_context():
        db.session.add(
            Permission(
                document_id=int(main["id"]),
                subject_type="ORG",
                subject_id=int(organization_id),
                subject_name="并发新权限",
                can_view=True,
            )
        )
        db.session.commit()
    conflict = _restore(
        b3_client, login_token, main["id"], deleted["rowVersion"]
    )
    assert conflict.status_code == 409
    assert conflict.get_json()["code"] == "RESTORE_RELATION_CONFLICT"
    assert conflict.get_json()["details"]["permissionConflictCount"] == 1
    with b3_app.app_context():
        assert db.session.get(Document, int(main["id"])).is_deleted

    all_main = _upload_plan(
        b3_client,
        login_token,
        category_id,
        "B8全员历史ACL",
        visibility="CUSTOM",
    )
    saved = b3_client.put(
        f"/api/v1/documents/{all_main['id']}/permissions",
        json={
            "visibilityType": "CUSTOM",
            "documentRowVersion": all_main["rowVersion"],
            "entries": [
                {
                    "subjectType": "ORG",
                    "subjectId": organization_id,
                    "canView": True,
                    "canDownload": False,
                    "canEdit": False,
                    "canManagePermission": False,
                    "canDelete": False,
                }
            ],
        },
        headers=_headers(login_token),
    ).get_json()["data"]
    # 构造历史异常数据：全员可见文档仍残留有效ACL。
    with b3_app.app_context():
        document = db.session.get(Document, int(all_main["id"]))
        document.visibility_type = "ALL_AUTHENTICATED"
        document.row_version = saved["documentRowVersion"] + 1
        db.session.commit()
        all_main["rowVersion"] = document.row_version
        all_main["visibilityType"] = document.visibility_type
    _delete(b3_client, login_token, all_main)
    all_deleted = _deleted_document(b3_app, all_main["id"])
    invalid = _restore(
        b3_client, login_token, all_main["id"], all_deleted["rowVersion"]
    )
    assert invalid.status_code == 409
    assert invalid.get_json()["code"] == "RESTORE_PERMISSION_INVALID"
    assert (
        invalid.get_json()["details"]["reason"]
        == "ALL_AUTHENTICATED_HAS_PERMISSIONS"
    )


def test_restore_attachment_rejects_anomalous_active_binding(
    b3_app, b3_client, login_token
):
    from dms.extensions import db
    from dms.models import AttachmentBinding, Document

    category_id = _category_id(b3_app)
    main = _upload_plan(
        b3_client, login_token, category_id, "B8异常挂载主案"
    )
    attachment = _upload_attachment(
        b3_client, login_token, "B8异常挂载附件"
    )
    _delete(b3_client, login_token, attachment, attachment=True)
    deleted = _deleted_document(b3_app, attachment["id"])
    with b3_app.app_context():
        db.session.add(
            AttachmentBinding(
                main_document_id=int(main["id"]),
                attachment_document_id=int(attachment["id"]),
                sort_no=10,
            )
        )
        db.session.commit()
    response = _restore(
        b3_client, login_token, attachment["id"], deleted["rowVersion"]
    )
    assert response.status_code == 409
    assert response.get_json()["code"] == "RESTORE_RELATION_CONFLICT"
    assert response.get_json()["details"]["bindingConflictCount"] == 1
    with b3_app.app_context():
        assert db.session.get(Document, int(attachment["id"])).is_deleted


def test_concurrent_double_restore_only_one_succeeds(
    b3_app, b3_client, login_token, monkeypatch
):
    from dms.services import recycle_bin_service

    category_id = _category_id(b3_app)
    document = _upload_plan(
        b3_client, login_token, category_id, "B8并发双恢复"
    )
    _delete(b3_client, login_token, document)
    deleted = _deleted_document(b3_app, document["id"])
    original = recycle_bin_service._restore_operation
    barrier = threading.Barrier(2)

    def synchronized(*args, **kwargs):
        barrier.wait(timeout=10)
        return original(*args, **kwargs)

    monkeypatch.setattr(
        recycle_bin_service, "_restore_operation", synchronized
    )

    def invoke():
        with b3_app.test_client() as client:
            response = _restore(
                client, login_token, document["id"], deleted["rowVersion"]
            )
            return response.status_code, response.get_json()["code"]

    with ThreadPoolExecutor(max_workers=2) as executor:
        results = list(executor.map(lambda _index: invoke(), range(2)))
    assert sorted(results) == [
        (200, "OK"),
        (409, "DOCUMENT_NOT_DELETED"),
    ]
