"""Comprehensive unit tests for MCAConfig (config/settings.py).

Tests cover initialization, from_env, from_file, from_dict, load precedence,
validation, security constraints, SensitiveString, TokenSanitizingFilter,
endpoint validation, and model taxonomy migration.
Target: >90% coverage for mca_sdk.config.settings module.
"""

import logging
import os
import sys
import warnings
from unittest.mock import Mock, patch, MagicMock

import pytest

from mca_sdk.config.settings import (
    MCAConfig,
    SensitiveString,
    TokenSanitizingFilter,
    SecurityWarning,
    sanitize_token,
    _is_loopback_hostname,
    _validate_endpoint_url,
)
from mca_sdk.utils.exceptions import ConfigurationError
from mca_sdk.security import _SECRET_MANAGER_AVAILABLE


# ---------------------------------------------------------------------------
# sanitize_token Tests
# ---------------------------------------------------------------------------
class TestSanitizeToken:
    def test_sanitize_token_present(self):
        result = sanitize_token("Bearer my-secret-token-xyz", "my-secret-token-xyz")
        assert "my-secret-token-xyz" not in result
        assert "***-xyz" in result

    def test_sanitize_token_short(self):
        """Short tokens (<=4 chars) are masked as '****'; longer use last-4 suffix.

        Implementation uses simple str.replace with no minimum-length guard.
        Tokens of any length are sanitized if present in the message.
        """
        result = sanitize_token("Token: abc", "abc")
        # len("abc") == 3, which is <= 4, so masked as "****"
        assert result == "Token: ****"
        assert "abc" not in result

    def test_sanitize_token_substring_replaced(self):
        """Implementation uses str.replace — tokens appearing as substrings are sanitized."""
        # Token is standalone word - should be sanitized
        result = sanitize_token("Using token mysecret123 for auth", "mysecret123")
        assert "mysecret123" not in result
        assert "***t123" in result

        # Token as substring — simple str.replace matches substrings (by design)
        result2 = sanitize_token("authentication failed", "authentic")
        # "authentic" (9 chars, last-4 = "ntic") → "***ntic" replaces everywhere it appears
        assert "authentic" not in result2
        assert "***ntic" in result2

    def test_sanitize_token_none(self):
        assert sanitize_token("message", None) == "message"

    def test_sanitize_token_not_present(self):
        assert sanitize_token("message", "other") == "message"

    def test_sanitize_token_empty_string(self):
        assert sanitize_token("message", "") == "message"


# ---------------------------------------------------------------------------
# TokenSanitizingFilter Tests
# ---------------------------------------------------------------------------
class TestTokenSanitizingFilter:
    def test_filter_sanitizes_msg(self):
        f = TokenSanitizingFilter()
        f.set_token("my-long-secret-token")
        record = logging.LogRecord(
            "test", logging.INFO, "", 0, "Token: my-long-secret-token", None, None
        )
        f.filter(record)
        assert "my-long-secret-token" not in record.msg

    def test_filter_sanitizes_tuple_args(self):
        f = TokenSanitizingFilter()
        f.set_token("secret-token-1234")
        record = logging.LogRecord(
            "test", logging.INFO, "", 0, "Token: %s", ("secret-token-1234",), None
        )
        f.filter(record)
        assert "secret-token-1234" not in str(record.args)

    def test_filter_sanitizes_dict_args(self):
        f = TokenSanitizingFilter()
        f.set_token("dict-secret-token")
        record = logging.LogRecord("test", logging.INFO, "", 0, "%(token)s", None, None)
        record.args = {"token": "dict-secret-token"}
        f.filter(record)
        assert "dict-secret-token" not in str(record.args)

    def test_filter_returns_true(self):
        f = TokenSanitizingFilter()
        record = logging.LogRecord("test", logging.INFO, "", 0, "msg", None, None)
        assert f.filter(record) is True

    def test_set_token_none(self):
        f = TokenSanitizingFilter()
        f.set_token(None)
        assert len(f._tokens) == 0

    def test_set_token_duplicate(self):
        f = TokenSanitizingFilter()
        f.set_token("token1")
        f.set_token("token1")
        assert len(f._tokens) == 1

    def test_filter_non_string_msg(self):
        f = TokenSanitizingFilter()
        f.set_token("token")
        record = logging.LogRecord("test", logging.INFO, "", 0, 42, None, None)
        f.filter(record)  # Should not raise

    def test_filter_non_string_args(self):
        f = TokenSanitizingFilter()
        f.set_token("token")
        record = logging.LogRecord("test", logging.INFO, "", 0, "msg %s", (42,), None)
        f.filter(record)  # Should not raise


