"""Unit tests for registry/client.py module.

Tests RegistryClient class covering initialization, configuration fetching,
caching, retry logic, security validation, and error handling.
"""

import pytest
import time
import warnings
from unittest.mock import Mock, patch, MagicMock
from requests.exceptions import ConnectionError, Timeout, HTTPError

from mca_sdk.registry.client import (
    RegistryClient,
    CacheEntry,
    DEFAULT_CACHE_TTL_SECS,
    DEFAULT_TIMEOUT_SECS,
    MIN_REFRESH_INTERVAL_SECS,
)
from mca_sdk.registry.models import ModelConfig, DeploymentConfig
from mca_sdk.config.settings import SensitiveString
from mca_sdk.utils.exceptions import (
    RegistryError,
    RegistryConnectionError,
    RegistryConfigNotFoundError,
    RegistryAuthError,
)


class TestCacheEntry:
    """Test CacheEntry dataclass."""

    def test_cache_entry_creation(self):
        """Test CacheEntry can be created with value and expiration."""
        value = {"test": "data"}
        expires_at = time.time() + 600

        entry = CacheEntry(value=value, expires_at=expires_at)

        assert entry.value == value
        assert entry.expires_at == expires_at


class TestRegistryClientInit:
    """Test RegistryClient initialization."""

    def test_init_with_minimal_params(self):
        """Test initialization with only required URL parameter."""
        client = RegistryClient(url="https://registry.example.com")

        assert client._url == "https://registry.example.com"
        assert client._token is None
        assert client._timeout == DEFAULT_TIMEOUT_SECS
        assert client._cache_ttl == DEFAULT_CACHE_TTL_SECS
        assert isinstance(client._cache, dict)
        assert len(client._cache) == 0

    def test_init_with_all_params(self):
        """Test initialization with all parameters."""
        token = SensitiveString("test-token")
        client = RegistryClient(
            url="https://registry.example.com",
            token=token,
            timeout=10.0,
            cache_ttl_secs=300,
            refresh_interval_secs=0,  # Disable background refresh
        )

        assert client._url == "https://registry.example.com"
        assert isinstance(client._token, SensitiveString)
        assert client._timeout == 10.0
        assert client._cache_ttl == 300

    def test_init_strips_trailing_slash_from_url(self):
        """Test that trailing slash is removed from URL."""
        client = RegistryClient(url="https://registry.example.com/")

        assert client._url == "https://registry.example.com"

    def test_init_with_plain_string_token_auto_wraps(self):
        """Test that plain string token is auto-wrapped in SensitiveString."""
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")

            client = RegistryClient(
                url="https://registry.example.com",
                token="plain-token",
                refresh_interval_secs=0,
            )

            # Should have issued deprecation warning
            assert len(w) == 1
            assert issubclass(w[0].category, DeprecationWarning)
            assert "plain string tokens" in str(w[0].message).lower()

            # Token should be wrapped
            assert isinstance(client._token, SensitiveString)

    def test_init_with_sensitive_string_token_no_warning(self):
        """Test that SensitiveString token doesn't trigger warning."""
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")

            client = RegistryClient(
                url="https://registry.example.com",
                token=SensitiveString("secret-token"),
                refresh_interval_secs=0,
            )

            # Should not issue warning
            assert len(w) == 0

    def test_init_sets_auth_header_when_token_provided(self):
        """Test that Authorization header is set when token provided."""
        client = RegistryClient(
            url="https://registry.example.com",
            token=SensitiveString("test-token"),
            refresh_interval_secs=0,
        )

        # Header should be set in session
        assert "Authorization" in client._session.headers
        assert client._session.headers["Authorization"] == "Bearer test-token"

    def test_init_with_invalid_refresh_interval_raises_error(self):
        """Test that refresh interval below minimum raises ValueError."""
        with pytest.raises(ValueError, match="refresh_interval_secs must be >= 30"):
            RegistryClient(
                url="https://registry.example.com",
                refresh_interval_secs=10,  # Below minimum of 30
            )

    def test_init_with_zero_refresh_interval_allowed(self):
        """Test that refresh_interval_secs=0 is allowed (disables refresh)."""
        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=0)

        assert client._refresh_interval == 0

    def test_init_with_none_refresh_interval_allowed(self):
        """Test that refresh_interval_secs=None is allowed."""
        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=None)

        assert client._refresh_interval is None

    @patch("mca_sdk.registry.client.RegistryClient._start_refresh_thread")
    def test_init_starts_refresh_thread_when_configured(self, mock_start):
        """Test that refresh thread is started when interval is set."""
        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=60)

        mock_start.assert_called_once()

    def test_init_with_meter_creates_telemetry(self):
        """Test that telemetry is initialized when meter provided."""
        mock_meter = Mock()
        client = RegistryClient(
            url="https://registry.example.com",
            meter=mock_meter,
            refresh_interval_secs=0,
        )

        assert client._telemetry is not None

    def test_init_without_meter_no_telemetry(self):
        """Test that telemetry is None when no meter provided."""
        client = RegistryClient(url="https://registry.example.com")

        assert client._telemetry is None


