"""gcloud_auth 모듈의 단위 테스트.

TDD RED 단계: 구현 전 테스트 먼저 작성.
"""

from __future__ import annotations

import sys
import time
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

sys.path.insert(0, str(Path(__file__).parent))


# ---------------------------------------------------------------------------
# 토큰 캐싱 동작 테스트
# ---------------------------------------------------------------------------


class TestTokenCaching:
    def test_get_access_token_returns_string(self) -> None:
        """get_access_token()은 문자열 토큰을 반환한다."""
        import gcloud_auth

        mock_creds = MagicMock()
        mock_creds.token = "cached-token-abc"
        mock_creds.valid = True
        mock_creds.expired = False

        with patch("google.auth.default", return_value=(mock_creds, "project-id")):
            with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
                token = gcloud_auth.get_access_token()

        assert isinstance(token, str)
        assert len(token) > 0

    def test_cached_token_is_reused(self) -> None:
        """유효한 캐시 토큰이 있으면 재획득 없이 반환한다."""
        import gcloud_auth

        future_expiry = time.time() + 3600  # 1시간 후 만료
        with patch.object(
            gcloud_auth,
            "_token_cache",
            {"token": "already-cached-token", "expiry": future_expiry},
        ):
            with patch("google.auth.default") as mock_adc:
                token = gcloud_auth.get_access_token()

        # ADC가 호출되지 않아야 함
        mock_adc.assert_not_called()
        assert token == "already-cached-token"

    def test_expired_cache_triggers_renewal(self) -> None:
        """만료된 캐시 토큰은 새 토큰을 획득한다."""
        import gcloud_auth

        past_expiry = time.time() - 10  # 이미 만료

        mock_creds = MagicMock()
        mock_creds.token = "new-token-after-expiry"
        mock_creds.valid = True
        mock_creds.expired = False

        with patch.object(
            gcloud_auth,
            "_token_cache",
            {"token": "old-expired-token", "expiry": past_expiry},
        ):
            with patch("google.auth.default", return_value=(mock_creds, "project-id")):
                with patch("google.auth.transport.requests.Request"):
                    token = gcloud_auth.get_access_token()

        assert token == "new-token-after-expiry"

    def test_token_near_expiry_triggers_renewal(self) -> None:
        """만료 5분 이내 토큰은 미리 갱신한다."""
        import gcloud_auth

        near_expiry = time.time() + 200  # 3분 20초 후 만료 (5분 미만)

        mock_creds = MagicMock()
        mock_creds.token = "refreshed-token"
        mock_creds.valid = True
        mock_creds.expired = False

        with patch.object(
            gcloud_auth,
            "_token_cache",
            {"token": "soon-expiring-token", "expiry": near_expiry},
        ):
            with patch("google.auth.default", return_value=(mock_creds, "project-id")):
                with patch("google.auth.transport.requests.Request"):
                    token = gcloud_auth.get_access_token()

        assert token == "refreshed-token"

    def test_valid_cache_not_near_expiry_not_renewed(self) -> None:
        """만료 5분 이상 남은 토큰은 갱신하지 않는다."""
        import gcloud_auth

        safe_expiry = time.time() + 600  # 10분 후 만료

        with patch.object(
            gcloud_auth,
            "_token_cache",
            {"token": "valid-token-10min", "expiry": safe_expiry},
        ):
            with patch("google.auth.default") as mock_adc:
                token = gcloud_auth.get_access_token()

        mock_adc.assert_not_called()
        assert token == "valid-token-10min"


# ---------------------------------------------------------------------------
# ADC 실패 시 gcloud fallback 테스트
# ---------------------------------------------------------------------------


class TestGcloudFallback:
    def test_falls_back_to_gcloud_when_adc_fails(self) -> None:
        """SA 및 ADC 실패 시 gcloud CLI fallback으로 토큰을 획득한다."""
        import gcloud_auth

        with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
            with patch.object(gcloud_auth, "get_service_account_token", side_effect=RuntimeError("SA 없음")):
                with patch("google.auth.default", side_effect=Exception("ADC 설정 없음")):
                    mock_result = MagicMock()
                    mock_result.stdout = "gcloud-fallback-token\n"
                    with patch("subprocess.run", return_value=mock_result):
                        token = gcloud_auth.get_access_token()

        assert token == "gcloud-fallback-token"

    def test_gcloud_fallback_raises_when_empty_token(self) -> None:
        """gcloud fallback에서 빈 토큰이 반환되면 RuntimeError를 발생시킨다."""
        import gcloud_auth

        with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
            with patch.object(gcloud_auth, "get_service_account_token", side_effect=RuntimeError("SA 없음")):
                with patch("google.auth.default", side_effect=Exception("ADC 없음")):
                    mock_result = MagicMock()
                    mock_result.stdout = "   \n"
                    with patch("subprocess.run", return_value=mock_result):
                        with pytest.raises(RuntimeError, match="빈 토큰"):
                            gcloud_auth.get_access_token()

    def test_gcloud_fallback_raises_when_all_fail(self) -> None:
        """SA, ADC, gcloud CLI 모두 실패하면 RuntimeError를 발생시킨다."""
        import gcloud_auth

        with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
            with patch.object(gcloud_auth, "get_service_account_token", side_effect=RuntimeError("SA 없음")):
                with patch("google.auth.default", side_effect=Exception("ADC 없음")):
                    with patch("subprocess.run", side_effect=Exception("gcloud 없음")):
                        with pytest.raises(RuntimeError):
                            gcloud_auth.get_access_token()

    def test_adc_token_refreshed_if_expired(self) -> None:
        """ADC 크레덴셜이 만료된 경우 refresh를 호출한다."""
        import gcloud_auth

        mock_creds = MagicMock()
        mock_creds.valid = False
        mock_creds.expired = True
        mock_creds.token = "refreshed-adc-token"

        with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
            with patch("google.auth.default", return_value=(mock_creds, "project-id")):
                mock_request = MagicMock()
                with patch(
                    "google.auth.transport.requests.Request",
                    return_value=mock_request,
                ):
                    token = gcloud_auth.get_access_token()

        mock_creds.refresh.assert_called_once_with(mock_request)
        assert token == "refreshed-adc-token"


