"""Tests for environment-aware configuration profiles (Story 8.4).

Tests cover:
- MCA_ENV parsing and storage
- Conditional registry validation based on environment
- Dynamic collector routing based on environment
"""

import os
import pytest
from unittest.mock import Mock, patch, MagicMock

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


class TestEnvironmentVariableParsing:
    """Test that MCA_ENV is correctly parsed and stored."""

    def test_default_environment_is_prod(self, monkeypatch):
        """Test that environment defaults to 'prod' when not specified."""
        # Clear MCA_ENV if set
        monkeypatch.delenv("MCA_ENV", raising=False)

        # Prod requires explicit collector_endpoint
        config = MCAConfig.load(
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
            collector_endpoint="https://prod-collector:4318",
        )

        assert config.environment == "prod"

    def test_environment_from_env_variable(self, monkeypatch):
        """Test that MCA_ENV environment variable is correctly parsed."""
        monkeypatch.setenv("MCA_ENV", "staging")

        config = MCAConfig.load(
            service_name="test-service", model_id="mdl-001", team_name="test-team"
        )

        assert config.environment == "staging"

    def test_environment_from_config_yaml(self, tmp_path, monkeypatch):
        """Test that environment can be set via mca.yaml config file."""
        # Create config file with dev environment (requires collector_endpoint)
        config_file = tmp_path / "mca.yaml"
        config_file.write_text(
            "environment: dev\n"
            "service_name: test-service\n"
            "model_id: mdl-001\n"
            "team_name: test-team\n"
            "collector_endpoint: https://dev-collector:4318\n"
        )

        monkeypatch.delenv("MCA_ENV", raising=False)

        config = MCAConfig.load(
            config_file=str(config_file),
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
        )

        assert config.environment == "dev"

    def test_environment_from_kwargs(self, monkeypatch):
        """Test that environment can be set via constructor kwargs."""
        monkeypatch.delenv("MCA_ENV", raising=False)

        config = MCAConfig(
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
            environment="staging",
        )

        assert config.environment == "staging"

    def test_env_variable_overrides_yaml(self, tmp_path, monkeypatch):
        """Test that MCA_ENV environment variable takes precedence over yaml."""
        # Create config file with dev (would require collector_endpoint)
        config_file = tmp_path / "mca.yaml"
        config_file.write_text(
            "environment: dev\n"
            "service_name: test-service\n"
            "model_id: mdl-001\n"
            "team_name: test-team\n"
            "collector_endpoint: https://dev-collector:4318\n"
        )

        # Set env variable to staging (overrides dev from YAML, defaults collector_endpoint)
        monkeypatch.setenv("MCA_ENV", "staging")

        config = MCAConfig.load(
            config_file=str(config_file),
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
        )

        # MCA_ENV should override environment from YAML
        assert config.environment == "staging"
        # Since we're in staging now, collector_endpoint from YAML is still used
        assert config.collector_endpoint == "https://dev-collector:4318"

    def test_local_environment_alias_for_staging(self, monkeypatch):
        """Test that 'local' is accepted as an alias for 'staging'."""
        monkeypatch.setenv("MCA_ENV", "local")

        config = MCAConfig.load(
            service_name="test-service", model_id="mdl-001", team_name="test-team"
        )

        # 'local' should be normalized to 'staging' in __post_init__
        assert config.environment == "staging"


