"""Unit tests for HTTPS enforcement on registry URLs.

Story 3.7: Validate HTTPS requirement for registry URLs with localhost exception.

Acceptance Criteria:
  AC1: HTTP non-localhost → ConfigurationError
  AC2: HTTP localhost → allowed
  AC3: HTTPS all hosts → allowed
"""

import pytest
from mca_sdk.config import MCAConfig
from mca_sdk.utils.exceptions import ConfigurationError


class TestRegistryURLHTTPSEnforcement:
    """Test HTTPS enforcement for registry URLs (Story 3.7)."""

    def test_http_non_localhost_raises_configuration_error(self):
        """Test HTTP registry URL for non-localhost raises ConfigurationError (AC1)."""
        with pytest.raises(ConfigurationError) as exc_info:
            MCAConfig(service_name="test", registry_url="http://registry.example.com/api")

        error_msg = str(exc_info.value)
        assert "https" in error_msg.lower()
        assert "registry_url" in error_msg.lower()
        assert "non-localhost" in error_msg.lower()

    def test_http_localhost_allowed(self):
        """Test HTTP registry URL for localhost is allowed (AC2)."""
        # Should not raise error
        config = MCAConfig(service_name="test", registry_url="http://localhost:5000/api")

        assert config.registry_url == "http://localhost:5000/api"

    def test_http_127_0_0_1_allowed(self):
        """Test HTTP registry URL for 127.0.0.1 is allowed (AC2)."""
        # Should not raise error
        config = MCAConfig(service_name="test", registry_url="http://127.0.0.1:5000/api")

        assert config.registry_url == "http://127.0.0.1:5000/api"

    def test_http_ipv6_localhost_allowed(self):
        """Test HTTP registry URL for IPv6 localhost (::1) is allowed (AC2)."""
        # Should not raise error
        config = MCAConfig(service_name="test", registry_url="http://[::1]:5000/api")

        assert config.registry_url == "http://[::1]:5000/api"

    def test_https_production_url_allowed(self):
        """Test HTTPS registry URL for production hosts is allowed (AC3)."""
        # Should not raise error
        config = MCAConfig(service_name="test", registry_url="https://registry.example.com/api")

        assert config.registry_url == "https://registry.example.com/api"

    def test_https_localhost_allowed(self):
        """Test HTTPS registry URL for localhost is allowed (AC3)."""
        # Should not raise error
        config = MCAConfig(service_name="test", registry_url="https://localhost:5000/api")

        assert config.registry_url == "https://localhost:5000/api"

    def test_http_localhost_substring_attack_blocked(self):
        """Test HTTP URL with localhost substring in domain raises error."""
        # Attack vector: localhost.attacker.com should NOT be treated as localhost
        with pytest.raises(ConfigurationError) as exc_info:
            MCAConfig(service_name="test", registry_url="http://localhost.attacker.com/api")

        error_msg = str(exc_info.value)
        assert "https" in error_msg.lower()
        assert "non-localhost" in error_msg.lower()

    def test_http_my_localhost_attack_blocked(self):
        """Test HTTP URL with my-localhost in domain raises error."""
        with pytest.raises(ConfigurationError) as exc_info:
            MCAConfig(service_name="test", registry_url="http://my-localhost.example.com/api")

        error_msg = str(exc_info.value)
        assert "https" in error_msg.lower()
        assert "non-localhost" in error_msg.lower()

    def test_http_internal_network_ip_blocked(self):
        """Test HTTP URL with internal network IP raises error."""
        with pytest.raises(ConfigurationError) as exc_info:
            MCAConfig(service_name="test", registry_url="http://192.168.1.100:5000/api")

        error_msg = str(exc_info.value)
        assert "https" in error_msg.lower()
        assert "non-localhost" in error_msg.lower()

    def test_https_enforcement_error_message_clarity(self):
        """Test error message provides clear guidance (AC1)."""
        with pytest.raises(ConfigurationError) as exc_info:
            MCAConfig(service_name="test", registry_url="http://prod-registry.company.com/api")

        error_msg = str(exc_info.value)
        # Error should clearly state:
        # 1. What's wrong (HTTP not allowed)
        assert "https" in error_msg.lower()
        # 2. What field has the issue
        assert "registry_url" in error_msg.lower()
        # 3. Why it failed (non-localhost)
        assert "non-localhost" in error_msg.lower()
        # 4. Show the problematic URL
        assert "prod-registry.company.com" in error_msg


