"""Tests for backend.api.routes.dashboard_api."""

from __future__ import annotations

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

import pytest
from backend.api.middleware.api_key_auth import (
    APIKeyAuthError,
    APIKeyContext,
    require_api_key,
)
from backend.api.routes.dashboard_api import router
from fastapi import FastAPI
from fastapi.testclient import TestClient

# Module paths for patching (late imports inside function bodies)
_P_DB = "backend.shared.firestore.get_db"
_P_UPLOAD = "backend.shared.gcs.upload_findings"
_P_DL = "backend.shared.gcs.download_findings"


def _make_app() -> FastAPI:
    test_app = FastAPI()
    test_app.include_router(router)

    @test_app.exception_handler(APIKeyAuthError)
    async def _handler(
        request: Any,
        exc: APIKeyAuthError,
    ) -> Any:
        from fastapi.responses import JSONResponse

        return JSONResponse(
            {"error": exc.detail},
            status_code=exc.status_code,
        )

    return test_app


def _team_auth() -> APIKeyContext:
    return APIKeyContext(
        customer_id="cus_test",
        email="test@example.com",
        tier="team",
        features=["hosted_dashboard", "drift_alerts"],
        project_limit=1,
        batch_limit=10,
    )


def _agency_auth() -> APIKeyContext:
    return APIKeyContext(
        customer_id="cus_agency",
        email="agency@example.com",
        tier="agency",
        features=[
            "hosted_dashboard",
            "multi_project_dashboard",
        ],
        project_limit=5,
        batch_limit=10,
    )


def _community_auth() -> APIKeyContext:
    return APIKeyContext(
        customer_id="cus_free",
        email="free@example.com",
        tier="community",
        features=[],
        project_limit=1,
        batch_limit=None,
    )


def _push_body(**overrides: Any) -> dict[str, Any]:
    base: dict[str, Any] = {
        "project_label": "prod",
        "client_label": None,
        "cloud": "gcp",
        "frameworks": ["soc2"],
        "scan_source": "state",
        "score": 85.0,
        "passed": 80,
        "failed": 15,
        "manual_review": 3,
        "not_assessed": 2,
        "severity_counts": {
            "critical": 1,
            "high": 3,
            "medium": 5,
            "low": 6,
        },
        "resource_count": 50,
        "assessment_duration_ms": 2500,
        "findings": [{"id": "f1", "status": "fail"}],
        "state_hash": "sha256:abc123",
        "cloud_intelligence": None,
    }
    base.update(overrides)
    return base


class _FakeDoc:
    def __init__(
        self,
        data: dict[str, Any] | None = None,
        doc_id: str = "prod",
    ) -> None:
        self._data = data
        self.exists = data is not None
        self.id = doc_id

    def to_dict(self) -> dict[str, Any] | None:
        return self._data


def _set_project_ref(
    db: MagicMock,
    project_doc: _FakeDoc,
) -> MagicMock:
    """Wire db mock so project ref returns given doc."""
    cust = db.collection.return_value.document.return_value
    proj_ref = MagicMock()
    proj_ref.get = AsyncMock(return_value=project_doc)
    proj_ref.collection.return_value.document.return_value = MagicMock()
    cust.collection.return_value.document.return_value = proj_ref
    return proj_ref


