from __future__ import annotations

from datetime import UTC, datetime

import pytest


RANGE = "createdFrom=2026-01-01T00:00:00.000Z&createdTo=2026-02-01T00:00:00.000Z"


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


def _get(client, path: str, token: str):
    return client.get(path, headers=_headers(token))


@pytest.mark.parametrize(
    "path",
    [
        "/api/v1/audit/logs",
        "/api/v1/audit/statistics/trend?granularity=DAY",
        "/api/v1/audit/statistics/actions",
        "/api/v1/audit/statistics/users",
        "/api/v1/statistics/documents",
    ],
)
def test_all_b7_endpoints_require_authentication(b7_client, path):
    assert b7_client.get(path).status_code == 401


@pytest.mark.parametrize(
    "path",
    [
        "/api/v1/audit/logs",
        "/api/v1/audit/statistics/trend?granularity=DAY",
        "/api/v1/audit/statistics/actions",
        "/api/v1/audit/statistics/users",
    ],
)
def test_audit_endpoints_are_admin_only(b7_client, token_for, path):
    assert _get(b7_client, path, token_for("admin")).status_code == 200
    assert _get(b7_client, path, token_for("user")).status_code == 403


def test_document_statistics_is_admin_only(b7_client, token_for):
    assert _get(
        b7_client, "/api/v1/statistics/documents", token_for("admin")
    ).status_code == 200
    assert _get(
        b7_client, "/api/v1/statistics/documents", token_for("user")
    ).status_code == 403


def test_audit_list_dto_paging_and_stable_sort(b7_client, token_for):
    token = token_for("admin")
    response = _get(
        b7_client,
        f"/api/v1/audit/logs?{RANGE}&page=1&pageSize=2&sortDirection=asc",
        token,
    )
    assert response.status_code == 200
    data = response.get_json()["data"]
    assert (data["page"], data["pageSize"], data["total"], data["totalPages"]) == (
        1,
        2,
        8,
        4,
    )
    expected_fields = {
        "id",
        "username",
        "realName",
        "organizationName",
        "actionType",
        "targetType",
        "targetId",
        "targetName",
        "operationResult",
        "failureReason",
        "clientIp",
        "requestId",
        "createdAt",
    }
    assert set(data["items"][0]) == expected_fields
    assert data["items"][0]["createdAt"] == "2026-01-05T00:00:00.000Z"
    assert isinstance(data["items"][0]["id"], str)
    assert isinstance(data["items"][0]["targetId"], str)
    assert "operationDetail" not in data["items"][0]
    assert "userAgent" not in data["items"][0]

    tied = _get(
        b7_client,
        "/api/v1/audit/logs?"
        "createdFrom=2026-01-07T01:00:00.000Z&"
        "createdTo=2026-01-07T01:00:00.001Z&sortDirection=asc",
        token,
    ).get_json()["data"]["items"]
    assert [int(item["id"]) for item in tied] == sorted(
        int(item["id"]) for item in tied
    )


@pytest.mark.parametrize(
    ("keyword", "expected_request"),
    [
        ("admin", "b7-request-alpha"),
        ("管理员旧名", "b7-request-alpha"),
        ("旧组织", "b7-request-alpha"),
        ("关键目标", "b7-request-alpha"),
        ("关键失败原因", "b7-request-beta"),
        ("b7-request-gamma", "b7-request-gamma"),
    ],
)
def test_keyword_searches_only_contract_fields(
    b7_client, token_for, keyword, expected_request
):
    response = _get(
        b7_client,
        f"/api/v1/audit/logs?{RANGE}&keyword={keyword}",
        token_for("admin"),
    )
    request_ids = {
        item["requestId"] for item in response.get_json()["data"]["items"]
    }
    assert expected_request in request_ids


def test_keyword_escapes_sql_wildcards(b7_client, token_for):
    response = _get(
        b7_client,
        f"/api/v1/audit/logs?{RANGE}&keyword=%25",
        token_for("admin"),
    )
    assert response.get_json()["data"]["total"] == 0