# ---------------------------------------------------------------------------
# SensitiveString Tests
# ---------------------------------------------------------------------------
class TestSensitiveString:
    def test_str_masks_value(self):
        s = SensitiveString("my-secret-value")
        assert "my-secret-value" not in str(s)
        assert "***alue" in str(s)

    def test_str_short_value(self):
        s = SensitiveString("abc")
        assert str(s) == "****"

    def test_str_empty_value(self):
        s = SensitiveString("")
        assert str(s) == "<empty>"

    def test_repr_masks_value(self):
        s = SensitiveString("long-secret-value")
        assert "SensitiveString(***alue)" == repr(s)

    def test_repr_short_value(self):
        s = SensitiveString("ab")
        assert repr(s) == "SensitiveString(****)"

    def test_repr_empty_value(self):
        s = SensitiveString("")
        assert repr(s) == "SensitiveString(<empty>)"

    def test_equality_raises(self):
        s = SensitiveString("test")
        with pytest.raises(TypeError, match="Comparison.*disabled"):
            s == "test"

    def test_hash_disabled(self):
        s = SensitiveString("test")
        with pytest.raises(TypeError):
            hash(s)

    def test_unsafe_process_value(self):
        s = SensitiveString("my-secret")
        result = s._SensitiveString__unsafe_process_value(lambda v: f"Bearer {v}")
        assert result == "Bearer my-secret"

    def test_unsafe_process_value_none_init(self):
        s = SensitiveString(None)
        result = s._SensitiveString__unsafe_process_value(lambda v: v)
        assert result == ""


# ---------------------------------------------------------------------------
# _is_loopback_hostname Tests
# ---------------------------------------------------------------------------
class TestIsLoopbackHostname:
    def test_localhost(self):
        assert _is_loopback_hostname("localhost") is True

    def test_localhost_uppercase(self):
        assert _is_loopback_hostname("LOCALHOST") is True

    def test_ipv4_loopback(self):
        assert _is_loopback_hostname("127.0.0.1") is True

    def test_ipv6_loopback(self):
        assert _is_loopback_hostname("::1") is True

    def test_ipv6_bracket_loopback(self):
        assert _is_loopback_hostname("[::1]") is True

    def test_non_loopback(self):
        assert _is_loopback_hostname("example.com") is False

    def test_non_loopback_ip(self):
        assert _is_loopback_hostname("10.0.0.1") is False

    def test_empty_string(self):
        assert _is_loopback_hostname("") is False


# ---------------------------------------------------------------------------
# _validate_endpoint_url Tests
# ---------------------------------------------------------------------------
class TestValidateEndpointUrl:
    def test_valid_https(self):
        _validate_endpoint_url("https://collector.example.com", "test_endpoint")

    def test_valid_http_localhost(self):
        _validate_endpoint_url("http://localhost:4318", "test_endpoint")

    def test_http_non_localhost_raises(self):
        with pytest.raises(ConfigurationError, match="must use HTTPS"):
            _validate_endpoint_url("http://remote.example.com", "test_endpoint")

    def test_no_protocol_raises(self):
        with pytest.raises(ConfigurationError, match="must start with http"):
            _validate_endpoint_url("collector.example.com", "test_endpoint")

    def test_no_protocol_with_format_check_no_dots(self):
        with pytest.raises(ConfigurationError, match="Invalid.*format"):
            _validate_endpoint_url("justahostname", "registry_url", require_format_check=True)

    def test_no_protocol_with_format_check_with_dots(self):
        with pytest.raises(ConfigurationError, match="must use http"):
            _validate_endpoint_url(
                "registry.example.com:8080", "registry_url", require_format_check=True
            )

    def test_http_non_localhost_allow_insecure(self):
        # Should not raise, just warn
        _validate_endpoint_url("http://remote.example.com", "test_endpoint", allow_insecure=True)