class TestConditionalRegistryValidation:
    """Test registry validation behavior based on environment."""

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_staging_allows_missing_registry_credentials(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test that staging environment allows initialization without registry credentials."""
        monkeypatch.setenv("MCA_ENV", "staging")
        # Clear any existing registry credentials
        monkeypatch.delenv("MCA_REGISTRY_URL", raising=False)
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)

        # Mock provider setup to return required tuple
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Should not raise error in staging
        client = MCAClient(
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
            # No registry credentials provided
        )

        assert client is not None
        # Registry client should not be instantiated in staging without credentials
        mock_registry_client.assert_not_called()

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_dev_allows_missing_registry_credentials(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test that dev environment allows operation without registry credentials."""
        monkeypatch.setenv("MCA_ENV", "dev")
        # Clear any existing registry credentials
        monkeypatch.delenv("MCA_REGISTRY_URL", raising=False)
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)
        monkeypatch.delenv("MCA_MODEL_ID", raising=False)
        # Provide collector endpoint (required in dev)
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "https://dev-collector:4318")

        # Mock provider setup
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Should successfully initialize without registry (Story 2.7)
        client = MCAClient(
            service_name="test-service",
            team_name="test-team",
            # No registry credentials provided
        )
        assert client is not None
        assert client.config.environment == "dev"
        # Verify registry client was never instantiated (network isolation)
        mock_registry_client.assert_not_called()

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_prod_allows_missing_registry_credentials(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test that prod environment allows operation without registry credentials.

        Note: Prod now defaults to strict_validation=True (Story 2.7 security fix).
        This test explicitly disables strict validation to test registry-only behavior.
        """
        monkeypatch.delenv("MCA_ENV", raising=False)  # Defaults to prod
        # Clear any existing registry credentials
        monkeypatch.delenv("MCA_REGISTRY_URL", raising=False)
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)
        monkeypatch.delenv("MCA_MODEL_ID", raising=False)
        # Provide collector endpoint (required in prod)
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "https://prod-collector:4318")

        # Mock provider setup
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Should successfully initialize without registry (Story 2.7)
        # Disable strict validation explicitly (prod defaults to strict=True as of 2.7 fix)
        client = MCAClient(
            service_name="test-service",
            team_name="test-team",
            strict_validation=False,
            # No registry credentials provided
        )
        assert client is not None
        assert client.config.environment == "prod"
        # Verify registry client was never instantiated (network isolation)
        mock_registry_client.assert_not_called()

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_complete_registry_config_requires_token_in_dev(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test that complete registry config (URL + model_id) requires token in dev/prod.

        This is Case 4: When user explicitly provides both URL and model_id,
        authentication is required. This validates intentional registry usage.
        """
        monkeypatch.setenv("MCA_ENV", "dev")
        # Provide URL but not token
        monkeypatch.setenv("MCA_REGISTRY_URL", "https://registry.example.com")
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "https://collector:4318")

        # Mock provider setup
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Should raise specific error about missing token (Case 4: complete config without auth)
        with pytest.raises(ConfigurationError, match="Registry authentication required"):
            MCAClient(service_name="test-service", model_id="mdl-001", team_name="test-team")

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_model_id_without_registry_url_works(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test that model_id without registry_url works (Case 3: local-only usage)."""
        monkeypatch.setenv("MCA_ENV", "dev")
        # Provide model_id but no registry URL
        monkeypatch.delenv("MCA_REGISTRY_URL", raising=False)
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "https://collector:4318")

        # Mock provider setup
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Should succeed with info log about local-only usage
        client = MCAClient(service_name="test-service", model_id="mdl-001", team_name="test-team")
        assert client is not None
        assert client.config.model_id == "mdl-001"
        # Verify registry client was never instantiated (network isolation)
        mock_registry_client.assert_not_called()

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_registry_url_without_model_id_warns_but_continues(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test that registry_url without model_id warns but continues (Case 2: partial config).

        Note: strict_validation is auto-enabled when registry_url is present, so we need to
        explicitly disable it for this test (registry_url presence triggers strict mode).
        """
        monkeypatch.setenv("MCA_ENV", "dev")
        # Provide registry URL but no model_id
        monkeypatch.setenv("MCA_REGISTRY_URL", "https://registry.example.com")
        monkeypatch.delenv("MCA_MODEL_ID", raising=False)
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "https://collector:4318")

        # Mock provider setup
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Should succeed with warning log (partial config)
        # Disable strict validation since registry_url triggers auto-strict mode
        client = MCAClient(
            service_name="test-service", team_name="test-team", strict_validation=False
        )
        assert client is not None
        # Verify registry client was never instantiated (network isolation)
        mock_registry_client.assert_not_called()

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_staging_allows_incomplete_registry_without_token(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test that staging allows complete registry config without token (backward compatibility).

        This validates staging's special Case 4 exemption: URL + model_id without token
        logs warning but continues, letting hydration fail gracefully.
        """
        monkeypatch.setenv("MCA_ENV", "staging")
        # Provide both URL and model_id but no token
        monkeypatch.setenv("MCA_REGISTRY_URL", "https://registry.example.com")
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "https://collector:4318")

        # Mock provider setup
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Should succeed with warning log (staging backward compatibility)
        # Note: With fixed validation, staging now skips hydration when auth is missing
        # to prevent slow startup penalty from doomed 401 network call
        client = MCAClient(service_name="test-service", model_id="mdl-001", team_name="test-team")
        assert client is not None
        assert client.config.environment == "staging"
        # Verify registry client was never instantiated (prevents slow 401/timeout)
        mock_registry_client.assert_not_called()


class TestDynamicCollectorRouting:
    """Test OTLP endpoint routing based on environment."""

    def test_staging_defaults_to_mock_collector(self, monkeypatch):
        """Test that staging environment defaults to mock collector endpoint."""
        monkeypatch.setenv("MCA_ENV", "staging")
        monkeypatch.delenv("MCA_COLLECTOR_ENDPOINT", raising=False)

        config = MCAConfig.load(
            service_name="test-service", model_id="mdl-001", team_name="test-team"
        )

        # Staging should default to local mock collector
        assert config.collector_endpoint == "http://localhost:4318"

    def test_staging_respects_explicit_endpoint_override(self, monkeypatch):
        """Test that explicit endpoint override works in staging."""
        monkeypatch.setenv("MCA_ENV", "staging")

        config = MCAConfig(
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
            collector_endpoint="http://custom-endpoint:4318",
            allow_insecure_collector=True,  # Required for HTTP non-localhost
        )

        # Explicit override should be respected
        assert config.collector_endpoint == "http://custom-endpoint:4318"

    def test_dev_requires_explicit_collector_endpoint(self, monkeypatch):
        """Test that dev environment REQUIRES explicit collector endpoint (strict routing per AC#3)."""
        monkeypatch.setenv("MCA_ENV", "dev")
        monkeypatch.delenv("MCA_COLLECTOR_ENDPOINT", raising=False)

        # Should raise ConfigurationError when endpoint is not provided in dev (strict routing)
        with pytest.raises(ConfigurationError, match="collector_endpoint.*required.*dev"):
            MCAConfig.load(
                service_name="test-service",
                model_id="mdl-001",
                team_name="test-team",
                # No collector_endpoint provided - should fail
            )

    def test_prod_requires_explicit_collector_endpoint(self, monkeypatch):
        """Test that prod environment REQUIRES explicit collector endpoint (strict routing per AC#3)."""
        monkeypatch.delenv("MCA_ENV", raising=False)  # Defaults to prod
        monkeypatch.delenv("MCA_COLLECTOR_ENDPOINT", raising=False)

        # Should raise ConfigurationError when endpoint is not provided in prod (strict routing)
        with pytest.raises(ConfigurationError, match="collector_endpoint.*required.*prod"):
            MCAConfig.load(
                service_name="test-service",
                model_id="mdl-001",
                team_name="test-team",
                # No collector_endpoint provided - should fail
            )

    def test_dev_accepts_explicit_collector_endpoint(self, monkeypatch):
        """Test that dev environment accepts explicit collector endpoint when provided."""
        monkeypatch.setenv("MCA_ENV", "dev")

        config = MCAConfig.load(
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
            collector_endpoint="http://dev-collector:4318",
            allow_insecure_collector=True,  # Required for HTTP non-localhost
        )

        # Dev should use explicitly configured endpoint
        assert config.collector_endpoint == "http://dev-collector:4318"

    def test_prod_accepts_explicit_collector_endpoint(self, monkeypatch):
        """Test that prod environment accepts explicit collector endpoint when provided."""
        monkeypatch.delenv("MCA_ENV", raising=False)  # Defaults to prod

        config = MCAConfig.load(
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
            collector_endpoint="https://prod-collector:4318",
        )

        # Prod should use explicitly configured endpoint
        assert config.collector_endpoint == "https://prod-collector:4318"

    def test_dev_does_not_default_to_localhost(self, monkeypatch):
        """Test that dev environment does NOT default to localhost (strict routing per AC#3)."""
        monkeypatch.setenv("MCA_ENV", "dev")
        monkeypatch.delenv("MCA_COLLECTOR_ENDPOINT", raising=False)

        # Without explicit endpoint, dev should raise ConfigurationError (no default in dev/prod)
        with pytest.raises(ConfigurationError, match="collector_endpoint.*required.*dev"):
            MCAConfig.load(service_name="test-service", model_id="mdl-001", team_name="test-team")

    def test_collector_endpoint_env_var_not_overridden_by_mcaclient(self, monkeypatch):
        """Test that MCA_COLLECTOR_ENDPOINT is NOT overridden by MCAClient defaults (Fix for Finding #1)."""
        monkeypatch.setenv("MCA_ENV", "staging")
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "http://custom-collector:9999")

        config = MCAConfig.load(
            service_name="test-service",
            model_id="mdl-001",
            team_name="test-team",
            allow_insecure_collector=True,  # Required for HTTP non-localhost
        )

        # Environment variable should take precedence
        assert config.collector_endpoint == "http://custom-collector:9999"


class TestEnvironmentIntegration:
    """Integration tests for environment-aware behavior."""

    def test_staging_full_initialization_without_registry(self, monkeypatch):
        """Test REAL client initialization in staging without registry (Finding #10 fix).

        This test does NOT mock setup_all_providers to verify that the SDK
        actually initializes successfully with real OpenTelemetry components
        in staging environment without registry credentials.
        """
        monkeypatch.setenv("MCA_ENV", "staging")
        monkeypatch.delenv("MCA_REGISTRY_URL", raising=False)
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)

        # Initialize with REAL providers (no mocking)
        # The SDK will use OTLP exporters which may fail to connect,
        # but the initialization should still succeed in staging
        try:
            client = MCAClient(
                service_name="test-service",
                model_id="mdl-001",
                team_name="test-team",
                # Override collector endpoint to prevent network calls
                collector_endpoint="http://localhost:4318",
            )

            # Verify configuration
            assert client.config.environment == "staging"
            assert client.config.collector_endpoint == "http://localhost:4318"

            # Verify client has real provider objects (not mocks)
            assert client.meter is not None
            assert client.tracer is not None
            assert client.logger is not None

            # Verify we can record metrics without errors
            client.record_prediction(latency=0.1, prediction_id="test-001")

            # Clean shutdown
            client.shutdown()

        except Exception as e:
            # If initialization fails, the test should fail
            pytest.fail(f"Client initialization failed in staging: {e}")

    @patch("mca_sdk.core.client.RegistryClient")
    @patch("mca_sdk.core.client.setup_all_providers")
    def test_telemetry_emission_without_registry(
        self, mock_setup_providers, mock_registry_client, monkeypatch
    ):
        """Test AC2: Telemetry events are processed normally without registry.

        Story 2.7 AC2: Given the SDK is running without a registration URL,
        When a telemetry event is emitted,
        Then the event should be processed and exported normally.
        """
        monkeypatch.setenv("MCA_ENV", "dev")
        # Clear registry config
        monkeypatch.delenv("MCA_REGISTRY_URL", raising=False)
        monkeypatch.delenv("MCA_REGISTRY_TOKEN", raising=False)
        monkeypatch.delenv("MCA_MODEL_ID", raising=False)
        monkeypatch.setenv("MCA_COLLECTOR_ENDPOINT", "https://dev-collector:4318")

        # Mock provider setup
        mock_provider = Mock()
        mock_setup_providers.return_value = (
            mock_provider,
            mock_provider,
            mock_provider,
            mock_provider,
            None,
            None,
            None,
            None,
            None,
        )

        # Initialize client without registry
        client = MCAClient(
            service_name="test-service",
            team_name="test-team",
            strict_validation=False,
        )

        # Verify registry was never contacted
        mock_registry_client.assert_not_called()

        # Emit telemetry events - should not raise any exceptions
        # This proves no registry blocker in data pipeline
        try:
            client.record_prediction(latency=0.25, prediction_id="pred-123")
            telemetry_success = True
        except Exception as e:
            telemetry_success = False
            pytest.fail(f"Telemetry emission failed without registry: {e}")

        assert telemetry_success, "Telemetry should work without registry"

        # Verify client has operational telemetry objects
        assert client.tracer is not None
        assert client.meter is not None
        assert client.logger is not None

        client.shutdown()
