"""Tests for app.api.v1.audit — admin read API over `user_events` (Feature 2/3).

Coverage:
  (a) static SQL guard — CAST(:x AS type), never `:x::type` (psycopg v3 trap).
  (b) GET /audit/accounts — empty table → [] (never errors); populated → shape-valid
      AccountSummary rows pass through response_model unchanged.
  (c) GET /audit/accounts/{username} — empty table (incl. unknown username, NOT 404) →
      all 4 lists == []; populated → shape-valid drilldown rows.
  (d) GET /analytics — empty table → zeroed summary + empty lists (never errors);
      populated → shape-valid dashboard bundle, `days` query param clamped [1, 365].
  (e) optional real-Postgres round trip — inserts a couple of user_events rows and
      asserts aggregation through the real DB; self-SKIPS without a reachable,
      non-placeholder Postgres (mirrors tests/test_house_dedup_merge.py's live-DB
      pattern).

Router is tested in isolation on a minimal FastAPI app (mirrors tests/test_ratelimit.py
and the quota_app fixture in tests/test_user_events.py) — no need to pull in the full
app.main (sentry/scheduler/CORS/rate-limit wiring).
"""

from __future__ import annotations

import inspect
import os
import re
import uuid
from datetime import UTC, date, datetime
from typing import Any

# psycopg v3 driver required; stub DATABASE_URL before any app import (mirrors other tests).
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from app.api.v1 import audit as audit_module
from app.core.db import get_db

# ---------------------------------------------------------------------------
# (a) Static SQL guard
# ---------------------------------------------------------------------------

_AUDIT_SRC = inspect.getsource(audit_module)


def test_no_psycopg_v3_doublecolon_cast() -> None:
    """CAST(:x AS type) — НИКОГДА `:x::type` (psycopg v3 trap, .claude/rules/backend.md)."""
    assert not re.search(r":\w+::\w", _AUDIT_SRC)


def test_days_param_uses_cast_as_int() -> None:
    assert "CAST(:days AS int)" in _AUDIT_SRC


# ---------------------------------------------------------------------------
# Fakes — mirror the mocked-DB convention used across tests/test_user_events.py etc.
# ---------------------------------------------------------------------------


class _FakeMappingResult:
    def __init__(self, rows: list[dict[str, Any]]) -> None:
        self._rows = rows

    def all(self) -> list[dict[str, Any]]:
        return list(self._rows)

    def one(self) -> dict[str, Any]:
        return self._rows[0]


class _FakeExecResult:
    def __init__(self, rows: list[dict[str, Any]]) -> None:
        self._rows = rows

    def mappings(self) -> _FakeMappingResult:
        return _FakeMappingResult(self._rows)


class _FakeSession:
    """Returns queued row-lists in the exact order audit.py issues db.execute() calls."""

    def __init__(self, responses: list[list[dict[str, Any]]]) -> None:
        self._responses = list(responses)
        self._i = 0

    def execute(self, _stmt: object, _params: dict[str, Any] | None = None) -> _FakeExecResult:
        rows = self._responses[self._i]
        self._i += 1
        return _FakeExecResult(rows)


def _make_app(responses: list[list[dict[str, Any]]]) -> FastAPI:
    application = FastAPI()
    application.include_router(audit_module.router, prefix="/api/v1/admin")

    def _override_db() -> Any:
        yield _FakeSession(responses)

    application.dependency_overrides[get_db] = _override_db
    return application


_NOW = datetime(2026, 7, 13, 12, 0, 0, tzinfo=UTC)

# ---------------------------------------------------------------------------
# (b) GET /audit/accounts
# ---------------------------------------------------------------------------


def test_list_accounts_empty_table() -> None:
    app = _make_app(responses=[[]])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/audit/accounts")
    assert resp.status_code == 200
    assert resp.json() == []


