"""Tests for _transport._auth — session resolution + interceptor."""

from __future__ import annotations

import json
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from grpc_interceptor import ClientCallDetails

from radiens_core._transport._auth import (
    AuthInterceptor,
    _cognito_client_id,
    _generate_local_hardware_uuid,
    _load_session_file,
    _session_dir,
)

# ===================================================================
# _session_dir
# ===================================================================


class TestSessionDir:
    def test_darwin(self) -> None:
        with patch.object(sys, "platform", "darwin"):
            result = _session_dir()
            assert result is not None
            assert result == Path.home() / "Library" / "Application Support" / "radiens"

    def test_linux(self) -> None:
        with patch.object(sys, "platform", "linux"):
            result = _session_dir()
            assert result is not None
            assert result == Path.home() / ".config" / "radiens"

    def test_win32(self) -> None:
        with patch.object(sys, "platform", "win32"):
            result = _session_dir()
            assert result is not None
            assert result == Path.home() / "AppData" / "Roaming" / "radiens"

    def test_unsupported(self) -> None:
        with patch.object(sys, "platform", "freebsd"):
            assert _session_dir() is None


# ===================================================================
# _cognito_client_id
# ===================================================================


class TestCognitoClientId:
    def test_prod(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("RADIENS_DEV", raising=False)
        monkeypatch.delenv("RADIENS_QA", raising=False)
        assert _cognito_client_id() == "62hgblnnv0dbfrsqad03ub44td"

    def test_dev(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("RADIENS_DEV", "1")
        monkeypatch.delenv("RADIENS_QA", raising=False)
        assert _cognito_client_id() == "1q4ugfpgijro91l1cjdhau60cq"

    def test_qa(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("RADIENS_DEV", raising=False)
        monkeypatch.setenv("RADIENS_QA", "1")
        assert _cognito_client_id() == "1q4ugfpgijro91l1cjdhau60cq"


# ===================================================================
# _load_session_file
# ===================================================================


class TestLoadSessionFile:
    @pytest.fixture()
    def session_dir(self, tmp_path: Path) -> Path:
        return tmp_path

    def _write_session(self, session_dir: Path, data: dict[str, object]) -> None:
        (session_dir / "session.json").write_text(json.dumps(data))

    def test_missing_file(self, session_dir: Path) -> None:
        assert _load_session_file("client123", session_dir) == (None, None)

    def test_empty_file(self, session_dir: Path) -> None:
        (session_dir / "session.json").write_text("")
        assert _load_session_file("client123", session_dir) == (None, None)

    def test_no_cognito_provider(self, session_dir: Path) -> None:
        self._write_session(session_dir, {"other_key": "value"})
        assert _load_session_file("client123", session_dir) == (None, None)

    def test_wrong_client_id(self, session_dir: Path) -> None:
        self._write_session(
            session_dir,
            {
                "CognitoIdentityServiceProvider": {
                    "other_client": {"LastAuthUser": "user@test.com"},
                },
            },
        )
        assert _load_session_file("client123", session_dir) == (None, None)

    def test_valid_session(self, session_dir: Path) -> None:
        self._write_session(
            session_dir,
            {
                "CognitoIdentityServiceProvider": {
                    "client123": {
                        "LastAuthUser": "user@test.com",
                        "user@test.com": {
                            "refreshToken": "refresh_abc",
                            "deviceKey": "device_xyz",
                        },
                    },
                },
            },
        )
        token, key = _load_session_file("client123", session_dir)
        assert token == "refresh_abc"
        assert key == "device_xyz"

    def test_missing_refresh_token(self, session_dir: Path) -> None:
        self._write_session(
            session_dir,
            {
                "CognitoIdentityServiceProvider": {
                    "client123": {
                        "LastAuthUser": "user@test.com",
                        "user@test.com": {
                            "deviceKey": "device_xyz",
                        },
                    },
                },
            },
        )
        token, key = _load_session_file("client123", session_dir)
        assert token is None
        assert key == "device_xyz"

    def test_invalid_json(self, session_dir: Path) -> None:
        (session_dir / "session.json").write_text("not json{{{")
        assert _load_session_file("client123", session_dir) == (None, None)


# ===================================================================
# _generate_local_hardware_uuid
# ===================================================================


class TestLocalHardwareUuid:
    def test_darwin(self) -> None:
        mock_output = '"IOPlatformUUID" = "FAKE-UUID-1234-5678"'
        with (
            patch("radiens_core._transport._auth.platform") as mock_platform,
            patch("radiens_core._transport._auth.subprocess") as mock_subprocess,
        ):
            mock_platform.system.return_value = "Darwin"
            mock_subprocess.check_output.return_value = mock_output
            mock_subprocess.CalledProcessError = OSError
            result = _generate_local_hardware_uuid()
            assert result == "FAKE-UUID-1234-5678"

    def test_unsupported_returns_empty(self) -> None:
        with patch("radiens_core._transport._auth.platform") as mock_platform:
            mock_platform.system.return_value = "FreeBSD"
            assert _generate_local_hardware_uuid() == ""


# ===================================================================
# AuthInterceptor — unit tests (no real gRPC)
# ===================================================================


class TestAuthInterceptor:
    @staticmethod
    def _make_call_details() -> ClientCallDetails:  # type: ignore[explicit-any]
        return ClientCallDetails("/test.Method", None, [], None, None, None)

    def test_bearer_token_attached(self) -> None:
        """Pre-resolve with a Bearer token and verify metadata injection."""
        interceptor = AuthInterceptor()
        # Manually pre-resolve to avoid the full chain
        interceptor._resolved = True
        interceptor._authorization = "Bearer test-token-123"

        method = MagicMock(return_value="response")
        details = self._make_call_details()

        interceptor.intercept(method, "request", details)

        call_args = method.call_args
        new_details: ClientCallDetails = call_args[0][1]  # type: ignore[explicit-any]
        assert new_details.metadata is not None
        metadata = dict(new_details.metadata)
        assert metadata["authorization"] == "Bearer test-token-123"

    def test_device_uuid_attached(self) -> None:
        interceptor = AuthInterceptor()
        interceptor._resolved = True
        interceptor._authorization = "deviceUUID FAKE-UUID"

        method = MagicMock(return_value="response")
        details = self._make_call_details()

        interceptor.intercept(method, "request", details)

        call_args = method.call_args
        new_details: ClientCallDetails = call_args[0][1]  # type: ignore[explicit-any]
        assert new_details.metadata is not None
        metadata = dict(new_details.metadata)
        assert metadata["authorization"] == "deviceUUID FAKE-UUID"

    def test_no_auth_passes_through(self) -> None:
        interceptor = AuthInterceptor()
        interceptor._resolved = True
        interceptor._authorization = None

        method = MagicMock(return_value="response")
        details = self._make_call_details()

        interceptor.intercept(method, "request", details)

        # Original call_details passed through unchanged
        method.assert_called_once_with("request", details)

    def test_existing_auth_replaced(self) -> None:
        """Ensure pre-existing authorization metadata is replaced."""
        interceptor = AuthInterceptor()
        interceptor._resolved = True
        interceptor._authorization = "Bearer new-token"

        method = MagicMock(return_value="response")
        details = ClientCallDetails(
            "/test.Method",
            None,
            [("authorization", "Bearer old-token"), ("other", "keep")],
            None,
            None,
            None,
        )

        interceptor.intercept(method, "request", details)

        call_args = method.call_args
        new_details: ClientCallDetails = call_args[0][1]  # type: ignore[explicit-any]
        assert new_details.metadata is not None
        metadata_dict = dict(new_details.metadata)
        assert metadata_dict["authorization"] == "Bearer new-token"
        assert metadata_dict["other"] == "keep"

    def test_resolve_runs_once(self) -> None:
        """Verify the lazy resolution only runs once."""
        interceptor = AuthInterceptor()

        with patch.object(interceptor, "_resolve") as mock_resolve:
            interceptor._ensure_resolved()
            interceptor._ensure_resolved()
            mock_resolve.assert_called_once()

    def test_offline_env_skips_cognito(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """RADIENS_OFFLINE=1 skips Cognito strategies entirely."""
        monkeypatch.setenv("RADIENS_OFFLINE", "1")
        interceptor = AuthInterceptor()

        with (
            patch("radiens_core._transport._auth._try_cognito_refresh") as mock_refresh,
            patch("radiens_core._transport._auth._try_cognito_password") as mock_pw,
            patch("radiens_core._transport._auth._generate_local_hardware_uuid", return_value="LOCAL-UUID"),
        ):
            interceptor._resolve()
            mock_refresh.assert_not_called()
            mock_pw.assert_not_called()
            assert interceptor._authorization == "deviceUUID LOCAL-UUID"

    def test_resolve_chain_stops_at_refresh(self) -> None:
        """If refresh token works, don't try subsequent strategies."""
        interceptor = AuthInterceptor()

        with (
            patch("radiens_core._transport._auth._try_cognito_refresh", return_value="id-token-abc"),
            patch("radiens_core._transport._auth._try_cognito_password") as mock_pw,
            patch("radiens_core._transport._auth._try_server_uuid") as mock_server,
            patch("radiens_core._transport._auth._generate_local_hardware_uuid") as mock_local,
        ):
            interceptor._resolve()
            assert interceptor._authorization == "Bearer id-token-abc"
            mock_pw.assert_not_called()
            mock_server.assert_not_called()
            mock_local.assert_not_called()

    def test_resolve_falls_through_to_server_uuid(self) -> None:
        """Cognito fails → server UUID succeeds."""
        interceptor = AuthInterceptor(server_address="localhost:50051")

        with (
            patch("radiens_core._transport._auth._try_cognito_refresh", return_value=None),
            patch("radiens_core._transport._auth._try_cognito_password", return_value=None),
            patch("radiens_core._transport._auth._try_server_uuid", return_value="SERVER-UUID"),
            patch("radiens_core._transport._auth._generate_local_hardware_uuid") as mock_local,
        ):
            interceptor._resolve()
            assert interceptor._authorization == "deviceUUID SERVER-UUID"
            mock_local.assert_not_called()

    def test_resolve_falls_through_to_local(self) -> None:
        """Everything fails except local UUID."""
        interceptor = AuthInterceptor(server_address="localhost:50051")

        with (
            patch("radiens_core._transport._auth._try_cognito_refresh", return_value=None),
            patch("radiens_core._transport._auth._try_cognito_password", return_value=None),
            patch("radiens_core._transport._auth._try_server_uuid", return_value=""),
            patch("radiens_core._transport._auth._generate_local_hardware_uuid", return_value="LOCAL-UUID"),
        ):
            interceptor._resolve()
            assert interceptor._authorization == "deviceUUID LOCAL-UUID"

    def test_resolve_all_fail(self) -> None:
        """All strategies fail — authorization remains None."""
        interceptor = AuthInterceptor()

        with (
            patch("radiens_core._transport._auth._try_cognito_refresh", return_value=None),
            patch("radiens_core._transport._auth._try_cognito_password", return_value=None),
            patch("radiens_core._transport._auth._generate_local_hardware_uuid", return_value=""),
        ):
            interceptor._resolve()
            assert interceptor._authorization is None
