"""Tests for GCPIAMAnalyzer using respx mocks."""

from __future__ import annotations

from unittest.mock import patch

import httpx
import respx

from complyform.core.cloud_intelligence.iam_analyzer import IAMAnalysisResult
from complyform.core.cloud_intelligence.iam_gcp import GCPIAMAnalyzer

_BASE = "https://recommender.googleapis.com/v1"


def _rec_url(project: str) -> str:
    return (
        f"{_BASE}/projects/{project}"
        f"/locations/-/recommenders/google.iam.policy.Recommender/recommendations"
    )


def _insight_url(project: str) -> str:
    return (
        f"{_BASE}/projects/{project}"
        f"/locations/-/insightTypes/google.iam.policy.Insight/insights"
    )


def _make_recommendation(
    principal: str = "sa@proj.iam.gserviceaccount.com",
    subtype: str = "REPLACE_ROLE",
    current_role: str = "roles/editor",
    recommended_role: str = "roles/viewer",
) -> dict[str, object]:
    return {
        "name": (
            f"projects/proj/locations/-/recommenders/"
            f"google.iam.policy.Recommender/recommendations/{principal}"
        ),
        "recommenderSubtype": subtype,
        "priority": "P2",
        "content": {
            "operationGroups": [
                {
                    "operations": [
                        {
                            "resource": principal,
                            "resourceType": (
                                "cloudresourcemanager.googleapis.com/Project"
                            ),
                            "pathFilters": {"/iamPolicy/bindings/*/role": current_role},
                            "value": recommended_role
                            if subtype != "REMOVE_ROLE"
                            else None,
                        }
                    ]
                }
            ]
        },
        "associatedInsights": [],
    }


def _make_insight(
    expression: str = "storage.buckets.delete,compute.instances.delete",
    last_refresh: str = "2025-01-15T10:00:00Z",
) -> dict[str, object]:
    return {
        "name": (
            "projects/proj/locations/-/insightTypes/"
            "google.iam.policy.Insight/insights/test-insight"
        ),
        "content": {
            "condition": {"expression": expression},
        },
        "stateInfo": {
            "stateMetadata": {"lastRefreshTime": last_refresh},
        },
    }


class TestGCPIAMAnalyzerAnalyze:
    """Tests for GCPIAMAnalyzer.analyze()."""

    @respx.mock
    def test_two_recommendations(self) -> None:
        project = "my-project"
        rec1 = _make_recommendation(principal="sa1@proj.iam.gserviceaccount.com")
        rec2 = _make_recommendation(
            principal="sa2@proj.iam.gserviceaccount.com",
            subtype="REMOVE_ROLE",
            recommended_role="",
        )
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(200, json={"recommendations": [rec1, rec2]})
        )
        respx.get(_insight_url(project)).mock(
            return_value=httpx.Response(200, json={"insights": []})
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            result = analyzer.analyze()

        assert isinstance(result, IAMAnalysisResult)
        assert result.success is True
        assert len(result.findings) == 2
        assert result.findings[0].principal == "sa1@proj.iam.gserviceaccount.com"
        assert result.findings[1].principal == "sa2@proj.iam.gserviceaccount.com"
        assert result.findings[0].recommended_role == "roles/viewer"
        assert result.findings[1].recommended_role is None  # REMOVE_ROLE

    @respx.mock
    def test_insight_populates_fields(self) -> None:
        project = "my-project"
        insight = _make_insight()
        rec = _make_recommendation()
        rec["associatedInsights"] = [{"insight": insight["name"]}]  # type: ignore[index]
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(200, json={"recommendations": [rec]})
        )
        respx.get(_insight_url(project)).mock(
            return_value=httpx.Response(200, json={"insights": [insight]})
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            result = analyzer.analyze()

        assert result.success is True
        assert len(result.findings) == 1
        finding = result.findings[0]
        assert len(finding.unused_permissions) > 0
        assert finding.last_activity == "2025-01-15T10:00:00Z"

    @respx.mock
    def test_pagination_follows_next_page_token(self) -> None:
        project = "my-project"
        rec1 = _make_recommendation(principal="sa1@proj.iam.gserviceaccount.com")
        rec2 = _make_recommendation(principal="sa2@proj.iam.gserviceaccount.com")

        route = respx.get(_rec_url(project))
        route.side_effect = [
            httpx.Response(
                200,
                json={"recommendations": [rec1], "nextPageToken": "tok1"},
            ),
            httpx.Response(200, json={"recommendations": [rec2]}),
        ]
        respx.get(_insight_url(project)).mock(
            return_value=httpx.Response(200, json={"insights": []})
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            result = analyzer.analyze()

        assert result.success is True
        assert len(result.findings) == 2

    @respx.mock
    def test_safety_cap_stops_at_500(self) -> None:
        project = "my-project"
        # Create 501 recommendations.
        recs = [
            _make_recommendation(principal=f"sa{i}@proj.iam.gserviceaccount.com")
            for i in range(501)
        ]
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(200, json={"recommendations": recs})
        )
        respx.get(_insight_url(project)).mock(
            return_value=httpx.Response(200, json={"insights": []})
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            result = analyzer.analyze()

        assert result.success is True
        assert len(result.findings) <= 500

    @respx.mock
    def test_http_403_returns_success_false(self) -> None:
        project = "my-project"
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(
                403,
                json={"error": {"status": "PERMISSION_DENIED", "message": "denied"}},
            )
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            result = analyzer.analyze()

        assert result.success is False
        assert result.error is not None

    @respx.mock
    def test_http_429_retries_then_fails(self) -> None:
        project = "my-project"
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(429, json={"error": "rate limited"})
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with (
            patch.object(analyzer, "_get_auth_headers", return_value={}),
            patch("complyform.core.cloud_intelligence.iam_gcp.time.sleep"),
        ):
            result = analyzer.analyze()

        assert result.success is False

    @respx.mock
    def test_http_500_retries_then_fails(self) -> None:
        project = "my-project"
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(500, json={"error": "server error"})
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with (
            patch.object(analyzer, "_get_auth_headers", return_value={}),
            patch("complyform.core.cloud_intelligence.iam_gcp.time.sleep"),
        ):
            result = analyzer.analyze()

        assert result.success is False


class TestGCPIAMAnalyzerPrerequisites:
    """Tests for GCPIAMAnalyzer.check_prerequisites()."""

    @respx.mock
    def test_api_not_enabled(self) -> None:
        project = "my-project"
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(
                403,
                json={
                    "error": {
                        "status": "PERMISSION_DENIED",
                        "message": "Recommender API has not been enabled",
                    }
                },
            )
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            issues = analyzer.check_prerequisites()

        assert len(issues) == 1
        assert "gcloud services enable" in issues[0]

    @respx.mock
    def test_permission_denied(self) -> None:
        project = "my-project"
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(
                403,
                json={
                    "error": {
                        "status": "PERMISSION_DENIED",
                        "message": "The caller does not have permission",
                    }
                },
            )
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            issues = analyzer.check_prerequisites()

        assert len(issues) == 1
        assert "roles/recommender.iamViewer" in issues[0]

    @respx.mock
    def test_all_good(self) -> None:
        project = "my-project"
        respx.get(_rec_url(project)).mock(
            return_value=httpx.Response(200, json={"recommendations": []})
        )

        analyzer = GCPIAMAnalyzer(cloud="gcp", project_id=project)
        with patch.object(analyzer, "_get_auth_headers", return_value={}):
            issues = analyzer.check_prerequisites()

        assert issues == []