class TestRegistryURLValidation:
    """Test registry URL validation beyond HTTPS enforcement."""

    def test_registry_url_none_accepted(self):
        """Test registry_url=None is accepted (optional field)."""
        # Should not raise error
        config = MCAConfig(service_name="test", registry_url=None)
        assert config.registry_url is None

    def test_registry_url_empty_string_accepted(self):
        """Test registry_url empty string is accepted."""
        # Should not raise error - validation only runs if registry_url is truthy
        config = MCAConfig(service_name="test", registry_url="")
        assert config.registry_url == ""

    def test_registry_url_invalid_protocol_rejected(self):
        """Test registry URLs with invalid protocols are rejected."""
        with pytest.raises(ConfigurationError, match="registry_url must use http:// or https://"):
            MCAConfig(service_name="test", registry_url="ftp://registry.example.com/api")

    def test_registry_url_missing_protocol_rejected(self):
        """Test registry URLs without protocol are rejected."""
        with pytest.raises(ConfigurationError):
            MCAConfig(service_name="test", registry_url="registry.example.com/api")


class TestEdgeCases:
    """Test edge cases for HTTPS enforcement."""

    def test_localhost_with_port_allowed(self):
        """Test localhost with various port numbers is allowed."""
        ports = [3000, 5000, 8080, 8443, 9090]

        for port in ports:
            config = MCAConfig(service_name="test", registry_url=f"http://localhost:{port}/api")
            assert config.registry_url == f"http://localhost:{port}/api"

    def test_localhost_without_port_allowed(self):
        """Test localhost without explicit port is allowed."""
        config = MCAConfig(service_name="test", registry_url="http://localhost/api")
        assert config.registry_url == "http://localhost/api"

    def test_https_production_with_path_allowed(self):
        """Test HTTPS URL with complex path is allowed."""
        config = MCAConfig(
            service_name="test",
            registry_url="https://registry.example.com/v1/models/api",
        )
        assert config.registry_url == "https://registry.example.com/v1/models/api"

    def test_https_production_with_query_params_allowed(self):
        """Test HTTPS URL with query parameters is allowed."""
        config = MCAConfig(
            service_name="test",
            registry_url="https://registry.example.com/api?version=v1&format=json",
        )
        assert config.registry_url == "https://registry.example.com/api?version=v1&format=json"


class TestLoopbackValidationEnhancements:
    """Test enhanced loopback validation (Code Review fixes)."""

    def test_full_127_loopback_range_allowed(self):
        """Test full 127.0.0.0/8 loopback range is allowed."""
        # Test various addresses in the 127.0.0.0/8 range
        loopback_addresses = [
            "127.0.0.1",
            "127.0.0.2",
            "127.1.1.1",
            "127.255.255.254",
        ]

        for addr in loopback_addresses:
            config = MCAConfig(service_name="test", registry_url=f"http://{addr}:5000/api")
            assert config.registry_url == f"http://{addr}:5000/api"

    def test_case_insensitive_localhost(self):
        """Test localhost matching is case-insensitive (RFC 4343)."""
        # All these should be accepted (case variations of localhost)
        localhost_variants = [
            "localhost",
            "LOCALHOST",
            "LocalHost",
            "lOcAlHoSt",
        ]

        for variant in localhost_variants:
            config = MCAConfig(service_name="test", registry_url=f"http://{variant}:5000/api")
            assert config.registry_url == f"http://{variant}:5000/api"

    def test_ipv6_with_brackets_allowed(self):
        """Test IPv6 addresses with bracket notation are handled correctly."""
        # IPv6 loopback with brackets (standard URL format)
        config = MCAConfig(service_name="test", registry_url="http://[::1]:5000/api")
        assert config.registry_url == "http://[::1]:5000/api"

    def test_ipv6_substring_attack_blocked(self):
        """Test hostname containing ::1 substring raises error."""
        # Attack vector: domain containing ::1 but not actual IPv6 loopback
        with pytest.raises(ConfigurationError) as exc_info:
            MCAConfig(service_name="test", registry_url="http://malicious::1.attacker.com/api")

        error_msg = str(exc_info.value)
        assert "https" in error_msg.lower()
        assert "non-localhost" in error_msg.lower()

    def test_collector_endpoint_full_127_range_allowed(self):
        """Test collector_endpoint allows full 127.0.0.0/8 range."""
        config = MCAConfig(service_name="test", collector_endpoint="http://127.0.0.99:4318")
        assert config.collector_endpoint == "http://127.0.0.99:4318"

    def test_collector_endpoint_case_insensitive(self):
        """Test collector_endpoint localhost is case-insensitive."""
        config = MCAConfig(service_name="test", collector_endpoint="http://LOCALHOST:4318")
        assert config.collector_endpoint == "http://LOCALHOST:4318"

    def test_non_loopback_127_style_address_blocked(self):
        """Test addresses that look like 127.x but aren't are blocked."""
        # 128.0.0.1 is NOT a loopback address
        with pytest.raises(ConfigurationError) as exc_info:
            MCAConfig(service_name="test", registry_url="http://128.0.0.1:5000/api")

        error_msg = str(exc_info.value)
        assert "https" in error_msg.lower()
        assert "non-localhost" in error_msg.lower()
