from __future__ import annotations

from pathlib import Path
import re

import pytest
import yaml

from dms.storage.paths import (
    STORAGE_SUBDIRECTORIES,
    UnsafeStoragePathError,
    ensure_storage_directories,
    resolve_storage_path,
)


BACKEND_ROOT = Path(__file__).resolve().parents[2]
OPENAPI_ROOT = BACKEND_ROOT / "openapi"


def test_all_openapi_yaml_files_parse():
    for path in OPENAPI_ROOT.rglob("*.yaml"):
        with path.open("r", encoding="utf-8") as stream:
            assert isinstance(yaml.safe_load(stream), dict)


def test_all_openapi_external_refs_resolve():
    def walk(value, source_path: Path):
        if isinstance(value, dict):
            reference = value.get("$ref")
            if isinstance(reference, str) and not reference.startswith("#"):
                target, _, fragment = reference.partition("#")
                target_path = (source_path.parent / target).resolve()
                assert target_path.is_file(), reference
                target_document = yaml.safe_load(
                    target_path.read_text(encoding="utf-8")
                )
                current = target_document
                if fragment:
                    for part in fragment.lstrip("/").split("/"):
                        assert part in current, reference
                        current = current[part]
            for child in value.values():
                walk(child, source_path)
        elif isinstance(value, list):
            for child in value:
                walk(child, source_path)

    for path in OPENAPI_ROOT.rglob("*.yaml"):
        walk(yaml.safe_load(path.read_text(encoding="utf-8")), path)


def test_flask_methods_match_openapi_exactly(flask_app):
    document = yaml.safe_load(
        (OPENAPI_ROOT / "openapi.yaml").read_text(encoding="utf-8")
    )
    openapi_methods = set()
    operation_ids = []
    for path, item in document["paths"].items():
        if "$ref" in item:
            target, _, fragment = item["$ref"].partition("#")
            referenced = yaml.safe_load(
                (OPENAPI_ROOT / target).read_text(encoding="utf-8")
            )
            item = referenced[fragment.lstrip("/")]
        for method, operation in item.items():
            if method.lower() in {"get", "post", "put", "delete", "patch"}:
                openapi_methods.add((method.upper(), path))
                operation_ids.append(operation["operationId"])
    flask_methods = set()
    for rule in flask_app.url_map.iter_rules():
        if not rule.rule.startswith("/api/v1"):
            continue
        path = re.sub(r"<(?:[^:>]+:)?([^>]+)>", r"{\1}", rule.rule)
        for method in rule.methods - {"HEAD", "OPTIONS"}:
            flask_methods.add((method, path))
    assert flask_methods == openapi_methods
    assert len(operation_ids) == len(set(operation_ids))


def test_openapi_contains_only_q2b_paths():
    document = yaml.safe_load(
        (OPENAPI_ROOT / "openapi.yaml").read_text(encoding="utf-8")
    )
    assert set(document["paths"]) == {
        "/api/v1/health",
        "/api/v1/auth/login",
        "/api/v1/auth/logout",
        "/api/v1/users/me",
        "/api/v1/organizations/tree",
        "/api/v1/users",
        "/api/v1/config/ui-dictionaries",
        "/api/v1/categories/tree",
        "/api/v1/categories",
        "/api/v1/categories/{id}",
        "/api/v1/documents",
        "/api/v1/documents/{id}",
        "/api/v1/documents/batch-import",
        "/api/v1/documents/{id}/preview",
        "/api/v1/documents/{id}/download",
        "/api/v1/main-plans/{id}/sub-plans",
        "/api/v1/attachments",
        "/api/v1/attachments/{id}",
        "/api/v1/attachments/batch-import",
        "/api/v1/attachments/{id}/main-plans",
        "/api/v1/main-plans/{id}/attachments",
        "/api/v1/main-plans/{id}/attachments/bind",
        "/api/v1/main-plans/{id}/attachments/{attachmentId}",
        "/api/v1/documents/{id}/permissions",
        "/api/v1/audit/logs",
        "/api/v1/statistics/documents",
        "/api/v1/audit/statistics/trend",
        "/api/v1/audit/statistics/actions",
        "/api/v1/audit/statistics/users",
        "/api/v1/recycle-bin/documents",
        "/api/v1/recycle-bin/documents/{id}/restore",
    }
    assert document["info"]["version"] == "1.6.0"
    assert "/api/v1/documents/{id}/restore" not in document["paths"]
    assert document["paths"]["/api/v1/health"]["get"]["security"] == []
    login_path = yaml.safe_load(
        (OPENAPI_ROOT / "paths" / "auth.yaml").read_text(encoding="utf-8")
    )
    assert login_path["Login"]["post"]["security"] == []


def test_openapi_string_id_component():
    schemas = yaml.safe_load(
        (OPENAPI_ROOT / "components" / "schemas.yaml").read_text(encoding="utf-8")
    )
    assert schemas["StringId"] == {
        "type": "string",
        "pattern": "^[0-9]+$",
        "description": "数据库BIGINT在JSON中的安全十进制字符串表达。",
    }


def test_openapi_health_response_matches_contract():
    schemas = yaml.safe_load(
        (OPENAPI_ROOT / "components" / "schemas.yaml").read_text(encoding="utf-8")
    )
    health = schemas["HealthResponse"]
    assert set(health["required"]) == {"code", "message", "data", "requestId"}
    assert "timestamp" not in health["properties"]


def test_openapi_has_all_fixed_enum_groups():
    schemas = yaml.safe_load(
        (OPENAPI_ROOT / "components" / "schemas.yaml").read_text(encoding="utf-8")
    )
    expected = {
        "RoleCode",
        "AllowedModule",
        "DocumentType",
        "DocumentStatus",
        "SecurityLevel",
        "VisibilityType",
        "AttachmentType",
        "SubjectType",
        "AllowedAction",
        "CategoryType",
        "EnabledStatus",
        "AuditResult",
        "AuditAction",
        "AuditTarget",
    }
    assert expected <= set(schemas)


def test_storage_directory_skeleton(tmp_path: Path):
    root = ensure_storage_directories(tmp_path / "storage")
    assert {path.name for path in root.iterdir()} == set(STORAGE_SUBDIRECTORIES)


@pytest.mark.parametrize(
    "unsafe_path",
    [
        "../outside.pdf",
        "original/../../outside.pdf",
        "/absolute/file.pdf",
        "C:\\Windows\\system.ini",
        "",
    ],
)
def test_storage_path_cannot_escape_root(tmp_path: Path, unsafe_path: str):
    with pytest.raises(UnsafeStoragePathError):
        resolve_storage_path(unsafe_path, tmp_path)


def test_storage_path_resolves_safe_relative_path(tmp_path: Path):
    resolved = resolve_storage_path(
        "original/2026/07/1/file.pdf",
        tmp_path,
    )
    assert resolved == (tmp_path / "original/2026/07/1/file.pdf").resolve()