class TestURLValidation:
    """Test URL validation logic."""

    def test_valid_https_url(self):
        """Test that HTTPS URL is accepted."""
        # Should not raise
        client = RegistryClient(url="https://registry.example.com")
        assert client._url == "https://registry.example.com"

    def test_valid_localhost_http_url(self):
        """Test that HTTP localhost URL is accepted (logs warning, doesn't raise)."""
        # Logger warning, not Python warning
        client = RegistryClient(url="http://localhost:8080")
        assert client._url == "http://localhost:8080"

    def test_valid_127_0_0_1_http_url(self):
        """Test that HTTP 127.0.0.1 URL is accepted (logs warning, doesn't raise)."""
        # Logger warning, not Python warning
        client = RegistryClient(url="http://127.0.0.1:8080")
        assert client._url == "http://127.0.0.1:8080"

    def test_http_non_localhost_raises_error(self):
        """Test that HTTP non-localhost URL raises RegistryError."""
        with pytest.raises(RegistryError, match="must use HTTPS"):
            RegistryClient(url="http://registry.example.com")

    def test_invalid_url_without_protocol_raises_error(self):
        """Test that URL without http/https raises RegistryError."""
        with pytest.raises(RegistryError, match="must start with http:// or https://"):
            RegistryClient(url="registry.example.com")

    def test_invalid_url_with_ftp_protocol_raises_error(self):
        """Test that non-HTTP protocol raises RegistryError."""
        with pytest.raises(RegistryError, match="must start with http:// or https://"):
            RegistryClient(url="ftp://registry.example.com")


class TestSanitizeMessage:
    """Test message sanitization for token masking."""

    def test_sanitize_message_with_no_token(self):
        """Test that message is unchanged when no token set."""
        client = RegistryClient(url="https://registry.example.com")

        message = "Error: token=secret123"
        result = client._sanitize_message(message)

        assert result == message

    def test_sanitize_message_masks_token(self):
        """Test that token is masked in error messages."""
        client = RegistryClient(
            url="https://registry.example.com",
            token=SensitiveString("secret123"),
            refresh_interval_secs=0,
        )

        message = "Error: Authorization: Bearer secret123"
        result = client._sanitize_message(message)

        assert "secret123" not in result
        assert "***" in result  # Uses *** pattern

    def test_sanitize_message_with_multiple_token_occurrences(self):
        """Test that all token occurrences are masked."""
        client = RegistryClient(
            url="https://registry.example.com",
            token=SensitiveString("mytoken"),
            refresh_interval_secs=0,
        )

        message = "Token mytoken was used. The mytoken is invalid."
        result = client._sanitize_message(message)

        assert "mytoken" not in result
        assert result.count("***") >= 2  # Uses *** pattern


class TestCacheMethods:
    """Test cache get/set methods."""

    def test_get_cached_returns_none_when_empty(self):
        """Test that _get_cached returns None for cache miss."""
        client = RegistryClient(url="https://registry.example.com")

        result = client._get_cached("nonexistent")

        assert result is None

    def test_set_and_get_cache(self):
        """Test setting and retrieving cache entries."""
        client = RegistryClient(url="https://registry.example.com")

        value = {"test": "data"}
        client._set_cache("test_key", value)

        result = client._get_cached("test_key")

        assert result == value

    def test_get_cached_returns_none_when_expired(self):
        """Test that expired cache entries return None."""
        client = RegistryClient(
            url="https://registry.example.com",
            cache_ttl_secs=0,  # Expire immediately
        )

        value = {"test": "data"}
        client._set_cache("test_key", value)

        # Wait for expiration
        time.sleep(0.1)

        result = client._get_cached("test_key")

        assert result is None

    def test_set_cache_with_custom_ttl(self):
        """Test setting cache with longer TTL."""
        client = RegistryClient(url="https://registry.example.com", cache_ttl_secs=3600)  # 1 hour

        value = {"test": "data"}
        client._set_cache("test_key", value)

        # Should still be cached
        result = client._get_cached("test_key")
        assert result == value

    def test_cache_thread_safety(self):
        """Test that cache operations are thread-safe."""
        client = RegistryClient(url="https://registry.example.com")

        # Access the cache lock
        assert client._cache_lock is not None

        # Set and get should work with lock
        with client._cache_lock:
            client._cache["test"] = CacheEntry(value={"data": "test"}, expires_at=time.time() + 600)

        result = client._get_cached("test")
        assert result == {"data": "test"}