def test_list_accounts_populated_shape() -> None:
    row = {
        "username": "user1",
        "first_seen_at": _NOW,
        "last_seen_at": _NOW,
        "distinct_ips": 3,
        "distinct_devices": 2,
        "login_count": 5,
        "request_count": 40,
        "search_count": 7,
    }
    app = _make_app(responses=[[row]])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/audit/accounts")
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 1
    assert data[0]["username"] == "user1"
    assert data[0]["distinct_ips"] == 3
    assert data[0]["login_count"] == 5
    assert data[0]["request_count"] == 40
    assert data[0]["search_count"] == 7


# ---------------------------------------------------------------------------
# (c) GET /audit/accounts/{username}
# ---------------------------------------------------------------------------


def test_account_drilldown_empty_table_unknown_user_not_404() -> None:
    """Unknown username → 200 with all 4 lists empty, NOT a 404."""
    app = _make_app(responses=[[], [], [], []])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/audit/accounts/ghost_user_xyz")
    assert resp.status_code == 200
    data = resp.json()
    assert data == {"ips": [], "devices": [], "searches": [], "recent_activity": []}


def test_account_drilldown_populated_shape() -> None:
    ips = [{"ip_address": "1.2.3.4", "event_count": 10, "first_seen": _NOW, "last_seen": _NOW}]
    devices = [
        {"user_agent": "pytest-agent", "event_count": 10, "first_seen": _NOW, "last_seen": _NOW}
    ]
    searches = [
        {
            "address": "ул. Малышева, 1",
            "area_m2": "50.0",
            "rooms": "2",
            "estimate_id": str(uuid.uuid4()),
            "ip_address": "1.2.3.4",
            "created_at": _NOW,
        }
    ]
    recent_activity = [
        {
            "event_type": "api_request",
            "path": "/api/v1/trade-in/estimate",
            "method": "POST",
            "ip_address": "1.2.3.4",
            "created_at": _NOW,
        }
    ]
    app = _make_app(responses=[ips, devices, searches, recent_activity])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/audit/accounts/user1")
    assert resp.status_code == 200
    data = resp.json()
    assert data["ips"][0]["ip_address"] == "1.2.3.4"
    assert data["ips"][0]["event_count"] == 10
    assert data["devices"][0]["user_agent"] == "pytest-agent"
    assert data["searches"][0]["address"] == "ул. Малышева, 1"
    assert data["searches"][0]["area_m2"] == "50.0"
    assert data["recent_activity"][0]["event_type"] == "api_request"
    assert data["recent_activity"][0]["path"] == "/api/v1/trade-in/estimate"


# ---------------------------------------------------------------------------
# (d) GET /analytics
# ---------------------------------------------------------------------------

_ZERO_SUMMARY = {
    "total_events": 0,
    "distinct_users": 0,
    "events_last_24h": 0,
    "active_users_last_24h": 0,
}


def test_analytics_empty_table_never_errors() -> None:
    app = _make_app(responses=[[_ZERO_SUMMARY], [], [], [], []])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/analytics")
    assert resp.status_code == 200
    data = resp.json()
    assert data["summary"] == _ZERO_SUMMARY
    assert data["daily"] == []
    assert data["top_searches"] == []
    assert data["top_paths"] == []
    assert data["by_account"] == []


def test_analytics_populated_shape() -> None:
    summary = {
        "total_events": 120,
        "distinct_users": 4,
        "events_last_24h": 15,
        "active_users_last_24h": 2,
    }
    daily = [{"day": date(2026, 7, 13), "events": 15, "users": 2}]
    top_searches = [{"address": "ул. Малышева, 1", "n": 3}]
    top_paths = [{"path": "/api/v1/trade-in/estimate", "n": 40}]
    by_account = [{"username": "user1", "events": 60, "searches": 7, "last_seen": _NOW}]
    app = _make_app(responses=[[summary], daily, top_searches, top_paths, by_account])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/analytics")
    assert resp.status_code == 200
    data = resp.json()
    assert data["summary"]["total_events"] == 120
    assert data["daily"][0]["events"] == 15
    assert data["daily"][0]["day"] == "2026-07-13"
    assert data["top_searches"][0]["address"] == "ул. Малышева, 1"
    assert data["top_paths"][0]["n"] == 40
    assert data["by_account"][0]["username"] == "user1"


