"""Unit tests for mTLS configuration.

Tests configuration loading, certificate validation, and error handling
for mTLS support in OTLP communication.
"""

import os
import sys
import tempfile
import pytest
from pathlib import Path
from unittest.mock import patch, Mock
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime, timedelta

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


class TestMTLSConfiguration:
    """Tests for mTLS configuration loading and validation."""

    def test_mtls_config_parameters_from_env(self):
        """Test loading mTLS config from environment variables."""
        with patch.dict(
            os.environ,
            {
                "MCA_SERVICE_NAME": "test-service",
                "MCA_MTLS_ENABLED": "true",
                "MCA_CLIENT_CERT_PATH": "/path/to/client.crt",
                "MCA_CLIENT_KEY_PATH": "/path/to/client.key",
                "MCA_CA_CERT_PATH": "/path/to/ca.crt",
            },
        ):
            config = MCAConfig.from_env()

            assert config.mtls_enabled is True
            assert config.client_cert_path == "/path/to/client.crt"
            assert config.client_key_path == "/path/to/client.key"
            assert config.ca_cert_path == "/path/to/ca.crt"

    def test_mtls_config_parameters_from_kwargs(self):
        """Test mTLS config via constructor kwargs."""
        config = MCAConfig(
            service_name="test-service",
            mtls_enabled=True,
            client_cert_path="/path/to/client.crt",
            client_key_path="/path/to/client.key",
            ca_cert_path="/path/to/ca.crt",
        )

        assert config.mtls_enabled is True
        assert config.client_cert_path == "/path/to/client.crt"
        assert config.client_key_path == "/path/to/client.key"
        assert config.ca_cert_path == "/path/to/ca.crt"

    def test_mtls_disabled_by_default(self):
        """Test that mTLS is disabled by default."""
        config = MCAConfig(service_name="test-service")

        assert config.mtls_enabled is False
        assert config.client_cert_path is None
        assert config.client_key_path is None
        assert config.ca_cert_path is None

    def test_mtls_enabled_requires_cert_paths(self):
        """Test that enabling mTLS without cert paths raises ConfigurationError."""
        with pytest.raises(
            ConfigurationError,
            match="client_cert_path is required when mtls_enabled=True",
        ):
            MCAConfig(
                service_name="test-service",
                mtls_enabled=True,
            )

    def test_mtls_enabled_requires_key_path(self):
        """Test that enabling mTLS without key path raises ConfigurationError."""
        with pytest.raises(
            ConfigurationError,
            match="client_key_path is required when mtls_enabled=True",
        ):
            MCAConfig(
                service_name="test-service",
                mtls_enabled=True,
                client_cert_path="/path/to/client.crt",
            )

    def test_mtls_enabled_requires_ca_cert_path(self):
        """Test that enabling mTLS without CA cert path raises ConfigurationError."""
        with pytest.raises(
            ConfigurationError, match="ca_cert_path is required when mtls_enabled=True"
        ):
            MCAConfig(
                service_name="test-service",
                mtls_enabled=True,
                client_cert_path="/path/to/client.crt",
                client_key_path="/path/to/client.key",
            )


class TestCertificateValidation:
    """Tests for certificate loading and validation logic."""

    @staticmethod
    def create_test_certificate():
        """Create a valid test certificate for testing."""
        # Generate private key
        private_key = rsa.generate_private_key(
            public_exponent=65537, key_size=2048, backend=default_backend()
        )

        # Generate self-signed certificate
        subject = issuer = x509.Name(
            [
                x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "test-client"),
            ]
        )

        cert = (
            x509.CertificateBuilder()
            .subject_name(subject)
            .issuer_name(issuer)
            .public_key(private_key.public_key())
            .serial_number(x509.random_serial_number())
            .not_valid_before(datetime.utcnow())
            .not_valid_after(datetime.utcnow() + timedelta(days=365))
            .sign(private_key, hashes.SHA256(), default_backend())
        )

        return cert, private_key

    def test_validate_certificate_format_valid_pem(self, tmp_path):
        """Test that valid PEM certificate format is accepted."""
        from mca_sdk.security.certificate import validate_certificate_format

        # Create valid certificate
        cert, private_key = self.create_test_certificate()

        # Write certificate to file
        cert_path = tmp_path / "client.crt"
        cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))

        # Should not raise
        validate_certificate_format(str(cert_path))

    def test_validate_certificate_format_invalid_file(self):
        """Test that non-existent certificate file raises ConfigurationError."""
        from mca_sdk.security.certificate import validate_certificate_format

        with pytest.raises(ConfigurationError, match="Certificate/Key file not found"):
            validate_certificate_format("/nonexistent/path.crt")

    def test_validate_certificate_format_invalid_pem(self, tmp_path):
        """Test that invalid PEM format raises ConfigurationError."""
        from mca_sdk.security.certificate import validate_certificate_format

        # Write invalid PEM content
        cert_path = tmp_path / "invalid.crt"
        cert_path.write_text("NOT A VALID CERTIFICATE")

        with pytest.raises(ConfigurationError, match="Invalid certificate format"):
            validate_certificate_format(str(cert_path))

    def test_check_certificate_expiration_valid(self, tmp_path):
        """Test certificate expiration check for valid certificate."""
        from mca_sdk.security.certificate import check_certificate_expiration

        # Create valid certificate
        cert, private_key = self.create_test_certificate()

        # Write certificate to file
        cert_path = tmp_path / "client.crt"
        cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))

        # Should not raise and not warn
        check_certificate_expiration(str(cert_path))

    def test_check_certificate_expiration_expired(self, tmp_path):
        """Test that expired certificate raises ConfigurationError."""
        from mca_sdk.security.certificate import check_certificate_expiration

        # Generate expired certificate
        private_key = rsa.generate_private_key(
            public_exponent=65537, key_size=2048, backend=default_backend()
        )

        subject = issuer = x509.Name(
            [
                x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "test-client"),
            ]
        )

        cert = (
            x509.CertificateBuilder()
            .subject_name(subject)
            .issuer_name(issuer)
            .public_key(private_key.public_key())
            .serial_number(x509.random_serial_number())
            .not_valid_before(datetime.utcnow() - timedelta(days=365))
            .not_valid_after(datetime.utcnow() - timedelta(days=1))  # Expired yesterday
            .sign(private_key, hashes.SHA256(), default_backend())
        )

        # Write certificate to file
        cert_path = tmp_path / "expired.crt"
        cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))

        with pytest.raises(ConfigurationError, match="Certificate has expired"):
            check_certificate_expiration(str(cert_path))

    def test_check_certificate_expiration_expiring_soon(self, tmp_path, caplog):
        """Test that certificate expiring within 30 days emits warning."""
        from mca_sdk.security.certificate import check_certificate_expiration
        import logging

        # Generate certificate expiring in 15 days
        private_key = rsa.generate_private_key(
            public_exponent=65537, key_size=2048, backend=default_backend()
        )

        subject = issuer = x509.Name(
            [
                x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "test-client"),
            ]
        )

        cert = (
            x509.CertificateBuilder()
            .subject_name(subject)
            .issuer_name(issuer)
            .public_key(private_key.public_key())
            .serial_number(x509.random_serial_number())
            .not_valid_before(datetime.utcnow() - timedelta(days=1))
            .not_valid_after(datetime.utcnow() + timedelta(days=15))  # Expires in 15 days
            .sign(private_key, hashes.SHA256(), default_backend())
        )

        # Write certificate to file
        cert_path = tmp_path / "expiring.crt"
        cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))

        with caplog.at_level(logging.WARNING):
            check_certificate_expiration(str(cert_path))

        assert "Certificate expires in" in caplog.text
        assert "days" in caplog.text