class TestFetchModelConfig:
    """Test fetch_model_config method."""

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_success(self, mock_get):
        """Test successful model config fetch."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "mdl-001",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")
        config = client.fetch_model_config("mdl-001", version="1.0.0")

        assert isinstance(config, ModelConfig)
        assert config.model_id == "mdl-001"
        assert config.model_version == "1.0.0"

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_without_version_requests_latest(self, mock_get):
        """Test that fetch_model_config without version explicitly requests 'latest'."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")
        config = client.fetch_model_config("test-model")  # No version parameter

        # Verify API was called with version=latest parameter
        mock_get.assert_called_once()
        call_args = mock_get.call_args
        assert "version=latest" in call_args[0][0]  # URL should contain version=latest

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_with_version_uses_specified_version(self, mock_get):
        """Test that fetch_model_config with explicit version uses that version."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "2.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")
        config = client.fetch_model_config("test-model", version="2.0.0")

        # Verify API was called with version=2.0.0 parameter
        mock_get.assert_called_once()
        call_args = mock_get.call_args
        assert "version=2.0.0" in call_args[0][0]

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_cache_key_matches_api_request(self, mock_get):
        """Test that cache key semantics match actual API request parameters."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")

        # First call - should hit API
        config1 = client.fetch_model_config("test-model")
        assert mock_get.call_count == 1

        # Second call - should hit cache (not call API again)
        config2 = client.fetch_model_config("test-model")
        assert mock_get.call_count == 1  # Still 1, cached

        # Verify same config returned
        assert config1.model_id == config2.model_id

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_caches_result(self, mock_get):
        """Test that successful fetch caches the result."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "mdl-001",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")

        # First fetch
        config1 = client.fetch_model_config("mdl-001", version="1.0.0")

        # Second fetch should use cache
        config2 = client.fetch_model_config("mdl-001", version="1.0.0")

        # Should only call API once
        assert mock_get.call_count == 1

        # Both should be the same object
        assert config1 == config2

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_404_raises_not_found(self, mock_get):
        """Test that 404 raises RegistryConfigNotFoundError."""
        mock_response = Mock()
        mock_response.status_code = 404
        mock_response.raise_for_status.side_effect = HTTPError()
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")

        with pytest.raises(RegistryConfigNotFoundError):
            client.fetch_model_config("nonexistent")

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_401_raises_auth_error(self, mock_get):
        """Test that 401 raises RegistryAuthError."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.raise_for_status.side_effect = HTTPError()
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")

        with pytest.raises(RegistryAuthError):
            client.fetch_model_config("mdl-001")

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_403_raises_auth_error(self, mock_get):
        """Test that 403 raises RegistryAuthError."""
        mock_response = Mock()
        mock_response.status_code = 403
        mock_response.raise_for_status.side_effect = HTTPError()
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")

        with pytest.raises(RegistryAuthError):
            client.fetch_model_config("mdl-001")

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_connection_error_retries(self, mock_get):
        """Test that connection errors are retried."""
        # First two calls fail, third succeeds
        mock_get.side_effect = [
            ConnectionError("Connection failed"),
            ConnectionError("Connection failed"),
            Mock(
                status_code=200,
                json=lambda: {
                    "service_name": "test-service",
                    "model_id": "mdl-001",
                    "model_version": "1.0.0",
                    "team_name": "test-team",
                    "model_category": "internal",
                    "model_type": "regression",
                },
            ),
        ]

        client = RegistryClient(url="https://registry.example.com")
        config = client.fetch_model_config("mdl-001")

        # Should have retried and succeeded
        assert config.model_id == "mdl-001"
        assert mock_get.call_count == 3

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_max_retries_exceeded(self, mock_get):
        """Test that max retries raises final exception."""
        mock_get.side_effect = ConnectionError("Connection failed")

        client = RegistryClient(url="https://registry.example.com")

        with pytest.raises(RegistryConnectionError):
            client.fetch_model_config("mdl-001")

        # Should have tried max_attempts times (3 by default)
        # Initial attempt catches ConnectionError and wraps in RegistryConnectionError
        # Then retry policy makes 3 more attempts
        assert mock_get.call_count >= 3

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_tracks_model_for_refresh(self, mock_get):
        """Test that fetched models are tracked for background refresh."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "mdl-001",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")
        client.fetch_model_config("mdl-001", version="1.0.0")

        # Model should be tracked
        assert "mdl-001" in client._fetched_models
        assert client._fetched_models["mdl-001"] == "1.0.0"

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_model_config_respects_max_tracked_models(self, mock_get):
        """Test that max_tracked_models limit is enforced."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "mdl-001",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")
        client._max_tracked_models = 2

        # Fetch 3 models
        client.fetch_model_config("mdl-001")
        client.fetch_model_config("mdl-002")
        client.fetch_model_config("mdl-003")

        # Only first 2 should be tracked
        assert len(client._fetched_models) == 2


