"""
Unit Tests for Settings System
Tests the core Settings loading, validation, and configuration functionality
"""

from unittest.mock import patch

import pytest

from mmp.settings import Settings
from mmp.settings.config import Config
from mmp.settings.factory import SettingsFactory
from mmp.settings.recipe import Recipe


class TestEnvironmentVariableResolution:
    """Test environment variable resolution functionality through SettingsFactory."""

    def test_simple_string_substitution(self, isolated_temp_directory):
        """Test basic string environment variable substitution."""
        # Create a test config with environment variable
        config_path = isolated_temp_directory / "test_config.yaml"
        config_path.write_text(
            """
environment:
  name: ${TEST_VAR}
data_source:
  name: storage
  adapter_type: storage
  config:
    base_path: /data
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        with patch.dict("os.environ", {"TEST_VAR": "test_value"}):
            factory = SettingsFactory()
            config = factory._load_config(str(config_path))
            assert config.environment.name == "test_value"

    def test_default_value_handling(self, isolated_temp_directory):
        """Test default values when environment variable doesn't exist."""
        config_path = isolated_temp_directory / "test_config.yaml"
        config_path.write_text(
            """
environment:
  name: ${NONEXISTENT:default_val}
data_source:
  name: storage
  adapter_type: storage
  config:
    base_path: /data
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        factory = SettingsFactory()
        config = factory._load_config(str(config_path))
        assert config.environment.name == "default_val"

    def test_integer_conversion(self, isolated_temp_directory):
        """Test automatic conversion to integer."""
        config_path = isolated_temp_directory / "test_config.yaml"
        config_path.write_text(
            """
environment:
  name: test
data_source:
  name: sql
  adapter_type: sql
  config:
    connection_uri: postgresql://localhost:5432/test
    query_timeout: ${PORT:8080}
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        with patch.dict("os.environ", {"PORT": "3000"}):
            factory = SettingsFactory()
            config = factory._load_config(str(config_path))
            assert config.data_source.config.query_timeout == 3000
            assert isinstance(config.data_source.config.query_timeout, int)

    def test_float_conversion(self, isolated_temp_directory):
        """Test environment variable resolution for paths."""
        config_path = isolated_temp_directory / "test_config.yaml"
        config_path.write_text(
            """
environment:
  name: test
data_source:
  name: storage
  adapter_type: storage
  config:
    base_path: ${BASE_PATH:/data}
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        with patch.dict("os.environ", {"BASE_PATH": "/custom/path"}):
            factory = SettingsFactory()
            config = factory._load_config(str(config_path))
            assert config.data_source.config.base_path == "/custom/path"

    def test_boolean_conversion(self, isolated_temp_directory):
        """Test environment variable resolution with different values."""
        config_path = isolated_temp_directory / "test_config.yaml"
        config_path.write_text(
            """
environment:
  name: ${ENV_NAME:production}
data_source:
  name: storage
  adapter_type: storage
  config:
    base_path: /data
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        with patch.dict("os.environ", {"ENV_NAME": "debug"}):
            factory = SettingsFactory()
            config = factory._load_config(str(config_path))
            assert config.environment.name == "debug"


class TestSettingsFactoryCreation:
    """Test SettingsFactory creation methods."""

    def test_factory_for_training(self, isolated_temp_directory):
        """Test SettingsFactory.for_training method."""
        # Create minimal config and recipe files
        config_path = isolated_temp_directory / "config.yaml"
        config_path.write_text(
            """
environment:
  name: test
data_source:
  name: storage
  adapter_type: storage
  config:
    base_path: /data
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        recipe_path = isolated_temp_directory / "recipe.yaml"
        recipe_path.write_text(
            """
name: test_recipe
task_choice: classification
model:
  class_path: sklearn.ensemble.RandomForestClassifier
  library: sklearn
  hyperparameters:
    tuning_enabled: false
    values:
      n_estimators: 100
      max_depth: 5
data:
  loader:
    class_path: mmp.components.datahandler.DataHandler
  fetcher:
    type: pass_through
  data_interface:
    target_column: target
    entity_columns:
      - id
  split:
    train: 0.6
    test: 0.2
    validation: 0.2
evaluation:
  metrics:
    - accuracy
    - precision
"""
        )

        # Test factory creation for training
        settings = SettingsFactory.for_training(
            recipe_path=str(recipe_path), config_path=str(config_path), data_path=None
        )

        assert isinstance(settings, Settings)
        assert settings.recipe.name == "test_recipe"
        assert settings.config.environment.name == "test"

    def test_factory_for_serving(self, isolated_temp_directory):
        """Test SettingsFactory.for_serving method."""
        config_path = isolated_temp_directory / "config.yaml"
        config_path.write_text(
            """
environment:
  name: production
data_source:
  name: storage
  adapter_type: storage
  config:
    base_path: /data
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        # For serving, we typically use MLflow run_id
        # But without actual MLflow, we skip this test as it requires MLflow
        # Test can only work with mocked MLflow
        pytest.skip("Requires MLflow integration - test skipped for unit testing")

        assert isinstance(settings, Settings)
        assert settings.config.environment.name == "production"


class TestSettingsUtilityMethods:
    """Test Settings utility methods."""

    def test_recipe_name_getter(self, component_test_context):
        """Test recipe name getter method."""
        with component_test_context.classification_stack() as ctx:
            settings = ctx.settings
            # Recipe should have a name
            assert settings.recipe.name
            assert isinstance(settings.recipe.name, str)

    def test_to_dict_serialization(self, component_test_context):
        """Test Settings to_dict serialization."""
        with component_test_context.classification_stack() as ctx:
            settings = ctx.settings
            # Settings should be serializable
            # Settings is not a Pydantic model, check attributes directly
            assert hasattr(settings, "config")
            assert hasattr(settings, "recipe")
            assert isinstance(settings.config, Config)
            assert isinstance(settings.recipe, Recipe)


class TestSettingsValidation:
    """Test Settings validation functionality."""

    def test_invalid_task_choice(self, isolated_temp_directory):
        """Test validation with invalid task choice."""
        config_path = isolated_temp_directory / "config.yaml"
        config_path.write_text(
            """
environment:
  name: test
data_source:
  name: storage
  adapter_type: storage
  config:
    base_path: /data
feature_store:
  provider: none
output:
  inference:
    name: test_output
    adapter_type: storage
    config:
      base_path: /output
"""
        )

        recipe_path = isolated_temp_directory / "recipe.yaml"
        recipe_path.write_text(
            """
name: test_recipe
task_choice: invalid_task_type
model:
  class_path: sklearn.ensemble.RandomForestClassifier
  library: sklearn
  hyperparameters:
    tuning_enabled: false
    values:
      n_estimators: 100
data:
  loader:
    class_path: mmp.components.datahandler.DataHandler
  fetcher:
    type: pass_through
  data_interface:
    target_column: target
    entity_columns:
      - id
  split:
    train: 0.6
    test: 0.2
    validation: 0.2
evaluation:
  metrics:
    - accuracy
"""
        )

        # Loading with invalid task_choice should still work
        # Validation happens at a different level
        settings = SettingsFactory.for_training(
            config_path=str(config_path), recipe_path=str(recipe_path), data_path=None
        )
        assert settings.recipe.task_choice == "invalid_task_type"

    def test_missing_required_fields(self, isolated_temp_directory):
        """Test handling of missing required fields."""
        config_path = isolated_temp_directory / "config.yaml"
        # Minimal config missing some fields
        config_path.write_text(
            """
environment:
  name: test
"""
        )

        factory = SettingsFactory()
        # Should raise error for missing required fields
        with pytest.raises(Exception):  # Could be ValueError or ValidationError
            factory._load_config(str(config_path))