def test_analytics_days_query_param_clamped() -> None:
    """days=0 (below ge=1) → 422; days=9999 (above le=365) → 422."""
    app = _make_app(responses=[[_ZERO_SUMMARY], [], [], [], []])
    client = TestClient(app)
    assert client.get("/api/v1/admin/analytics?days=0").status_code == 422
    assert client.get("/api/v1/admin/analytics?days=9999").status_code == 422


def test_analytics_days_query_param_default_30() -> None:
    app = _make_app(responses=[[_ZERO_SUMMARY], [], [], [], []])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/analytics")
    assert resp.status_code == 200


def test_analytics_days_query_param_accepted_in_range() -> None:
    app = _make_app(responses=[[_ZERO_SUMMARY], [], [], [], []])
    client = TestClient(app)
    resp = client.get("/api/v1/admin/analytics?days=7")
    assert resp.status_code == 200


# ---------------------------------------------------------------------------
# (e) Optional real-Postgres round trip (self-skips without a reachable DB)
# ---------------------------------------------------------------------------


def _live_session() -> Any | None:
    """Return a SQLAlchemy Session if a non-placeholder Postgres is reachable, else None."""
    try:
        from sqlalchemy import create_engine
        from sqlalchemy import text as _t
        from sqlalchemy.orm import sessionmaker

        dsn = os.environ.get("TEST_DATABASE_URL") or os.environ.get("DATABASE_URL", "")
        if not dsn or "localhost:5432/test" in dsn:
            return None
        engine = create_engine(dsn, future=True)
        conn = engine.connect()
        conn.execute(_t("SELECT 1"))
        conn.close()
        return sessionmaker(bind=engine, future=True)()
    except Exception:
        return None


@pytest.mark.skipif(_live_session() is None, reason="no reachable Postgres test DB")
def test_real_accounts_and_analytics_aggregate_inserted_rows() -> None:
    """End-to-end on a real DB: insert 2 user_events rows for a throwaway test username,
    assert /audit/accounts, /audit/accounts/{username} and /analytics reflect them."""
    from sqlalchemy import text as _t

    from app.core.db import SessionLocal

    db = _live_session()
    assert db is not None
    username = f"audit_test_{uuid.uuid4().hex[:8]}"
    try:
        db.execute(
            _t(
                """
                INSERT INTO user_events
                    (event_type, username, ip_address, user_agent, path, method, payload)
                VALUES
                    ('login', :u, CAST('9.9.9.9' AS inet), 'pytest-ua', NULL, NULL,
                     CAST('{}' AS jsonb)),
                    ('estimate_request', :u, CAST('9.9.9.9' AS inet), 'pytest-ua',
                     '/api/v1/trade-in/estimate', 'POST',
                     CAST(:payload AS jsonb))
                """
            ),
            {"u": username, "payload": '{"address": "ул. Тестовая, 1", "area_m2": "42.0"}'},
        )
        db.commit()

        application = FastAPI()
        application.include_router(audit_module.router, prefix="/api/v1/admin")

        def _override_db() -> Any:
            yield SessionLocal()

        application.dependency_overrides[get_db] = _override_db
        client = TestClient(application)

        accounts = client.get("/api/v1/admin/audit/accounts").json()
        row = next(r for r in accounts if r["username"] == username)
        assert row["login_count"] == 1
        assert row["search_count"] == 1

        drilldown = client.get(f"/api/v1/admin/audit/accounts/{username}").json()
        assert len(drilldown["searches"]) == 1
        assert drilldown["searches"][0]["address"] == "ул. Тестовая, 1"
        assert len(drilldown["recent_activity"]) == 2

        dashboard = client.get("/api/v1/admin/analytics?days=1").json()
        assert dashboard["summary"]["total_events"] >= 2
    finally:
        db.rollback()
        db.execute(_t("DELETE FROM user_events WHERE username = :u"), {"u": username})
        db.commit()