class TestFetchDeploymentConfig:
    """Test fetch_deployment_config method."""

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_deployment_config_success(self, mock_get):
        """Test successful deployment config fetch."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "deployment_id": "dep-001",
            "environment": "production",
            "region": "us-east-1",
            "resource_overrides": {},
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com")
        config = client.fetch_deployment_config("dep-001")

        assert isinstance(config, DeploymentConfig)
        assert config.deployment_id == "dep-001"
        assert config.region == "us-east-1"


class TestTelemetryIntegration:
    """Test telemetry recording."""

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_success_records_telemetry(self, mock_get):
        """Test that successful fetch records telemetry."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "mdl-001",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        mock_meter = Mock()
        mock_telemetry = Mock()

        with patch("mca_sdk.registry.client.RegistryTelemetry", return_value=mock_telemetry):
            client = RegistryClient(
                url="https://registry.example.com",
                meter=mock_meter,
                refresh_interval_secs=0,
            )
            client.fetch_model_config("mdl-001")

            # Should record success
            mock_telemetry.record_success.assert_called_once()

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_fetch_error_records_telemetry(self, mock_get):
        """Test that fetch errors record telemetry."""
        mock_response = Mock()
        mock_response.status_code = 404
        mock_response.raise_for_status.side_effect = HTTPError()
        mock_get.return_value = mock_response

        mock_meter = Mock()
        mock_telemetry = Mock()

        with patch("mca_sdk.registry.client.RegistryTelemetry", return_value=mock_telemetry):
            client = RegistryClient(
                url="https://registry.example.com",
                meter=mock_meter,
                refresh_interval_secs=0,
            )

            try:
                client.fetch_model_config("mdl-001")
            except RegistryConfigNotFoundError:
                pass

            # Should record error
            mock_telemetry.record_error.assert_called_once()


class TestStopRefreshThread:
    """Test stop_refresh_thread method."""

    @patch("mca_sdk.registry.client.RegistryClient._start_refresh_thread")
    def test_stop_refresh_thread_stops_refresh_thread(self, mock_start):
        """Test that stop_refresh_thread can be called."""
        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=60)

        # Should not raise
        client.stop_refresh_thread()

    def test_stop_refresh_thread_without_refresh_thread(self):
        """Test that stop_refresh_thread works when no refresh thread exists."""
        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=0)

        # Should not raise
        client.stop_refresh_thread()


