"""Tests for Phase 17 — Audit log module."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from backend.dashboard.middleware import CustomerContext, require_dashboard_auth
from backend.dashboard.routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient

_PATCH_FIRESTORE_DB = "backend.shared.firestore.get_db"


def _ctx(
    tier: str = "team",
    role: str = "owner",
    features: list[str] | None = None,
) -> CustomerContext:
    return CustomerContext(
        customer_id="cus_test",
        email="owner@example.com",
        tier=tier,
        role=role,
        member_email="owner@example.com",
        features=features
        or [
            "hosted_dashboard",
            "team_members",
            "audit_log",
            "webhooks",
            "shared_scan_links",
        ],
        project_limit=1,
        batch_limit=5,
        cancellation_pending=False,
        subscription_expires_at="2027-01-01T00:00:00+00:00",
        consent_accepted=True,
        tos_version="1.0",
        privacy_version="1.0",
    )


def _build_app(ctx: CustomerContext | None = None) -> FastAPI:
    app = FastAPI()
    app.include_router(router)
    if ctx is not None:
        app.dependency_overrides[require_dashboard_auth] = lambda: ctx
    return app


def _mock_audit_db(events: list[dict[str, Any]] | None = None) -> MagicMock:
    """Build chainable mock Firestore DB for audit log."""
    mock_db = MagicMock()

    event_docs: list[MagicMock] = []
    if events:
        for i, e in enumerate(events):
            doc = MagicMock()
            doc.id = e.get("_id", f"evt_{i}")
            doc.exists = True
            doc.to_dict = MagicMock(return_value=e)
            event_docs.append(doc)

    # audit_log subcollection
    audit_coll = MagicMock()

    # For add() — returns (timestamp, doc_ref)
    new_doc_ref = MagicMock()
    new_doc_ref.id = "new_audit_1"
    audit_coll.add = AsyncMock(return_value=(None, new_doc_ref))

    # For query chain
    query_mock = MagicMock()

    async def _query_stream() -> Any:
        for d in event_docs:
            yield d

    query_mock.stream = _query_stream
    query_mock.where = MagicMock(return_value=query_mock)
    query_mock.order_by = MagicMock(return_value=query_mock)
    query_mock.limit = MagicMock(return_value=query_mock)
    query_mock.start_after = MagicMock(return_value=query_mock)

    audit_coll.order_by = MagicMock(return_value=query_mock)
    audit_coll.where = MagicMock(return_value=query_mock)

    # cursor doc fetch
    cursor_doc = MagicMock()
    cursor_doc.exists = False

    def _audit_doc(doc_id: str) -> MagicMock:
        d = MagicMock()
        d.get = AsyncMock(return_value=cursor_doc)
        return d

    audit_coll.document = MagicMock(side_effect=_audit_doc)

    # Wire up collection routing
    def _get_collection(name: str) -> MagicMock:
        cust_coll = MagicMock()
        cust_doc = MagicMock()

        def _subcoll(sub_name: str) -> MagicMock:
            if sub_name == "audit_log":
                return audit_coll
            return MagicMock()

        cust_doc.collection = MagicMock(side_effect=_subcoll)
        cust_coll.document = MagicMock(return_value=cust_doc)
        return cust_coll

    mock_db.collection = MagicMock(side_effect=_get_collection)
    return mock_db


class TestWriteAuditEvent:
    @pytest.mark.anyio
    async def test_write_audit_event(self) -> None:
        db = _mock_audit_db()
        with patch(_PATCH_FIRESTORE_DB, return_value=db):
            from backend.dashboard.audit import write_audit_event

            doc_id = await write_audit_event(
                customer_id="cus_test",
                event="dashboard.login",
                actor_email="user@example.com",
                actor_role="owner",
                ip_hash="abc123",
                metadata={"source": "google"},
                retention_months=6,
            )

        assert doc_id == "new_audit_1"


class TestAuditEventTTL:
    @pytest.mark.anyio
    async def test_audit_event_ttl_team(self) -> None:
        from backend.dashboard.audit import _compute_expires_at

        result = _compute_expires_at(6)
        assert result is not None
        # Should be ~6 months from now
        diff = (result - datetime.now(UTC)).days
        assert 175 < diff < 185

    @pytest.mark.anyio
    async def test_audit_event_ttl_agency(self) -> None:
        from backend.dashboard.audit import _compute_expires_at

        result = _compute_expires_at(12)
        assert result is not None
        diff = (result - datetime.now(UTC)).days
        assert 355 < diff < 365

    @pytest.mark.anyio
    async def test_audit_event_ttl_enterprise(self) -> None:
        from backend.dashboard.audit import _compute_expires_at

        result = _compute_expires_at(24)
        assert result is not None
        diff = (result - datetime.now(UTC)).days
        assert 715 < diff < 725

    @pytest.mark.anyio
    async def test_audit_event_ttl_unlimited(self) -> None:
        from backend.dashboard.audit import _compute_expires_at

        result = _compute_expires_at(None)
        assert result is None


class TestQueryAuditLog:
    @pytest.mark.anyio
    async def test_query_audit_log_pagination(self) -> None:
        events = [
            {
                "_id": f"evt_{i}",
                "event": "dashboard.login",
                "actor_email": "user@example.com",
                "actor_role": "owner",
                "timestamp": f"2025-01-{i + 1:02d}T00:00:00+00:00",
                "ip_hash": "abc",
                "metadata": {},
            }
            for i in range(5)
        ]
        db = _mock_audit_db(events=events)
        with patch(_PATCH_FIRESTORE_DB, return_value=db):
            from backend.dashboard.audit import query_audit_log

            page = await query_audit_log(
                customer_id="cus_test",
                page_size=50,
            )

        assert len(page.events) == 5

    @pytest.mark.anyio
    async def test_query_audit_log_filters(self) -> None:
        events = [
            {
                "event": "dashboard.login",
                "actor_email": "user@example.com",
                "actor_role": "owner",
                "timestamp": "2025-01-01T00:00:00+00:00",
                "ip_hash": "abc",
                "metadata": {},
            }
        ]
        db = _mock_audit_db(events=events)
        with patch(_PATCH_FIRESTORE_DB, return_value=db):
            from backend.dashboard.audit import query_audit_log

            page = await query_audit_log(
                customer_id="cus_test",
                event_filter="dashboard.login",
                actor_filter="user@example.com",
                start_date="2025-01-01",
                end_date="2025-12-31",
            )

        assert len(page.events) == 1


class TestExportCSV:
    @pytest.mark.anyio
    async def test_export_csv(self) -> None:
        events = [
            {
                "event": "dashboard.login",
                "actor_email": "user@example.com",
                "actor_role": "owner",
                "timestamp": "2025-01-01T00:00:00+00:00",
                "ip_hash": "abc",
                "metadata": {"source": "google"},
            }
        ]
        db = _mock_audit_db(events=events)
        with patch(_PATCH_FIRESTORE_DB, return_value=db):
            from backend.dashboard.audit import export_audit_csv

            csv_str = await export_audit_csv(customer_id="cus_test")

        assert "timestamp" in csv_str
        assert "dashboard.login" in csv_str
        assert "user@example.com" in csv_str


class TestAuditLogRouteAccess:
    def test_audit_log_viewer_forbidden(self) -> None:
        ctx = _ctx(role="viewer")
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        resp = client.get("/dashboard/audit-log")
        assert resp.status_code == 403

    def test_audit_log_no_feature(self) -> None:
        ctx = _ctx(features=["hosted_dashboard"])
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        resp = client.get("/dashboard/audit-log")
        assert resp.status_code == 403

    def test_audit_log_page_renders(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        db = _mock_audit_db(events=[])
        with patch(_PATCH_FIRESTORE_DB, return_value=db):
            resp = client.get("/dashboard/audit-log")

        assert resp.status_code == 200
        assert "Audit Log" in resp.text

    def test_export_csv_route(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        db = _mock_audit_db(events=[])
        with patch(_PATCH_FIRESTORE_DB, return_value=db):
            resp = client.get("/dashboard/audit-log/export-csv")

        assert resp.status_code == 200
        assert "text/csv" in resp.headers.get("content-type", "")