# ---------------------------------------------------------------------------
# MCAConfig Initialization Tests
# ---------------------------------------------------------------------------
class TestMCAConfigInit:
    def test_minimal_config(self):
        config = MCAConfig(service_name="test")
        assert config.service_name == "test"
        assert config.model_type == "regression"
        assert config.model_category == "internal"

    def test_empty_service_name_raises(self):
        with pytest.raises(ConfigurationError, match="service_name is required"):
            MCAConfig(service_name="")

    def test_invalid_model_category_raises(self):
        with pytest.raises(ConfigurationError, match="model_category must be one of"):
            MCAConfig(service_name="test", model_category="invalid")

    def test_invalid_model_type_raises(self):
        with pytest.raises(ConfigurationError, match="model_type must be one of"):
            MCAConfig(service_name="test", model_type="invalid")

    def test_legacy_model_type_internal_migrates(self):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            config = MCAConfig(service_name="test", model_type="internal")
        assert config.model_category == "internal"
        assert config.model_type in [
            "regression",
            "time-series",
            "classification",
            "generative",
            "agentic",
        ]

    def test_legacy_model_type_vendor_migrates(self):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            config = MCAConfig(service_name="test", model_type="vendor")
        assert config.model_category == "vendor"

    def test_generative_with_internal_logs_info(self, caplog):
        with caplog.at_level(logging.INFO):
            MCAConfig(service_name="test", model_type="generative", model_category="internal")
        # Should log info about being explicit with both fields
        assert any("generative" in r.message for r in caplog.records)


# ---------------------------------------------------------------------------
# Resilience Config Validation Tests
# ---------------------------------------------------------------------------
class TestResilienceValidation:
    def test_negative_base_delay_raises(self):
        with pytest.raises(ConfigurationError, match="base_delay must be positive"):
            MCAConfig(service_name="test", base_delay=-1.0)

    def test_max_delay_less_than_base_raises(self):
        with pytest.raises(ConfigurationError, match="max_delay.*must be >= base_delay"):
            MCAConfig(service_name="test", base_delay=5.0, max_delay=2.0)

    def test_backoff_multiplier_lte_1_raises(self):
        with pytest.raises(ConfigurationError, match="backoff_multiplier must be > 1.0"):
            MCAConfig(service_name="test", backoff_multiplier=0.5)

    def test_circuit_breaker_fail_max_zero_raises(self):
        with pytest.raises(ConfigurationError, match="circuit_breaker_fail_max must be positive"):
            MCAConfig(service_name="test", circuit_breaker_fail_max=0)

    def test_circuit_breaker_timeout_zero_raises(self):
        with pytest.raises(ConfigurationError, match="circuit_breaker_timeout must be positive"):
            MCAConfig(service_name="test", circuit_breaker_timeout=0)


# ---------------------------------------------------------------------------
# Persist Queue HIPAA Warning Tests
# ---------------------------------------------------------------------------
class TestPersistQueueWarning:
    def test_persist_queue_security_warning(self):
        with pytest.warns(SecurityWarning):
            MCAConfig(service_name="test", persist_queue=True)


# ---------------------------------------------------------------------------
# Endpoint Validation in __post_init__ Tests
# ---------------------------------------------------------------------------
class TestEndpointValidationInInit:
    def test_invalid_collector_endpoint_raises(self):
        with pytest.raises(ConfigurationError):
            MCAConfig(service_name="test", collector_endpoint="ftp://invalid")

    def test_invalid_registry_url_raises(self):
        with pytest.raises(ConfigurationError):
            MCAConfig(service_name="test", registry_url="invalid")

    def test_valid_registry_url(self):
        config = MCAConfig(service_name="test", registry_url="https://registry.example.com")
        assert config.registry_url == "https://registry.example.com"


# ---------------------------------------------------------------------------
# __setattr__ Auto-wrapping Tests
# ---------------------------------------------------------------------------
class TestSetattr:
    def test_registry_token_auto_wrapped(self):
        config = MCAConfig(service_name="test")
        config.registry_token = "plain-token"
        assert isinstance(config.registry_token, SensitiveString)

    def test_registry_token_none_not_wrapped(self):
        config = MCAConfig(service_name="test")
        config.registry_token = None
        assert config.registry_token is None

    def test_registry_token_already_sensitive(self):
        config = MCAConfig(service_name="test")
        token = SensitiveString("token")
        config.registry_token = token
        assert config.registry_token is token


# ---------------------------------------------------------------------------
# __repr__ and __str__ Tests
# ---------------------------------------------------------------------------
class TestReprStr:
    def test_repr_includes_all_fields(self):
        config = MCAConfig(service_name="test")
        r = repr(config)
        assert "service_name=" in r
        assert "MCAConfig(" in r

    def test_str_delegates_to_repr(self):
        config = MCAConfig(service_name="test")
        assert str(config) == repr(config)

    def test_repr_masks_token(self):
        config = MCAConfig(service_name="test")
        config.registry_token = "super-secret-token"
        r = repr(config)
        assert "super-secret-token" not in r

    def test_repr_non_sensitive_token_fallback(self):
        config = MCAConfig(service_name="test")
        # Force a non-SensitiveString token (bypassing __setattr__)
        object.__setattr__(config, "registry_token", "plain-token-value")
        r = repr(config)
        assert "plain-token-value" not in r