class TestDashboardPush:
    """Tests for POST /v1/dashboard/push."""

    @pytest.fixture
    def app(self) -> FastAPI:
        return _make_app()

    @pytest.fixture
    def client(self, app: FastAPI) -> TestClient:
        return TestClient(app)

    def _override_auth(
        self,
        app: FastAPI,
        auth: APIKeyContext,
    ) -> None:
        """Override the require_api_key dependency."""
        app.dependency_overrides[require_api_key] = lambda: auth

    def test_push_invalid_project_label(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Label with uppercase/spaces gets 400."""
        self._override_auth(app, _team_auth())
        resp = client.post(
            "/v1/dashboard/push",
            json=_push_body(project_label="INVALID LABEL!"),
            headers={"X-API-Key": "test"},
        )
        assert resp.status_code == 400
        assert "project_label" in resp.json()["error"]

    def test_push_community_tier_blocked(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Community API key gets 403."""
        self._override_auth(app, _community_auth())
        resp = client.post(
            "/v1/dashboard/push",
            json=_push_body(),
            headers={"X-API-Key": "test"},
        )
        assert resp.status_code == 403
        assert "hosted_dashboard" in resp.json()["error"]

    def test_push_missing_api_key(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """No X-API-Key header returns 401."""

        async def _fail() -> None:
            raise APIKeyAuthError(
                401,
                "Missing X-API-Key header",
            )

        app.dependency_overrides[require_api_key] = _fail
        resp = client.post(
            "/v1/dashboard/push",
            json=_push_body(),
        )
        assert resp.status_code == 401

    def test_push_invalid_api_key(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Bad X-API-Key returns 401."""

        async def _fail() -> None:
            raise APIKeyAuthError(401, "Invalid API key")

        app.dependency_overrides[require_api_key] = _fail
        resp = client.post(
            "/v1/dashboard/push",
            json=_push_body(),
            headers={"X-API-Key": "badkey"},
        )
        assert resp.status_code == 401

    def test_push_suspended_project(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Push to suspended project returns 403."""
        self._override_auth(app, _team_auth())

        with patch(_P_DB) as mock_db:
            db = MagicMock()
            mock_db.return_value = db
            _set_project_ref(
                db,
                _FakeDoc({"status": "suspended"}),
            )
            resp = client.post(
                "/v1/dashboard/push",
                json=_push_body(),
                headers={"X-API-Key": "test"},
            )

        assert resp.status_code == 403
        assert "suspended" in resp.json()["error"]

    def test_push_project_limit_reached(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Team tier (limit=1), second project → 409."""
        self._override_auth(app, _team_auth())

        with patch(_P_DB) as mock_db:
            db = MagicMock()
            mock_db.return_value = db
            _set_project_ref(db, _FakeDoc(None))

            existing = _FakeDoc(
                {"project_label": "old-proj"},
                doc_id="old-proj",
            )
            cust = db.collection.return_value.document.return_value

            async def _stream() -> Any:
                yield existing

            cust.collection.return_value.stream = _stream

            resp = client.post(
                "/v1/dashboard/push",
                json=_push_body(project_label="new-project"),
                headers={"X-API-Key": "test"},
            )

        assert resp.status_code == 409
        assert "Limit reached" in resp.json()["error"]

    def test_push_findings_stored_in_gcs(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Verify findings uploaded to correct GCS path."""
        self._override_auth(app, _team_auth())
        findings = [{"id": "f1"}, {"id": "f2"}]

        with (
            patch(
                _P_UPLOAD,
                new_callable=AsyncMock,
            ) as mock_upload,
            patch(_P_DB) as mock_db,
        ):
            mock_upload.return_value = "gs://complyform-prod-findings/c/p/s.json"
            db = MagicMock()
            mock_db.return_value = db
            _set_project_ref(db, _FakeDoc(None))

            cust = db.collection.return_value.document.return_value

            async def _stream() -> Any:
                return
                yield  # async generator

            cust.collection.return_value.stream = _stream

            # Transaction will fail in test env (no real
            # Firestore) — that's OK, we verify upload
            # happened before the transaction attempt.
            db.transactional = lambda fn: fn
            db.transaction.return_value = MagicMock()

            client.post(
                "/v1/dashboard/push",
                json=_push_body(findings=findings),
                headers={"X-API-Key": "test"},
            )

            # Upload is called before the transaction
            mock_upload.assert_called_once()
            kw = mock_upload.call_args.kwargs
            assert kw["customer_id"] == "cus_test"
            assert kw["findings"] == findings


class TestDashboardProjects:
    """Tests for GET /v1/dashboard/projects."""

    @pytest.fixture
    def app(self) -> FastAPI:
        return _make_app()

    @pytest.fixture
    def client(self, app: FastAPI) -> TestClient:
        return TestClient(app)

    def test_get_projects(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """List projects returns active/dormant projects."""
        app.dependency_overrides[require_api_key] = lambda: _agency_auth()

        proj1 = _FakeDoc(
            {
                "project_label": "prod",
                "client_label": None,
                "cloud": "gcp",
                "frameworks": ["soc2"],
                "status": "active",
                "latest_scan": {
                    "score": 85.0,
                    "passed": 80,
                    "failed": 15,
                },
                "score_delta": 2.5,
                "severity_counts": {"critical": 1},
                "last_scan": "2024-01-01T00:00:00",
            },
            doc_id="prod",
        )
        proj2 = _FakeDoc(
            {
                "project_label": "staging",
                "client_label": "client-a",
                "cloud": "aws",
                "frameworks": ["hipaa"],
                "status": "active",
                "latest_scan": {
                    "score": 92.0,
                    "passed": 90,
                    "failed": 8,
                },
                "score_delta": -1.0,
                "severity_counts": {},
                "last_scan": "2024-01-02T00:00:00",
            },
            doc_id="staging",
        )

        with patch(_P_DB) as mock_db:
            db = MagicMock()
            mock_db.return_value = db
            cust = db.collection.return_value.document.return_value

            async def _stream() -> Any:
                yield proj1
                yield proj2

            cust.collection.return_value.stream = _stream

            resp = client.get(
                "/v1/dashboard/projects",
                headers={"X-API-Key": "test"},
            )

        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 2
        assert data[0]["project_id"] == "prod"
        assert data[0]["score"] == 85.0
        assert data[1]["project_id"] == "staging"

    def test_get_projects_cross_customer_isolation(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Customer A's key only sees their projects."""
        app.dependency_overrides[require_api_key] = lambda: _team_auth()

        with patch(_P_DB) as mock_db:
            db = MagicMock()
            mock_db.return_value = db
            cust = db.collection.return_value.document.return_value

            async def _stream() -> Any:
                return
                yield

            cust.collection.return_value.stream = _stream

            resp = client.get(
                "/v1/dashboard/projects",
                headers={"X-API-Key": "test"},
            )

        assert resp.status_code == 200
        assert resp.json() == []

        # Query path includes customer_id
        doc_call = mock_db.return_value.collection.return_value.document
        assert doc_call.call_args[0][0] == "cus_test"


class TestDashboardScans:
    """Tests for GET /v1/dashboard/projects/{id}/scans."""

    @pytest.fixture
    def app(self) -> FastAPI:
        return _make_app()

    @pytest.fixture
    def client(self, app: FastAPI) -> TestClient:
        return TestClient(app)

    def test_get_scans(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Returns scan history in DESC order."""
        app.dependency_overrides[require_api_key] = lambda: _team_auth()

        scan1 = _FakeDoc(
            {
                "scan_id": "s1",
                "score": 85,
                "timestamp": "2024-01-03",
            },
            doc_id="s1",
        )
        scan2 = _FakeDoc(
            {
                "scan_id": "s2",
                "score": 87,
                "timestamp": "2024-01-02",
            },
            doc_id="s2",
        )
        scan3 = _FakeDoc(
            {
                "scan_id": "s3",
                "score": 82,
                "timestamp": "2024-01-01",
            },
            doc_id="s3",
        )

        with patch(_P_DB) as mock_db:
            db = MagicMock()
            mock_db.return_value = db

            proj_ref = MagicMock()
            proj_ref.get = AsyncMock(
                return_value=_FakeDoc({"status": "active"}),
            )

            scan_query = MagicMock()

            async def _stream() -> Any:
                yield scan1
                yield scan2
                yield scan3

            scan_query.stream = _stream
            scan_query.limit.return_value = scan_query
            scan_query.start_after.return_value = scan_query

            proj_ref.collection.return_value.order_by.return_value = scan_query
            cust = db.collection.return_value.document.return_value
            cust.collection.return_value.document.return_value = proj_ref

            resp = client.get(
                "/v1/dashboard/projects/prod/scans",
                headers={"X-API-Key": "test"},
            )

        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 3


class TestDashboardFindings:
    """Tests for GET /v1/dashboard/projects/{id}/findings."""

    @pytest.fixture
    def app(self) -> FastAPI:
        return _make_app()

    @pytest.fixture
    def client(self, app: FastAPI) -> TestClient:
        return TestClient(app)

    def test_get_findings(
        self,
        app: FastAPI,
        client: TestClient,
    ) -> None:
        """Returns findings from GCS."""
        app.dependency_overrides[require_api_key] = lambda: _team_auth()
        findings = [
            {"id": "f1", "status": "fail"},
            {"id": "f2", "status": "pass"},
        ]
        uri = "gs://complyform-prod-findings/cus_test/prod/scan.json"

        with (
            patch(_P_DB) as mock_db,
            patch(
                _P_DL,
                new_callable=AsyncMock,
            ) as mock_dl,
        ):
            db = MagicMock()
            mock_db.return_value = db

            proj_ref = MagicMock()
            proj_ref.get = AsyncMock(
                return_value=_FakeDoc(
                    {"latest_scan": {"findings_uri": uri}},
                )
            )
            cust = db.collection.return_value.document.return_value
            cust.collection.return_value.document.return_value = proj_ref

            mock_dl.return_value = findings

            resp = client.get(
                "/v1/dashboard/projects/prod/findings",
                headers={"X-API-Key": "test"},
            )

        assert resp.status_code == 200
        assert resp.json() == findings
