"""Tests for authentication."""

from __future__ import annotations

from collections.abc import Iterator
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from cryptography.hazmat.primitives.asymmetric import rsa
from fastapi.testclient import TestClient

from ilum.api.app import create_app
from ilum.api.deps import set_manager
from ilum.api.operations import OperationStore

from .conftest import TEST_API_KEY


class TestApiKeyAuth:
    def test_valid_api_key_allowed(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        """Requests with valid X-API-Key header should succeed."""
        mock_api_manager.fetch_computed_values.return_value = {}
        resp = api_client.get("/api/v1/modules")
        assert resp.status_code == 200

    def test_missing_api_key_rejected(self, unauthed_client: TestClient) -> None:
        """Requests without auth should get 401."""
        resp = unauthed_client.get("/api/v1/modules")
        assert resp.status_code == 401

    def test_wrong_api_key_rejected(self, unauthed_client: TestClient) -> None:
        """Requests with wrong X-API-Key should get 401."""
        resp = unauthed_client.get("/api/v1/modules", headers={"X-API-Key": "wrong-key"})
        assert resp.status_code == 401


class TestNginxCookieAuth:
    def test_nginx_header_allowed(
        self, unauthed_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        """Requests with X-Ilum-Authenticated: true should succeed."""
        mock_api_manager.fetch_computed_values.return_value = {}
        resp = unauthed_client.get(
            "/api/v1/modules",
            headers={"X-Ilum-Authenticated": "true"},
        )
        assert resp.status_code == 200

    def test_nginx_header_false_rejected(self, unauthed_client: TestClient) -> None:
        """X-Ilum-Authenticated: false should not grant access."""
        resp = unauthed_client.get(
            "/api/v1/modules",
            headers={"X-Ilum-Authenticated": "false"},
        )
        assert resp.status_code == 401


class TestHealthNoAuth:
    def test_health_no_auth_needed(self, unauthed_client: TestClient) -> None:
        """Health endpoint never requires auth."""
        resp = unauthed_client.get("/api/v1/health")
        assert resp.status_code == 200

    def test_release_requires_auth(self, unauthed_client: TestClient) -> None:
        """Release endpoint requires auth."""
        resp = unauthed_client.get("/api/v1/release")
        assert resp.status_code == 401

    def test_pods_requires_auth(self, unauthed_client: TestClient) -> None:
        """Pods endpoint requires auth."""
        resp = unauthed_client.get("/api/v1/pods")
        assert resp.status_code == 401


# ---------------------------------------------------------------------------
# Helper to build a TestClient with Bearer-specific env vars
# ---------------------------------------------------------------------------


@pytest.fixture()
def _bearer_client_base(
    mock_api_manager: MagicMock,
    rsa_key_pair: tuple[rsa.RSAPrivateKey, str],
    monkeypatch: pytest.MonkeyPatch,
) -> Iterator[tuple[TestClient, MagicMock]]:
    """TestClient configured with JWT public key for local validation."""
    _, public_pem = rsa_key_pair
    monkeypatch.setenv("ILUM_API_KEY", TEST_API_KEY)
    monkeypatch.setenv("ILUM_JWT_PUBLIC_KEY", public_pem)
    monkeypatch.setenv("ILUM_JWT_ISSUER_URI", "https://ilum.cloud")
    monkeypatch.setenv("ILUM_JWT_AUDIENCES", "")
    # Reload module-level config in auth.py
    import ilum.api.auth as auth_mod

    monkeypatch.setattr(auth_mod, "_JWT_PUBLIC_KEY", public_pem)
    monkeypatch.setattr(auth_mod, "_JWT_ISSUER_URI", "https://ilum.cloud")
    monkeypatch.setattr(auth_mod, "_JWT_AUDIENCES", "")
    monkeypatch.setattr(auth_mod, "_token_cache", auth_mod._TokenCache())
    with patch("ilum.api.startup.auto_connect"):
        app = create_app()
        set_manager(mock_api_manager)  # type: ignore[arg-type]
        import ilum.api.operations as ops_mod

        ops_mod._store = OperationStore()
        import ilum.api.chart_cache as cache_mod

        cache_mod._chart_defaults = None
        with TestClient(app, raise_server_exceptions=False) as client:
            yield client, mock_api_manager

    set_manager(None)  # type: ignore[arg-type]
    import ilum.api.operations as ops_mod2

    ops_mod2._store = None
    import ilum.api.chart_cache as cache_mod2

    cache_mod2._chart_defaults = None


@pytest.fixture()
def _forwarding_client_base(
    mock_api_manager: MagicMock,
    monkeypatch: pytest.MonkeyPatch,
) -> Iterator[tuple[TestClient, MagicMock]]:
    """TestClient with NO JWT public key — forces forwarding path."""
    monkeypatch.setenv("ILUM_API_KEY", TEST_API_KEY)
    monkeypatch.setenv("ILUM_JWT_PUBLIC_KEY", "")
    monkeypatch.setenv("ILUM_CORE_URL", "http://ilum-core:9888")
    import ilum.api.auth as auth_mod

    monkeypatch.setattr(auth_mod, "_JWT_PUBLIC_KEY", "")
    monkeypatch.setattr(auth_mod, "_CORE_URL", "http://ilum-core:9888")
    monkeypatch.setattr(auth_mod, "_token_cache", auth_mod._TokenCache())
    with patch("ilum.api.startup.auto_connect"):
        app = create_app()
        set_manager(mock_api_manager)  # type: ignore[arg-type]
        import ilum.api.operations as ops_mod

        ops_mod._store = OperationStore()
        import ilum.api.chart_cache as cache_mod

        cache_mod._chart_defaults = None
        with TestClient(app, raise_server_exceptions=False) as client:
            yield client, mock_api_manager

    set_manager(None)  # type: ignore[arg-type]
    import ilum.api.operations as ops_mod2

    ops_mod2._store = None
    import ilum.api.chart_cache as cache_mod2

    cache_mod2._chart_defaults = None


# ---------------------------------------------------------------------------
# Local JWT validation tests
# ---------------------------------------------------------------------------


class TestBearerLocalJwt:
    """Bearer token auth with local JWT validation (public key configured)."""

    def test_valid_jwt_allowed(
        self,
        _bearer_client_base: tuple[TestClient, MagicMock],
        valid_jwt: str,
    ) -> None:
        client, mgr = _bearer_client_base
        mgr.fetch_computed_values.return_value = {}
        resp = client.get(
            "/api/v1/modules",
            headers={"Authorization": f"Bearer {valid_jwt}"},
        )
        assert resp.status_code == 200

    def test_expired_jwt_rejected(
        self,
        _bearer_client_base: tuple[TestClient, MagicMock],
        expired_jwt: str,
    ) -> None:
        client, _ = _bearer_client_base
        resp = client.get(
            "/api/v1/modules",
            headers={"Authorization": f"Bearer {expired_jwt}"},
        )
        assert resp.status_code == 401

    def test_wrong_signature_rejected(
        self,
        _bearer_client_base: tuple[TestClient, MagicMock],
        wrong_key_jwt: str,
    ) -> None:
        client, _ = _bearer_client_base
        resp = client.get(
            "/api/v1/modules",
            headers={"Authorization": f"Bearer {wrong_key_jwt}"},
        )
        assert resp.status_code == 401

    def test_wrong_issuer_rejected(
        self,
        _bearer_client_base: tuple[TestClient, MagicMock],
        wrong_issuer_jwt: str,
    ) -> None:
        client, _ = _bearer_client_base
        resp = client.get(
            "/api/v1/modules",
            headers={"Authorization": f"Bearer {wrong_issuer_jwt}"},
        )
        assert resp.status_code == 401


# ---------------------------------------------------------------------------
# Forwarding path tests (mock httpx)
# ---------------------------------------------------------------------------


class TestBearerForwarding:
    """Bearer token auth with ilum-core forwarding (no public key)."""

    def test_core_returns_200_authenticated(
        self,
        _forwarding_client_base: tuple[TestClient, MagicMock],
    ) -> None:
        client, mgr = _forwarding_client_base
        mgr.fetch_computed_values.return_value = {}
        mock_response = MagicMock()
        mock_response.status_code = 200
        with patch("ilum.api.auth.httpx.AsyncClient") as mock_httpx:
            mock_instance = AsyncMock()
            mock_instance.post.return_value = mock_response
            mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
            mock_instance.__aexit__ = AsyncMock(return_value=False)
            mock_httpx.return_value = mock_instance
            resp = client.get(
                "/api/v1/modules",
                headers={"Authorization": "Bearer some-token"},
            )
        assert resp.status_code == 200

    def test_core_returns_401_rejected(
        self,
        _forwarding_client_base: tuple[TestClient, MagicMock],
    ) -> None:
        client, _ = _forwarding_client_base
        mock_response = MagicMock()
        mock_response.status_code = 401
        with patch("ilum.api.auth.httpx.AsyncClient") as mock_httpx:
            mock_instance = AsyncMock()
            mock_instance.post.return_value = mock_response
            mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
            mock_instance.__aexit__ = AsyncMock(return_value=False)
            mock_httpx.return_value = mock_instance
            resp = client.get(
                "/api/v1/modules",
                headers={"Authorization": "Bearer bad-token"},
            )
        assert resp.status_code == 401

    def test_core_timeout_rejected(
        self,
        _forwarding_client_base: tuple[TestClient, MagicMock],
    ) -> None:
        client, _ = _forwarding_client_base
        import httpx as httpx_mod

        with patch("ilum.api.auth.httpx.AsyncClient") as mock_httpx:
            mock_instance = AsyncMock()
            mock_instance.post.side_effect = httpx_mod.TimeoutException("timeout")
            mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
            mock_instance.__aexit__ = AsyncMock(return_value=False)
            mock_httpx.return_value = mock_instance
            resp = client.get(
                "/api/v1/modules",
                headers={"Authorization": "Bearer some-token"},
            )
        assert resp.status_code == 401

    def test_cache_hit_skips_http_call(
        self,
        _forwarding_client_base: tuple[TestClient, MagicMock],
    ) -> None:
        client, mgr = _forwarding_client_base
        mgr.fetch_computed_values.return_value = {}
        mock_response = MagicMock()
        mock_response.status_code = 200
        with patch("ilum.api.auth.httpx.AsyncClient") as mock_httpx:
            mock_instance = AsyncMock()
            mock_instance.post.return_value = mock_response
            mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
            mock_instance.__aexit__ = AsyncMock(return_value=False)
            mock_httpx.return_value = mock_instance
            # First call — populates cache
            resp1 = client.get(
                "/api/v1/modules",
                headers={"Authorization": "Bearer cached-token"},
            )
            assert resp1.status_code == 200
            first_call_count = mock_httpx.call_count
            # Second call — should use cache, no new httpx client created
            resp2 = client.get(
                "/api/v1/modules",
                headers={"Authorization": "Bearer cached-token"},
            )
            assert resp2.status_code == 200
            assert mock_httpx.call_count == first_call_count

    def test_cache_miss_after_ttl(
        self,
        _forwarding_client_base: tuple[TestClient, MagicMock],
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        client, mgr = _forwarding_client_base
        mgr.fetch_computed_values.return_value = {}
        mock_response = MagicMock()
        mock_response.status_code = 200

        import ilum.api.auth as auth_mod

        call_count = 0

        with patch("ilum.api.auth.httpx.AsyncClient") as mock_httpx:
            mock_instance = AsyncMock()
            mock_instance.post.return_value = mock_response
            mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
            mock_instance.__aexit__ = AsyncMock(return_value=False)
            mock_httpx.return_value = mock_instance

            # First call
            resp1 = client.get(
                "/api/v1/modules",
                headers={"Authorization": "Bearer ttl-token"},
            )
            assert resp1.status_code == 200
            call_count = mock_httpx.call_count

            # Expire cache by manipulating the store directly
            for key in auth_mod._token_cache._store:
                auth_mod._token_cache._store[key] = (True, 0.0)

            # Second call — cache expired, should make new HTTP call
            resp2 = client.get(
                "/api/v1/modules",
                headers={"Authorization": "Bearer ttl-token"},
            )
            assert resp2.status_code == 200
            assert mock_httpx.call_count > call_count


# ---------------------------------------------------------------------------
# Integration: Bearer + other auth methods
# ---------------------------------------------------------------------------


class TestBearerWithOtherAuth:
    """Bearer failure should not block other auth methods."""

    def test_bearer_fails_api_key_works(
        self, unauthed_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.fetch_computed_values.return_value = {}
        resp = unauthed_client.get(
            "/api/v1/modules",
            headers={
                "Authorization": "Bearer invalid-garbage",
                "X-API-Key": TEST_API_KEY,
            },
        )
        assert resp.status_code == 200

    def test_bearer_fails_nginx_cookie_works(
        self, unauthed_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.fetch_computed_values.return_value = {}
        resp = unauthed_client.get(
            "/api/v1/modules",
            headers={
                "Authorization": "Bearer invalid-garbage",
                "X-Ilum-Authenticated": "true",
            },
        )
        assert resp.status_code == 200

    def test_all_three_fail_gives_401(self, unauthed_client: TestClient) -> None:
        resp = unauthed_client.get(
            "/api/v1/modules",
            headers={
                "Authorization": "Bearer invalid-garbage",
                "X-API-Key": "wrong-key",
                "X-Ilum-Authenticated": "false",
            },
        )
        assert resp.status_code == 401