# ---------------------------------------------------------------------------
# from_env Tests
# ---------------------------------------------------------------------------
class TestFromEnv:
    def test_from_env_basic(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "env-service")
        config = MCAConfig.from_env()
        assert config.service_name == "env-service"

    def test_from_env_bool_parsing(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "test")
        monkeypatch.setenv("MCA_BUFFERING_ENABLED", "true")
        monkeypatch.setenv("MCA_DEBUG", "1")
        config = MCAConfig.from_env()
        assert config.buffering_enabled is True
        assert config.debug_mode is True

    def test_from_env_int_parsing(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "test")
        monkeypatch.setenv("MCA_MAX_QUEUE_SIZE", "2000")
        config = MCAConfig.from_env()
        assert config.max_queue_size == 2000

    def test_from_env_float_parsing(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "test")
        monkeypatch.setenv("MCA_BASE_DELAY", "2.5")
        config = MCAConfig.from_env()
        assert config.base_delay == 2.5

    def test_from_env_invalid_int_raises(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "test")
        monkeypatch.setenv("MCA_MAX_QUEUE_SIZE", "not_a_number")
        with pytest.raises(ConfigurationError, match="Invalid integer"):
            MCAConfig.from_env()

    def test_from_env_invalid_float_raises(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "test")
        monkeypatch.setenv("MCA_BASE_DELAY", "not_a_float")
        with pytest.raises(ConfigurationError, match="Invalid float"):
            MCAConfig.from_env()


# ---------------------------------------------------------------------------
# from_file Tests
# ---------------------------------------------------------------------------
class TestFromFile:
    def test_from_file_valid_yaml(self, tmp_path):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("service_name: file-service\nmodel_id: mdl-001\n")
        config = MCAConfig.from_file(str(config_file))
        assert config.service_name == "file-service"
        assert config.model_id == "mdl-001"

    def test_from_file_not_found(self, tmp_path):
        with pytest.raises(ConfigurationError, match="not found"):
            MCAConfig.from_file(str(tmp_path / "nonexistent.yaml"))

    def test_from_file_invalid_yaml(self, tmp_path):
        config_file = tmp_path / "config.yaml"
        config_file.write_text(":\n  - invalid: [yaml: {{{")
        with pytest.raises(ConfigurationError, match="Invalid YAML"):
            MCAConfig.from_file(str(config_file))

    def test_from_file_not_dict(self, tmp_path):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("- item1\n- item2\n")
        with pytest.raises(ConfigurationError, match="must contain a YAML dictionary"):
            MCAConfig.from_file(str(config_file))

    def test_from_file_unknown_key_raises(self, tmp_path):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("service_name: test\nunknown_field: value\n")
        with pytest.raises(ConfigurationError, match="Unknown configuration key"):
            MCAConfig.from_file(str(config_file))

    def test_from_file_secret_key_raises(self, tmp_path):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("service_name: test\nregistry_token: secret123\n")
        with pytest.raises(ConfigurationError, match="Security violation"):
            MCAConfig.from_file(str(config_file))

    def test_from_file_no_pyyaml(self, tmp_path):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("service_name: test\n")
        with patch.dict("sys.modules", {"yaml": None}):
            with pytest.raises(ConfigurationError, match="pyyaml is required"):
                MCAConfig.from_file(str(config_file))