class TestBackwardCompatibility:
    """Test backward compatibility per-request flag for version parameter handling."""

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_default_sends_latest(self, mock_get):
        """Test that default behavior sends version=latest (modern behavior)."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(
            url="https://registry.example.com",
            refresh_interval_secs=0,
        )
        config = client.fetch_model_config("test-model")

        # Verify version=latest was sent
        mock_get.assert_called_once()
        call_url = mock_get.call_args[0][0]
        assert "version=latest" in call_url

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_omit_default_version_true_omits_parameter(self, mock_get):
        """Test that omit_default_version=True omits version parameter (legacy behavior, per-request)."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(
            url="https://registry.example.com",
            refresh_interval_secs=0,
        )
        # Per-request legacy mode
        config = client.fetch_model_config("test-model", omit_default_version=True)

        # Verify version parameter was NOT sent
        mock_get.assert_called_once()
        call_url = mock_get.call_args[0][0]
        assert "version=" not in call_url

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_omit_default_version_true_with_explicit_version_sends_version(self, mock_get):
        """Test that explicit version is always sent regardless of omit flag."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "2.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(
            url="https://registry.example.com",
            refresh_interval_secs=0,
        )
        config = client.fetch_model_config("test-model", version="2.0.0", omit_default_version=True)

        # Verify explicit version was sent even with omit_default_version=True
        mock_get.assert_called_once()
        call_url = mock_get.call_args[0][0]
        assert "version=2.0.0" in call_url

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_per_request_flag_allows_mixed_behavior(self, mock_get):
        """Test that same client can use different behaviors for different models."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(
            url="https://registry.example.com",
            refresh_interval_secs=0,
        )

        # Fetch model A with modern behavior
        config_a = client.fetch_model_config("model-a")
        assert "version=latest" in mock_get.call_args_list[0][0][0]

        # Fetch model B with legacy behavior (same client instance)
        config_b = client.fetch_model_config("model-b", omit_default_version=True)
        assert "version=" not in mock_get.call_args_list[1][0][0]

        # Verify both calls worked with same client (connection pooling preserved)
        assert mock_get.call_count == 2

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_legacy_mode_logs_warning_per_request(self, mock_get, caplog):
        """Test that using legacy mode per-request logs a warning."""
        import logging

        caplog.set_level(logging.WARNING)

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(
            url="https://registry.example.com",
            refresh_interval_secs=0,
        )
        config = client.fetch_model_config("test-model", omit_default_version=True)

        # Verify warning was logged about legacy mode for this specific model
        warnings = [r for r in caplog.records if r.levelname == "WARNING"]
        assert len(warnings) > 0
        assert any("omitting version parameter" in w.message.lower() for w in warnings)
        assert any("test-model" in w.message for w in warnings)

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_async_warning_debounced(self, mock_get, caplog):
        """Test that async context warning is logged only once (debounced)."""
        import logging
        import asyncio

        caplog.set_level(logging.WARNING)

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(
            url="https://registry.example.com",
            refresh_interval_secs=0,
        )

        # Simulate async context
        async def call_from_async():
            # Clear caplog for clean counting
            caplog.clear()

            # Call multiple times from async context
            client.fetch_model_config("test-model")
            client.fetch_model_config("test-model")
            client.fetch_model_config("test-model")

        # Run in async context
        asyncio.run(call_from_async())

        # Verify warning was logged only ONCE despite 3 calls
        async_warnings = [
            r
            for r in caplog.records
            if r.levelname == "WARNING" and "async context" in r.message.lower()
        ]
        assert len(async_warnings) == 1, f"Expected 1 async warning, got {len(async_warnings)}"
        assert "only be logged once" in async_warnings[0].message