@pytest.mark.parametrize(
    ("query", "request_id"),
    [
        ("userId={admin}", "b7-request-alpha"),
        ("organizationId={admin_org}", "b7-request-alpha"),
        ("actionType=DOWNLOAD_DOCUMENT", "b7-request-beta"),
        ("operationResult=FAILURE", "b7-request-beta"),
        ("targetType=AUTH", "b7-request-gamma"),
        ("targetId=103", "b7-request-epsilon"),
    ],
)
def test_audit_list_contract_filters(
    b7_app, b7_client, token_for, query, request_id
):
    with b7_app.app_context():
        from dms.extensions import db
        from dms.models import User

        admin = db.session.scalar(
            db.select(User).where(User.username == "admin")
        )
        query = query.format(
            admin=admin.id, admin_org=admin.organization_id
        )
    response = _get(
        b7_client,
        f"/api/v1/audit/logs?{RANGE}&{query}",
        token_for("admin"),
    )
    assert request_id in {
        item["requestId"] for item in response.get_json()["data"]["items"]
    }


def test_time_range_is_half_open_and_each_bound_is_optional(
    b7_client, token_for
):
    token = token_for("admin")
    inclusive = _get(
        b7_client,
        "/api/v1/audit/logs?"
        "createdFrom=2026-01-05T00:00:00.000Z&"
        "createdTo=2026-01-05T01:00:00.000Z",
        token,
    ).get_json()["data"]["items"]
    assert [item["requestId"] for item in inclusive] == ["b7-request-alpha"]
    only_from = _get(
        b7_client,
        "/api/v1/audit/logs?createdFrom=2026-02-01T00:00:00.000Z",
        token,
    ).get_json()["data"]
    assert any(item["requestId"] == "b7-request-iota" for item in only_from["items"])
    only_to = _get(
        b7_client,
        "/api/v1/audit/logs?createdTo=2026-01-05T00:00:00.001Z",
        token,
    ).get_json()["data"]
    assert only_to["total"] == 1


@pytest.mark.parametrize(
    "query",
    [
        "createdFrom=2026-01-01T00:00:00",
        "createdFrom=2026-01-01T00:00:00%2B08:00",
        "createdFrom=not-a-date",
        "createdFrom=2026-01-02T00:00:00Z&createdTo=2026-01-01T00:00:00Z",
        "actionType=UNKNOWN",
        "operationResult=UNKNOWN",
        "targetType=UNKNOWN",
        "sortDirection=sideways",
        "page=0",
        "pageSize=101",
        "userId=abc",
        "unknown=value",
        "page=1&page=2",
    ],
)
def test_invalid_audit_list_parameters_return_400(
    b7_client, token_for, query
):
    response = _get(
        b7_client, f"/api/v1/audit/logs?{query}", token_for("admin")
    )
    assert response.status_code == 400
    assert response.get_json()["code"] == "INVALID_ARGUMENT"


def test_document_statistics_and_business_timezone_day(
    b7_app, b7_client, token_for, monkeypatch
):
    from dms.extensions import db
    from dms.models import Document
    from dms.services import audit_query_service

    monkeypatch.setattr(
        audit_query_service,
        "_utc_now",
        lambda: datetime(2026, 1, 7, 12, 0, tzinfo=UTC),
    )
    response = _get(
        b7_client, "/api/v1/statistics/documents", token_for("admin")
    )
    data = response.get_json()["data"]
    with b7_app.app_context():
        counts = {
            kind: db.session.scalar(
                db.select(db.func.count(Document.id)).where(
                    Document.document_type == kind,
                    Document.is_deleted.is_(False),
                )
            )
            for kind in ("MAIN", "SUB_PLAN", "ATTACHMENT")
        }
    assert data["mainPlanCount"] == counts["MAIN"]
    assert data["subPlanCount"] == counts["SUB_PLAN"]
    assert data["attachmentCount"] == counts["ATTACHMENT"]
    assert data["planDocumentCount"] == counts["MAIN"] + counts["SUB_PLAN"]
    assert data["todayViewerCount"] == 2


@pytest.mark.parametrize(
    ("granularity", "expected_periods"),
    [
        ("DAY", ["2026-01-05", "2026-01-06", "2026-01-07", "2026-01-08"]),
        ("WEEK", ["2026-01-05"]),
        ("MONTH", ["2026-01"]),
    ],
)
def test_trend_database_buckets(
    b7_client, token_for, granularity, expected_periods
):
    response = _get(
        b7_client,
        f"/api/v1/audit/statistics/trend?{RANGE}&granularity={granularity}",
        token_for("admin"),
    )
    assert response.status_code == 200
    items = response.get_json()["data"]["items"]
    assert [item["period"] for item in items] == expected_periods
    assert sum(item["operationCount"] for item in items) == 8