# ---------------------------------------------------------------------------
# from_dict Tests
# ---------------------------------------------------------------------------
class TestFromDict:
    def test_from_dict_basic(self):
        config = MCAConfig.from_dict({"service_name": "dict-service"})
        assert config.service_name == "dict-service"

    def test_from_dict_filters_unknown_keys(self):
        config = MCAConfig.from_dict({"service_name": "test", "unknown_key": "ignored"})
        assert config.service_name == "test"
        assert not hasattr(config, "unknown_key")

    def test_from_dict_does_not_mutate_input(self):
        """Test MCAConfig does not mutate input dictionary (Python anti-pattern prevention).

        Verifies AC2 requirement for config immutability. Modifying user-provided kwargs
        in-place is a classic Python anti-pattern that can cause subtle bugs.
        """
        import copy

        # Create input dict with nested structure
        input_dict = {
            "service_name": "test-service",
            "model_id": "mdl-001",
            "team_name": "test-team",
            "debug_mode": False,
        }

        # Deep copy to compare later
        original_dict = copy.deepcopy(input_dict)

        # Create config from dict
        config = MCAConfig.from_dict(input_dict)

        # Verify input dict was not modified
        assert (
            input_dict == original_dict
        ), "MCAConfig.from_dict() mutated the input dictionary (anti-pattern detected)"

        # Verify no new keys added to input
        assert set(input_dict.keys()) == set(
            original_dict.keys()
        ), "MCAConfig.from_dict() added keys to input dictionary"

        # Verify values unchanged
        for key in original_dict:
            assert (
                input_dict[key] == original_dict[key]
            ), f"MCAConfig.from_dict() modified value for key '{key}'"

    def test_kwargs_initialization_does_not_mutate_input(self):
        """Test MCAClient initialization does not mutate kwargs dictionary."""
        input_kwargs = {
            "service_name": "test",
            "model_id": "mdl-001",
            "team_name": "team",
        }

        original_keys = set(input_kwargs.keys())
        original_values = {k: input_kwargs[k] for k in input_kwargs}

        # This would be called like: MCAConfig(**input_kwargs)
        config = MCAConfig(**input_kwargs)

        # Verify kwargs dict unchanged
        assert set(input_kwargs.keys()) == original_keys
        for key in original_values:
            assert input_kwargs[key] == original_values[key]

    def test_deeply_nested_config_immutability(self):
        """Test DEEP immutability of nested data structures in config.

        AC Round 5 requirement: Shallow immutability checks miss nested mutations.
        Test that modifying nested lists/dicts within config payload does NOT
        corrupt the MCAConfig instance.
        """
        import copy

        # Create deeply nested input structure (simulates complex routing rules)
        input_dict = {
            "service_name": "test-service",
            "model_id": "mdl-001",
            "team_name": "test-team",
            # Simulate nested routing configuration
            "resource_attributes": {
                "model.type": "generative",
                "model.category": "internal",
                "routing_rules": [
                    {"region": "us-east-1", "endpoints": ["http://collector1:4318"]},
                    {"region": "us-west-2", "endpoints": ["http://collector2:4318"]},
                ],
            },
        }

        # Deep copy before passing to config
        original_dict = copy.deepcopy(input_dict)

        # Create config
        config = MCAConfig.from_dict(input_dict)

        # NOW: Attempt to mutate the nested structures in input_dict
        # (This simulates a bug where user reuses and modifies the config dict)
        input_dict["resource_attributes"]["routing_rules"][0]["region"] = "eu-central-1"
        input_dict["resource_attributes"]["routing_rules"].append(
            {"region": "ap-south-1", "endpoints": ["http://collector3:4318"]}
        )
        input_dict["resource_attributes"]["model.type"] = "MUTATED"

        # Verify original input dict is now corrupted (expected)
        assert input_dict != original_dict, "Input dict should be mutated by user"

        # CRITICAL: Verify MCAConfig instance is NOT affected by mutations
        # The config should have captured immutable copies of the data
        assert (
            config.resource_attributes.get("model.type") == "generative"
        ), "Config model.type should not be affected by input dict mutation"

        # Verify routing_rules list in config is not affected
        original_routing = original_dict["resource_attributes"]["routing_rules"]
        if "routing_rules" in config.resource_attributes:
            config_routing = config.resource_attributes["routing_rules"]
            # Config should match ORIGINAL, not mutated input
            assert len(config_routing) == len(
                original_routing
            ), "Config routing rules should not reflect mutations to input dict"
            assert (
                config_routing[0]["region"] == "us-east-1"
            ), "Config should preserve original region, not mutated value"

    def test_nested_list_mutation_does_not_corrupt_config(self):
        """Test that mutating nested lists in input dict does not corrupt config.

        AC Round 5: Focuses specifically on list mutations within dicts.
        """
        import copy

        input_dict = {
            "service_name": "test",
            "model_id": "mdl-001",
            "team_name": "team",
            "resource_attributes": {"allowed_models": ["model-1", "model-2", "model-3"]},
        }

        original_list = copy.deepcopy(input_dict["resource_attributes"]["allowed_models"])

        config = MCAConfig.from_dict(input_dict)

        # Mutate the list in the input dict
        input_dict["resource_attributes"]["allowed_models"].append("model-4")
        input_dict["resource_attributes"]["allowed_models"][0] = "MUTATED"

        # Verify config is not affected
        if "allowed_models" in config.resource_attributes:
            config_list = config.resource_attributes["allowed_models"]
            assert len(config_list) == len(original_list), "Config list length should not change"
            assert config_list[0] == "model-1", "Config list should preserve original values"
            assert "model-4" not in config_list, "Config should not see appended values"