class TestErrorTelemetry:
    """Test ERROR/WARNING/CRITICAL level logging for telemetry."""

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_404_logs_error(self, mock_get, caplog):
        """Test that 404 response logs ERROR level message."""
        import logging

        caplog.set_level(logging.ERROR)

        mock_response = Mock()
        mock_response.status_code = 404
        mock_response.json.return_value = {"error": "Not found"}
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=0)

        with pytest.raises(RegistryConfigNotFoundError):
            client.fetch_model_config("nonexistent-model")

        # Verify ERROR log was emitted
        error_logs = [r for r in caplog.records if r.levelname == "ERROR"]
        assert len(error_logs) > 0
        assert any("non-retryable" in r.message.lower() for r in error_logs)
        assert any("nonexistent-model" in r.message for r in error_logs)

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_401_logs_error(self, mock_get, caplog):
        """Test that 401 response logs ERROR level message."""
        import logging

        caplog.set_level(logging.ERROR)

        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.json.return_value = {"error": "Unauthorized"}
        mock_get.return_value = mock_response

        client = RegistryClient(
            url="https://registry.example.com", token="invalid-token", refresh_interval_secs=0
        )

        with pytest.raises(RegistryAuthError):
            client.fetch_model_config("test-model")

        # Verify ERROR log was emitted
        error_logs = [r for r in caplog.records if r.levelname == "ERROR"]
        assert len(error_logs) > 0
        assert any("non-retryable" in r.message.lower() for r in error_logs)
        assert any("RegistryAuthError" in r.message for r in error_logs)

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_connection_error_logs_warning(self, mock_get, caplog):
        """Test that connection error logs WARNING before retry."""
        import logging

        caplog.set_level(logging.WARNING)

        mock_get.side_effect = ConnectionError("Connection failed")

        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=0)

        with pytest.raises(RegistryConnectionError):
            client.fetch_model_config("test-model")

        # Verify WARNING log was emitted
        warning_logs = [r for r in caplog.records if r.levelname == "WARNING"]
        assert len(warning_logs) > 0
        assert any("retryable" in r.message.lower() for r in warning_logs)
        assert any("test-model" in r.message for r in warning_logs)

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_all_retries_exhausted_logs_critical(self, mock_get, caplog):
        """Test that exhausted retries logs CRITICAL message."""
        import logging

        caplog.set_level(logging.CRITICAL)

        mock_get.side_effect = ConnectionError("Connection failed")

        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=0)

        with pytest.raises(RegistryConnectionError):
            client.fetch_model_config("test-model")

        # Verify CRITICAL log was emitted
        critical_logs = [r for r in caplog.records if r.levelname == "CRITICAL"]
        assert len(critical_logs) > 0
        assert any("CRITICAL" in r.message for r in critical_logs)
        assert any("after all retries" in r.message.lower() for r in critical_logs)
        assert any("fallback config" in r.message.lower() for r in critical_logs)

    def test_invalid_refresh_interval_logs_error(self, caplog):
        """Test that invalid refresh interval logs ERROR before raising."""
        import logging

        caplog.set_level(logging.ERROR)

        with pytest.raises(ValueError):
            RegistryClient(
                url="https://registry.example.com",
                refresh_interval_secs=5,  # Too small (min is 60)
            )

        # Verify ERROR log was emitted
        error_logs = [r for r in caplog.records if r.levelname == "ERROR"]
        assert len(error_logs) > 0
        assert any("configuration error" in r.message.lower() for r in error_logs)
        assert any("refresh_interval_secs" in r.message for r in error_logs)

    def test_both_token_and_gcp_auth_logs_error(self, caplog):
        """Test that conflicting auth params logs ERROR before raising."""
        import logging

        caplog.set_level(logging.ERROR)

        with pytest.raises(RegistryError):
            RegistryClient(
                url="https://registry.example.com", token="test-token", use_gcp_auth=True
            )

        # Verify ERROR log was emitted
        error_logs = [r for r in caplog.records if r.levelname == "ERROR"]
        assert len(error_logs) > 0
        assert any("configuration error" in r.message.lower() for r in error_logs)
        assert any("both token and use_gcp_auth" in r.message.lower() for r in error_logs)

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_successful_fetch_after_retry_logs_info(self, mock_get, caplog):
        """Test that successful fetch after retry logs INFO message."""
        import logging

        caplog.set_level(logging.INFO)

        # First call fails, subsequent calls succeed
        mock_response_success = Mock()
        mock_response_success.status_code = 200
        mock_response_success.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }

        mock_get.side_effect = [
            ConnectionError("First attempt fails"),
            mock_response_success,
        ]

        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=0)
        config = client.fetch_model_config("test-model")

        # Verify INFO log for successful retry
        info_logs = [r for r in caplog.records if r.levelname == "INFO"]
        assert len(info_logs) > 0
        assert any("after retry" in r.message.lower() for r in info_logs)
        assert any("test-model" in r.message for r in info_logs)

    @patch("mca_sdk.registry.client.requests.Session.get")
    def test_successful_fetch_logs_info_with_latency(self, mock_get, caplog):
        """Test that successful fetch logs INFO with latency."""
        import logging

        caplog.set_level(logging.INFO)

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "service_name": "test-service",
            "model_id": "test-model",
            "model_version": "1.0.0",
            "team_name": "test-team",
            "model_category": "internal",
            "model_type": "regression",
        }
        mock_get.return_value = mock_response

        client = RegistryClient(url="https://registry.example.com", refresh_interval_secs=0)
        config = client.fetch_model_config("test-model")

        # Verify INFO log was emitted with latency
        info_logs = [r for r in caplog.records if r.levelname == "INFO"]
        assert len(info_logs) > 0
        assert any("successfully fetched" in r.message.lower() for r in info_logs)
        assert any("latency=" in r.message.lower() for r in info_logs)
        assert any("test-model" in r.message for r in info_logs)