def test_trend_requires_valid_granularity_and_supports_empty_result(
    b7_client, token_for
):
    token = token_for("admin")
    assert _get(
        b7_client, "/api/v1/audit/statistics/trend", token
    ).status_code == 400
    assert _get(
        b7_client,
        "/api/v1/audit/statistics/trend?granularity=YEAR",
        token,
    ).status_code == 400
    data = _get(
        b7_client,
        "/api/v1/audit/statistics/trend?"
        "granularity=DAY&createdFrom=2025-01-01T00:00:00Z&"
        "createdTo=2025-01-02T00:00:00Z",
        token,
    ).get_json()["data"]
    assert data == {"items": []}


def test_action_distribution_order_and_all_results(b7_client, token_for):
    items = _get(
        b7_client,
        f"/api/v1/audit/statistics/actions?{RANGE}",
        token_for("admin"),
    ).get_json()["data"]["items"]
    assert items[0] == {"actionType": "VIEW_DOCUMENT", "count": 5}
    assert sum(item["count"] for item in items) == 8
    tied = [item["actionType"] for item in items if item["count"] == 1]
    assert tied == sorted(tied)


def test_active_users_latest_audit_snapshot_and_order(b7_client, token_for):
    items = _get(
        b7_client,
        f"/api/v1/audit/statistics/users?{RANGE}&limit=2",
        token_for("admin"),
    ).get_json()["data"]["items"]
    assert [item["operationCount"] for item in items] == [4, 2]
    assert items[0]["realName"] == "管理员新名"
    assert items[0]["organizationName"] == "新组织"
    assert all(isinstance(item["userId"], str) for item in items)


@pytest.mark.parametrize("limit", ["0", "101", "abc"])
def test_active_user_limit_validation(b7_client, token_for, limit):
    assert _get(
        b7_client,
        f"/api/v1/audit/statistics/users?limit={limit}",
        token_for("admin"),
    ).status_code == 400


def test_audit_queries_do_not_write_audit_log(b7_app, b7_client, token_for):
    from dms.extensions import db
    from dms.models import AuditLog

    token = token_for("admin")
    with b7_app.app_context():
        before = db.session.scalar(db.select(db.func.count(AuditLog.id)))
    for path in (
        f"/api/v1/audit/logs?{RANGE}",
        f"/api/v1/audit/statistics/trend?{RANGE}&granularity=DAY",
        f"/api/v1/audit/statistics/actions?{RANGE}",
        f"/api/v1/audit/statistics/users?{RANGE}",
    ):
        assert _get(b7_client, path, token).status_code == 200
    with b7_app.app_context():
        after = db.session.scalar(db.select(db.func.count(AuditLog.id)))
    assert after == before