# ---------------------------------------------------------------------------
# 환경변수 로드 테스트
# ---------------------------------------------------------------------------


class TestEnvVarLoading:
    def test_load_env_keys_sets_google_creds(self, tmp_path: Path, monkeypatch: Any) -> None:
        """GOOGLE_APPLICATION_CREDENTIALS를 .env.keys에서 로드한다."""
        import gcloud_auth

        fake_env = tmp_path / ".env.keys"
        fake_creds_path = "/fake/path/service-account.json"
        fake_env.write_text(
            f'export GOOGLE_APPLICATION_CREDENTIALS="{fake_creds_path}"\n',
            encoding="utf-8",
        )

        monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False)
        gcloud_auth.load_env_keys(str(fake_env))

        import os

        assert os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") == fake_creds_path

    def test_load_env_keys_skips_missing_file(self) -> None:
        """존재하지 않는 .env.keys 파일은 조용히 무시한다."""
        import gcloud_auth

        # 예외 없이 실행되어야 함
        gcloud_auth.load_env_keys("/nonexistent/path/.env.keys")

    def test_load_env_keys_handles_no_google_creds_line(
        self, tmp_path: Path, monkeypatch: Any
    ) -> None:
        """GOOGLE_APPLICATION_CREDENTIALS가 없는 파일도 오류 없이 처리한다."""
        import gcloud_auth

        fake_env = tmp_path / ".env.keys"
        fake_env.write_text("export OTHER_VAR=some_value\n", encoding="utf-8")

        monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False)
        gcloud_auth.load_env_keys(str(fake_env))  # 예외 없이 실행

    def test_load_env_keys_without_quotes(self, tmp_path: Path, monkeypatch: Any) -> None:
        """따옴표 없는 GOOGLE_APPLICATION_CREDENTIALS 값도 파싱한다."""
        import gcloud_auth

        fake_env = tmp_path / ".env.keys"
        fake_creds_path = "/another/path/creds.json"
        fake_env.write_text(
            f"export GOOGLE_APPLICATION_CREDENTIALS={fake_creds_path}\n",
            encoding="utf-8",
        )

        monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False)
        gcloud_auth.load_env_keys(str(fake_env))

        import os

        assert os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") == fake_creds_path


# ---------------------------------------------------------------------------
# 로깅 이벤트 테스트
# ---------------------------------------------------------------------------


class TestLogging:
    def test_get_access_token_logs_on_adc_success(self) -> None:
        """ADC 성공 시 로그 메시지가 기록된다."""
        import gcloud_auth
        import logging

        mock_creds = MagicMock()
        mock_creds.token = "log-test-token"
        mock_creds.valid = True
        mock_creds.expired = False

        with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
            with patch("google.auth.default", return_value=(mock_creds, "project-id")):
                with patch("google.auth.transport.requests.Request"):
                    with patch.object(gcloud_auth.logger, "info") as mock_log:
                        gcloud_auth.get_access_token()

        assert mock_log.called

    def test_get_access_token_logs_warning_on_adc_failure(self) -> None:
        """SA 및 ADC 실패 시 경고 로그가 기록된다."""
        import gcloud_auth

        with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
            with patch.object(gcloud_auth, "get_service_account_token", side_effect=RuntimeError("SA 없음")):
                with patch("google.auth.default", side_effect=Exception("ADC 없음")):
                    mock_result = MagicMock()
                    mock_result.stdout = "fallback-token\n"
                    with patch("subprocess.run", return_value=mock_result):
                        with patch.object(gcloud_auth.logger, "warning") as mock_warn:
                            gcloud_auth.get_access_token()

        assert mock_warn.called

    def test_logger_is_named_gcloud_auth(self) -> None:
        """logger 이름이 'gcloud_auth'이다."""
        import gcloud_auth

        assert gcloud_auth.logger.name == "gcloud_auth"


# ---------------------------------------------------------------------------
# 토큰 캐시 구조 테스트
# ---------------------------------------------------------------------------


class TestTokenCacheStructure:
    def test_token_cache_has_required_keys(self) -> None:
        """_token_cache에 'token'과 'expiry' 키가 있다."""
        import gcloud_auth

        assert "token" in gcloud_auth._token_cache
        assert "expiry" in gcloud_auth._token_cache

    def test_token_cache_initially_empty(self) -> None:
        """모듈 초기 상태에서 _token_cache.token은 None이거나 문자열이다."""
        import gcloud_auth

        cache_token = gcloud_auth._token_cache["token"]
        assert cache_token is None or isinstance(cache_token, str)

    def test_get_access_token_updates_cache(self) -> None:
        """get_access_token() 성공 후 _token_cache에 토큰이 저장된다."""
        import gcloud_auth

        mock_creds = MagicMock()
        mock_creds.token = "cache-update-token"
        mock_creds.valid = True
        mock_creds.expired = False

        with patch.object(gcloud_auth, "_token_cache", {"token": None, "expiry": None}):
            with patch("google.auth.default", return_value=(mock_creds, "project-id")):
                with patch("google.auth.transport.requests.Request"):
                    gcloud_auth.get_access_token()
            assert gcloud_auth._token_cache["token"] == "cache-update-token"
            assert gcloud_auth._token_cache["expiry"] is not None