# ---------------------------------------------------------------------------
# _get_env_value / _get_env_bool / _get_env_int / _get_env_float Tests
# ---------------------------------------------------------------------------
class TestEnvHelpers:
    def test_get_env_value(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "test")
        assert MCAConfig._get_env_value("service_name", "MCA_") == "test"

    def test_get_env_value_missing(self):
        assert MCAConfig._get_env_value("NONEXISTENT_KEY_12345", "MCA_") is None

    def test_get_env_bool_true(self, monkeypatch):
        monkeypatch.setenv("MCA_DEBUG_MODE", "true")
        assert MCAConfig._get_env_bool("debug_mode", "MCA_") is True

    def test_get_env_bool_false(self, monkeypatch):
        monkeypatch.setenv("MCA_DEBUG_MODE", "false")
        assert MCAConfig._get_env_bool("debug_mode", "MCA_") is False

    def test_get_env_bool_missing(self):
        assert MCAConfig._get_env_bool("NONEXISTENT_12345", "MCA_") is None

    def test_get_env_int(self, monkeypatch):
        monkeypatch.setenv("MCA_MAX_QUEUE_SIZE", "500")
        assert MCAConfig._get_env_int("max_queue_size", "MCA_") == 500

    def test_get_env_int_invalid(self, monkeypatch):
        monkeypatch.setenv("MCA_MAX_QUEUE_SIZE", "abc")
        with pytest.raises(ConfigurationError, match="Invalid integer"):
            MCAConfig._get_env_int("max_queue_size", "MCA_")

    def test_get_env_int_missing(self):
        assert MCAConfig._get_env_int("NONEXISTENT_12345", "MCA_") is None

    def test_get_env_float(self, monkeypatch):
        monkeypatch.setenv("MCA_BASE_DELAY", "1.5")
        assert MCAConfig._get_env_float("base_delay", "MCA_") == 1.5

    def test_get_env_float_invalid(self, monkeypatch):
        monkeypatch.setenv("MCA_BASE_DELAY", "abc")
        with pytest.raises(ConfigurationError, match="Invalid float"):
            MCAConfig._get_env_float("base_delay", "MCA_")

    def test_get_env_float_missing(self):
        assert MCAConfig._get_env_float("NONEXISTENT_12345", "MCA_") is None


# ---------------------------------------------------------------------------
# _load_from_yaml Tests
# ---------------------------------------------------------------------------
class TestLoadFromYaml:
    def test_load_from_yaml_file(self, tmp_path):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("service_name: yaml-svc\n")
        result = MCAConfig._load_from_yaml(str(config_file), "MCA_")
        assert result["service_name"] == "yaml-svc"

    def test_load_from_yaml_env_fallback(self, tmp_path, monkeypatch):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("service_name: yaml-svc\n")
        monkeypatch.setenv("MCA_CONFIG_FILE", str(config_file))
        result = MCAConfig._load_from_yaml(None, "MCA_")
        assert result["service_name"] == "yaml-svc"

    def test_load_from_yaml_no_file(self):
        result = MCAConfig._load_from_yaml(None, "MCA_")
        assert result == {}

    def test_load_from_yaml_nonexistent_file(self):
        result = MCAConfig._load_from_yaml("nonexistent_path.yaml", "MCA_")
        assert result == {}


# ---------------------------------------------------------------------------
# _load_from_env Tests
# ---------------------------------------------------------------------------
class TestLoadFromEnvMethod:
    def test_load_from_env_string_fields(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "env-svc")
        monkeypatch.setenv("MCA_MODEL_ID", "mdl-1")
        result = MCAConfig._load_from_env("MCA_")
        assert result["service_name"] == "env-svc"
        assert result["model_id"] == "mdl-1"

    def test_load_from_env_bool_fields(self, monkeypatch):
        monkeypatch.setenv("MCA_BUFFERING_ENABLED", "true")
        result = MCAConfig._load_from_env("MCA_")
        assert result["buffering_enabled"] is True

    def test_load_from_env_int_fields(self, monkeypatch):
        monkeypatch.setenv("MCA_COLLECTOR_TIMEOUT", "30")
        result = MCAConfig._load_from_env("MCA_")
        assert result["collector_timeout"] == 30

    def test_load_from_env_float_fields(self, monkeypatch):
        monkeypatch.setenv("MCA_BASE_DELAY", "2.5")
        result = MCAConfig._load_from_env("MCA_")
        assert result["base_delay"] == 2.5