def test_b7_real_http_on_8755_uses_only_formal_apis_for_audit_writes(
    b4_app, b2_password
):
    """单进程内启停真实TCP服务，避免后台子进程遗留或占住测试通道。"""
    import json
    import threading
    from datetime import timedelta
    from urllib.error import HTTPError
    from urllib.parse import urlencode
    from urllib.request import Request, urlopen

    from werkzeug.serving import make_server
    import requests

    from dms.extensions import db
    from dms.models import User

    origin = "http://127.0.0.1:9346"
    base_url = "http://127.0.0.1:8755"

    def call(method, path, *, token=None, body=None):
        headers = {"Origin": origin}
        if token:
            headers["Authorization"] = f"Bearer {token}"
        data = None
        if body is not None:
            data = json.dumps(body, ensure_ascii=False).encode("utf-8")
            headers["Content-Type"] = "application/json"
        request = Request(
            f"{base_url}{path}",
            data=data,
            headers=headers,
            method=method,
        )
        try:
            response = urlopen(request, timeout=5)
        except HTTPError as error:
            response = error
        content = response.read()
        payload = (
            json.loads(content.decode("utf-8"))
            if response.headers.get_content_type() == "application/json"
            else content
        )
        return response.status, response.headers, payload

    def login(username, password=b2_password):
        status, _, payload = call(
            "POST",
            "/api/v1/auth/login",
            body={
                "username": username,
                "password": b2_password,
                "keepSignedIn": False,
            },
        )
        assert status == 200
        return payload["data"]["accessToken"]

    with b4_app.app_context():
        ids = b4_app.config["B4_IDS"]
        admin = db.session.scalar(
            db.select(User).where(User.username == "admin")
        )
        admin_id = admin.id
        organization_id = admin.organization_id

    server = make_server("127.0.0.1", 8755, b4_app, threaded=True)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    try:
        admin_token = login("admin")
        user_token = login("user")
        assert call(
            "POST",
            "/api/v1/auth/login",
            body={
                "username": "missing-user",
                "password": "wrong-password",
                "keepSignedIn": False,
            },
        )[0] == 401
        assert call(
            "POST",
            "/api/v1/auth/login",
                body={
                    "username": "auditor",
                    "password": b2_password,
                    "keepSignedIn": False,
                },
        )[0] == 401
        audit_token = admin_token

        assert call(
            "GET", f"/api/v1/documents/{ids['main_all']}", token=admin_token
        )[0] == 200
        assert call(
            "GET", f"/api/v1/documents/{ids['main_all']}", token=user_token
        )[0] == 200
        upload_response = requests.post(
            f"{base_url}/api/v1/attachments",
            files={
                "file": (
                    "B7真实HTTP附件.pdf",
                    b"%PDF-1.4\nB7-REAL-HTTP-DOWNLOAD",
                    "application/pdf",
                )
            },
            data={
                "metadata": json.dumps(
                    {
                        "documentName": "B7真实HTTP附件",
                        "attachmentType": "POLICY",
                        "summary": "B7真实HTTP验证",
                        "tags": ["B7"],
                    },
                    ensure_ascii=False,
                )
            },
            headers={
                "Authorization": f"Bearer {admin_token}",
                "Origin": origin,
            },
            timeout=5,
        )
        assert upload_response.status_code == 201, upload_response.text
        attachment_id = upload_response.json()["data"]["id"]
        download_result = call(
            "GET",
            f"/api/v1/documents/{attachment_id}/download",
            token=admin_token,
        )
        assert download_result[0] == 200, download_result[2]
        status, _, _ = call(
            "POST",
            "/api/v1/categories",
            token=admin_token,
            body={
                "categoryCode": "B7_HTTP_CATEGORY",
                "categoryName": "B7真实HTTP分类",
                "categoryType": "OTHER",
                "parentId": None,
                "sortNo": 99,
            },
        )
        assert status == 201

        now = datetime.now(UTC)
        time_query = {
            "createdFrom": (now - timedelta(minutes=5))
            .isoformat(timespec="milliseconds")
            .replace("+00:00", "Z"),
            "createdTo": (now + timedelta(minutes=5))
            .isoformat(timespec="milliseconds")
            .replace("+00:00", "Z"),
        }
        base_query = urlencode(time_query)
        status, headers, payload = call(
            "GET",
            f"/api/v1/audit/logs?{base_query}&page=1&pageSize=100&"
            "sortDirection=asc",
            token=audit_token,
        )
        assert status == 200
        assert headers["Access-Control-Allow-Origin"] == origin
        assert "X-Request-Id" in headers["Access-Control-Expose-Headers"]
        before_total = payload["data"]["total"]
        assert before_total >= 9

        checks = [
            f"/api/v1/audit/logs?{base_query}&"
            f"{urlencode({'keyword': 'B7真实HTTP分类'})}",
            f"/api/v1/audit/logs?{base_query}&userId={admin_id}",
            f"/api/v1/audit/logs?{base_query}&organizationId={organization_id}",
            f"/api/v1/audit/logs?{base_query}&actionType=VIEW_DOCUMENT",
            f"/api/v1/audit/logs?{base_query}&operationResult=FAILURE",
            f"/api/v1/audit/logs?{base_query}&targetType=DOCUMENT&"
            f"targetId={ids['main_all']}",
            f"/api/v1/audit/statistics/trend?{base_query}&granularity=DAY",
            f"/api/v1/audit/statistics/trend?{base_query}&granularity=WEEK",
            f"/api/v1/audit/statistics/trend?{base_query}&granularity=MONTH",
            f"/api/v1/audit/statistics/actions?{base_query}",
            f"/api/v1/audit/statistics/users?{base_query}&limit=10",
        ]
        for path in checks:
            assert call("GET", path, token=audit_token)[0] == 200

        status, _, statistics = call(
            "GET", "/api/v1/statistics/documents", token=admin_token
        )
        assert status == 200
        assert statistics["data"]["todayViewerCount"] == 2
        assert call(
            "GET", "/api/v1/audit/logs", token=admin_token
        )[0] == 200
        assert call(
            "GET", "/api/v1/statistics/documents", token=audit_token
        )[0] == 200
        assert call(
            "GET", "/api/v1/audit/export", token=audit_token
        )[0] == 404

        after_total = call(
            "GET",
            f"/api/v1/audit/logs?{base_query}&pageSize=100",
            token=audit_token,
        )[2]["data"]["total"]
        assert after_total == before_total
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=5)
        assert not thread.is_alive()