class TestMTLSIntegration:
    """Tests for mTLS integration with gRPC client."""

    def test_create_grpc_channel_without_mtls(self):
        """Test gRPC channel creation without mTLS."""
        from mca_sdk.security.certificate import create_grpc_channel_credentials

        # Without mTLS, should return None (insecure)
        credentials = create_grpc_channel_credentials(mtls_enabled=False)
        assert credentials is None

    def test_create_grpc_channel_with_mtls(self, tmp_path):
        """Test gRPC channel creation with mTLS credentials."""
        from mca_sdk.security.certificate import create_grpc_channel_credentials

        # Create test certificates
        cert, private_key = TestCertificateValidation.create_test_certificate()

        # Write certificate files
        cert_path = tmp_path / "client.crt"
        key_path = tmp_path / "client.key"
        ca_path = tmp_path / "ca.crt"

        cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
        key_path.write_bytes(
            private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.TraditionalOpenSSL,
                encryption_algorithm=serialization.NoEncryption(),
            )
        )
        ca_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))

        # Mock grpc module since it may not be installed in test environment
        mock_credentials = Mock()
        with patch.dict(
            "sys.modules",
            {"grpc": Mock(ssl_channel_credentials=Mock(return_value=mock_credentials))},
        ):
            # Create credentials
            credentials = create_grpc_channel_credentials(
                mtls_enabled=True,
                client_cert_path=str(cert_path),
                client_key_path=str(key_path),
                ca_cert_path=str(ca_path),
            )

            assert credentials is not None

    def test_create_grpc_channel_invalid_cert(self, tmp_path):
        """Test that invalid certificate raises ConfigurationError."""
        from mca_sdk.security.certificate import create_grpc_channel_credentials

        # Create invalid certificate files
        cert_path = tmp_path / "invalid.crt"
        key_path = tmp_path / "invalid.key"
        ca_path = tmp_path / "invalid_ca.crt"

        cert_path.write_text("INVALID CERT")
        key_path.write_text("INVALID KEY")
        ca_path.write_text("INVALID CA")

        # Should fail due to invalid certificate format (before grpc import)
        with pytest.raises(ConfigurationError, match="Invalid certificate format"):
            create_grpc_channel_credentials(
                mtls_enabled=True,
                client_cert_path=str(cert_path),
                client_key_path=str(key_path),
                ca_cert_path=str(ca_path),
            )


class TestSecurityPractices:
    """Tests for security best practices in mTLS implementation."""

    def test_private_key_not_logged_in_errors(self, tmp_path, caplog):
        """Test that private key content is never exposed in error messages."""
        from mca_sdk.security.certificate import validate_certificate_format
        import logging

        # Create file with secret content
        key_path = tmp_path / "secret.key"
        secret_content = "SUPER SECRET PRIVATE KEY CONTENT"
        key_path.write_text(secret_content)

        with caplog.at_level(logging.ERROR):
            try:
                validate_certificate_format(str(key_path))
            except ConfigurationError as e:
                # Error message should NOT contain private key content
                assert secret_content not in str(e)
                assert secret_content not in caplog.text

    def test_certificate_paths_support_relative_paths(self):
        """Test that certificate paths support relative path resolution."""
        config = MCAConfig(
            service_name="test-service",
            mtls_enabled=True,
            client_cert_path="./certs/client.crt",
            client_key_path="./certs/client.key",
            ca_cert_path="./certs/ca.crt",
        )

        # Paths should be stored as-is, resolution happens at load time
        assert config.client_cert_path == "./certs/client.crt"
        assert config.client_key_path == "./certs/client.key"
        assert config.ca_cert_path == "./certs/ca.crt"