# ---------------------------------------------------------------------------
# _validate_security_constraints Tests
# ---------------------------------------------------------------------------
class TestValidateSecurityConstraints:
    def test_registry_token_in_kwargs_raises(self):
        with pytest.raises(ConfigurationError, match="Security violation"):
            MCAConfig._validate_security_constraints({"registry_token": "token123"})

    def test_no_registry_token_passes(self):
        MCAConfig._validate_security_constraints({"service_name": "test"})


# ---------------------------------------------------------------------------
# _resolve_secrets Tests
# ---------------------------------------------------------------------------
# Secret Resolution Tests (Story 3-9 integration)
# NOTE: These tests are verified to align with Story 3-9's SecretManagerClient
# implementation. Tests will activate when Story 3-9 is fully deployed.
# ---------------------------------------------------------------------------
class TestResolveSecrets:
    def test_no_secret_urls(self):
        config_dict = {"service_name": "test", "model_id": "mdl-1"}
        result = MCAConfig._resolve_secrets(config_dict, "MCA_")
        assert result == config_dict

    @pytest.mark.skipif(
        not _SECRET_MANAGER_AVAILABLE,
        reason="Secret manager module not yet implemented (Story 3-9)",
    )
    @patch("mca_sdk.config.settings.logger")
    def test_secret_url_resolved(self, mock_logger):
        with patch("mca_sdk.security.secret_manager.SecretManagerClient") as MockClient:
            mock_instance = Mock()
            mock_instance.fetch_secret.return_value = "resolved-value"
            MockClient.get_instance.return_value = mock_instance

            config_dict = {"registry_token": "secret://proj/token/latest"}
            result = MCAConfig._resolve_secrets(config_dict, "MCA_")
            assert result["registry_token"] == "resolved-value"

    @pytest.mark.skipif(
        not _SECRET_MANAGER_AVAILABLE,
        reason="Secret manager module not yet implemented (Story 3-9)",
    )
    def test_secret_url_fetch_failure(self):
        with patch("mca_sdk.security.secret_manager.SecretManagerClient") as MockClient:
            from mca_sdk.security.secret_manager import SecretManagerError

            mock_instance = Mock()
            mock_instance.fetch_secret.side_effect = SecretManagerError("fetch failed")
            MockClient.get_instance.return_value = mock_instance

            config_dict = {"registry_token": "secret://proj/token/latest"}
            with pytest.raises(ConfigurationError, match="Failed to resolve secret"):
                MCAConfig._resolve_secrets(config_dict, "MCA_")


# ---------------------------------------------------------------------------
# MCAConfig.load() Precedence Tests
# ---------------------------------------------------------------------------
class TestConfigLoad:
    def test_load_kwargs_override_env(self, monkeypatch):
        monkeypatch.setenv("MCA_SERVICE_NAME", "env-svc")
        config = MCAConfig.load(service_name="kwarg-svc")
        assert config.service_name == "kwarg-svc"

    def test_load_env_override_yaml(self, tmp_path, monkeypatch):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("service_name: yaml-svc\nmodel_id: yaml-mdl\n")
        monkeypatch.setenv("MCA_SERVICE_NAME", "env-svc")
        config = MCAConfig.load(config_file=str(config_file))
        assert config.service_name == "env-svc"

    def test_load_defaults_when_nothing_set(self, monkeypatch):
        # Ensure no MCA_ env vars are set
        for key in list(os.environ.keys()):
            if key.startswith("MCA_"):
                monkeypatch.delenv(key, raising=False)
        # Story 8.4: Use staging to test defaults without collector_endpoint requirement
        monkeypatch.setenv("MCA_ENV", "staging")
        config = MCAConfig.load(service_name="test")
        assert config.max_queue_size == 1000
        assert config.retry_attempts == 3

    def test_load_registry_token_in_kwargs_raises(self):
        with pytest.raises(ConfigurationError, match="Security violation"):
            MCAConfig.load(service_name="test", registry_token="bad")

    def test_load_strict_metric_validation_defaults_to_strict(self, monkeypatch):
        for key in list(os.environ.keys()):
            if key.startswith("MCA_"):
                monkeypatch.delenv(key, raising=False)
        # Story 8.4: Use staging to test defaults without collector_endpoint requirement
        monkeypatch.setenv("MCA_ENV", "staging")
        config = MCAConfig.load(service_name="test", strict_validation=False)
        assert config.strict_validation is False


# ---------------------------------------------------------------------------
# _apply_token_sanitizing_filter Tests
# ---------------------------------------------------------------------------
class TestApplyTokenFilter:
    def test_filter_applied_on_init(self):
        config = MCAConfig(service_name="test")
        config.registry_token = "my-secret-token"
        sdk_logger = logging.getLogger("mca_sdk")
        has_filter = any(isinstance(f, TokenSanitizingFilter) for f in sdk_logger.filters)
        assert has_filter

    def test_filter_updated_on_reassignment(self):
        config = MCAConfig(service_name="test")
        config.registry_token = "token-1"
        config.registry_token = "token-2"
        sdk_logger = logging.getLogger("mca_sdk")
        filter_count = sum(1 for f in sdk_logger.filters if isinstance(f, TokenSanitizingFilter))
        assert filter_count == 1  # Should not duplicate

    def test_no_filter_for_non_sensitive_token(self):
        config = MCAConfig(service_name="test")
        # No token set
        config._apply_token_sanitizing_filter()  # Should be no-op


# ---------------------------------------------------------------------------
# Windows Path Normalization Regression Tests
# ---------------------------------------------------------------------------
class TestWindowsPathNormalization:
    """Regression tests for Windows path separator mismatch in _validate_file_paths.

    On Windows, os.path.expanduser returns forward slashes (C:/Users/User/.mca_sdk)
    while os.path.realpath returns backslashes (C:\\Users\\User\\.mca_sdk\\file.pkl).
    The fix applies os.path.normcase(os.path.normpath()) to both sides before comparison.
    """

    @pytest.mark.skipif(
        sys.platform != "win32",
        reason="Windows path normalization test only relevant on Windows",
    )
    def test_mixed_separators_are_accepted(self, tmp_path):
        """Paths with mixed separators should not raise ConfigurationError."""
        queue_dir = tmp_path / "mca_sdk"
        queue_dir.mkdir()
        queue_file = queue_dir / "queue.pkl"

        # Simulate the mismatch: allowed prefix uses forward slash, canonical uses backslash
        forward_slash_prefix = str(tmp_path).replace("\\", "/")
        backslash_canonical = str(queue_file).replace("/", "\\")

        import os

        # Both normalize to the same path regardless of separator style
        assert os.path.normcase(os.path.normpath(backslash_canonical)).startswith(
            os.path.normcase(os.path.normpath(forward_slash_prefix))
        ), "Normalized paths should match despite separator differences"

    def test_persist_path_with_default_dir_uses_normalized_comparison(self):
        """persist_path under the default ~/.mca_sdk dir should not raise on Windows.

        Regression test: on Windows, os.path.expanduser returns forward slashes but
        os.path.realpath returns backslashes. The fix normalizes both with normcase/normpath.
        """
        import os

        home = os.path.expanduser("~")
        allowed_dir = os.path.join(home, ".mca_sdk")
        os.makedirs(allowed_dir, exist_ok=True)
        queue_pkl = os.path.join(allowed_dir, "queue.pkl")

        config = MCAConfig(service_name="test", persist_queue=True, persist_path=queue_pkl)
        # Should not raise — the allowed dir check must handle mixed separators
        assert config.persist_path is not None


# ---------------------------------------------------------------------------
# Edge Cases and Missing Coverage Tests
# ---------------------------------------------------------------------------
class TestConfigEdgeCases:
    """Tests for edge cases and error conditions to reach 85% coverage."""

    def test_registry_token_auto_wrapped_in_sensitive_string(self):
        """Test that plain string registry_token is auto-wrapped in SensitiveString."""
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            config = MCAConfig(
                service_name="test",
                registry_url="https://registry.example.com",
            )
            # Assign plain string token
            config.registry_token = "plain-token-string"

            # Should auto-wrap in SensitiveString
            assert isinstance(config.registry_token, SensitiveString)

    def test_very_small_telemetry_max_attribute_length_warning(self):
        """Test warning for very small telemetry_max_attribute_length."""
        with patch("mca_sdk.config.settings.logger") as mock_logger:
            config = MCAConfig(
                service_name="test",
                telemetry_max_attribute_length=5,  # Very small
            )

            # Should log warning
            mock_logger.warning.assert_called()
            warning_call = str(mock_logger.warning.call_args)
            assert "very small" in warning_call.lower()

    def test_invalid_collector_protocol_raises_error(self):
        """Test that invalid collector_protocol raises ConfigurationError."""
        with pytest.raises(ConfigurationError, match="collector_protocol must be"):
            config = MCAConfig(
                service_name="test",
                collector_protocol="invalid",  # Not 'http' or 'grpc'
            )
