#!/usr/bin/env python3
"""
Test suite for demo server configuration.
"""

import os
from unittest.mock import patch

from ..config import DemoServerConfig


class TestDemoServerConfig:
    """Test cases for DemoServerConfig."""

    def test_default_configuration(self):
        """Test default configuration values."""
        config = DemoServerConfig()
        assert config.log_level == "info"

    def test_config_dict_override(self):
        """Test configuration override with config_dict."""
        config_dict = {"hello_from": "Test Server"}
        config = DemoServerConfig(config_dict).get_template_config()
        assert config.get("hello_from") == "Test Server", "Config dict override failed"

    @patch.dict(
        os.environ, {"MCP_HELLO_FROM": "Environment Server", "MCP_LOG_LEVEL": "warning"}
    )
    def test_environment_variables(self):
        """Test configuration from environment variables."""
        config = DemoServerConfig()
        assert config.log_level == "warning"

    @patch.dict(os.environ, {"MCP_HELLO_FROM": "TEST SERVER FROM ENV"})
    def test_config_dict_precedence_over_env(self):
        """Test that config_dict takes precedence over environment."""

        config_dict = {"hello_from": "Config Dict Server"}
        config = DemoServerConfig(config_dict).get_template_config()
        assert (
            config.get("hello_from") == "Config Dict Server"
        ), "Config dict should override"

    def test_invalid_log_level_validation(self):
        """Test validation of invalid log level."""
        config_dict = {"log_level": "invalid"}
        config = DemoServerConfig(config_dict)

        # Should default to "info" for invalid log level
        assert config.log_level == "info"


class TestProcessNestedConfig:
    """Test the _process_nested_config method and type coercion."""

    def setup_method(self):
        """Setup for each test method."""
        # Mock template data
        self.mock_template_data = {
            "config_schema": {
                "properties": {
                    "hello_from": {
                        "type": "string",
                        "default": "MCP Platform",
                        "env_mapping": "MCP_HELLO_FROM",
                    },
                    "log_level": {
                        "type": "string",
                        "default": "info",
                        "env_mapping": "MCP_LOG_LEVEL",
                    },
                    "debug_mode": {
                        "type": "boolean",
                        "default": False,
                        "env_mapping": "MCP_DEBUG_MODE",
                    },
                    "max_connections": {
                        "type": "integer",
                        "default": 10,
                        "env_mapping": "MCP_MAX_CONNECTIONS",
                    },
                    "timeout_seconds": {
                        "type": "number",
                        "default": 30.5,
                        "env_mapping": "MCP_TIMEOUT_SECONDS",
                    },
                    "allowed_hosts": {
                        "type": "array",
                        "default": ["localhost"],
                        "env_mapping": "MCP_ALLOWED_HOSTS",
                        "env_separator": ",",
                    },
                    "metadata": {
                        "type": "object",
                        "default": {},
                        "env_mapping": "MCP_METADATA",
                    },
                }
            }
        }

    def test_process_nested_config_simple(self):
        """Test processing simple double underscore notation."""
        config_dict = {
            "demo__hello_from": "Custom Server",
            "template__log_level": "debug",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            # Should process template-level overrides
            assert config.config_dict["hello_from"] == "Custom Server"
            assert config.config_dict["log_level"] == "debug"

    def test_process_nested_config_deep(self):
        """Test processing deep nested double underscore notation."""
        config_dict = {
            "system__config__debug_mode": "true",
            "app__settings__timeout": "45.0",
            "server__network__max_connections": "20",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            # Should process deep nested configs
            # Config properties get their prefixes stripped
            assert (
                config.config_dict["debug_mode"] is True
            )  # Recognized as config property, type coerced
            assert (
                config.config_dict["max_connections"] == 20
            )  # Recognized as config property, type coerced
            # Non-config properties keep their nested structure
            assert config.config_dict["app_settings_timeout"] == "45.0"

    def test_type_coercion_boolean(self):
        """Test boolean type coercion."""
        config_dict = {
            "debug_mode": "true",
            "MCP_DEBUG_MODE": "false",  # Test env_mapping lookup too
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            assert config.config_dict["debug_mode"] is True
            assert config.config_dict["MCP_DEBUG_MODE"] is False

    def test_type_coercion_integer(self):
        """Test integer type coercion."""
        config_dict = {"max_connections": "25"}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            assert config.config_dict["max_connections"] == 25
            assert isinstance(config.config_dict["max_connections"], int)

    def test_type_coercion_number(self):
        """Test number (float) type coercion."""
        config_dict = {"timeout_seconds": "45.5"}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            assert abs(config.config_dict["timeout_seconds"] - 45.5) < 0.001
            assert isinstance(config.config_dict["timeout_seconds"], float)

    def test_type_coercion_array_json(self):
        """Test array type coercion from JSON string."""
        config_dict = {"allowed_hosts": '["host1", "host2", "host3"]'}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            assert config.config_dict["allowed_hosts"] == ["host1", "host2", "host3"]
            assert isinstance(config.config_dict["allowed_hosts"], list)

    def test_type_coercion_array_comma_separated(self):
        """Test array type coercion from comma-separated string."""
        config_dict = {"allowed_hosts": "host1,host2,host3"}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            assert config.config_dict["allowed_hosts"] == ["host1", "host2", "host3"]
            assert isinstance(config.config_dict["allowed_hosts"], list)

    def test_type_coercion_object(self):
        """Test object type coercion from JSON string."""
        config_dict = {"metadata": '{"version": "1.0.0", "author": "Test"}'}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            expected = {"version": "1.0.0", "author": "Test"}
            assert config.config_dict["metadata"] == expected
            assert isinstance(config.config_dict["metadata"], dict)

    def test_type_coercion_fallback_on_error(self):
        """Test that invalid values fall back to original value."""
        config_dict = {
            "max_connections": "not_a_number",
            "debug_mode": "invalid_bool",
            "allowed_hosts": "[malformed json]",  # Malformed JSON array
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            # Should fall back to original string values
            assert config.config_dict["max_connections"] == "not_a_number"
            assert config.config_dict["debug_mode"] == "invalid_bool"
            assert config.config_dict["allowed_hosts"] == "[malformed json]"

    def test_env_mapping_lookup(self):
        """Test that type coercion works with env_mapping lookup."""
        config_dict = {"MCP_DEBUG_MODE": "true", "MCP_MAX_CONNECTIONS": "15"}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            # Should find property by env_mapping and coerce types
            assert config.config_dict["MCP_DEBUG_MODE"] is True
            assert config.config_dict["MCP_MAX_CONNECTIONS"] == 15

    def test_unknown_properties_remain_unchanged(self):
        """Test that unknown properties are not type-coerced."""
        config_dict = {"unknown_property": "123", "another_unknown": "true"}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            # Should remain as strings since no schema is found
            assert config.config_dict["unknown_property"] == "123"
            assert config.config_dict["another_unknown"] == "true"

    def test_complex_nested_override_with_coercion(self):
        """Test complex nested override with type coercion."""
        config_dict = {
            "app__debug_mode": "true",
            "system__max_connections": "50",
            "network__log_level": "warning",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            # Should process nested keys and apply type coercion for schema properties
            assert config.config_dict["debug_mode"] is True  # schema property, coerced
            assert (
                config.config_dict["max_connections"] == 50
            )  # schema property, coerced
            assert (
                config.config_dict["log_level"] == "warning"
            )  # schema property, no coercion needed

    def test_custom_separator_array(self):
        """Test array parsing with custom separator."""
        # Add custom separator to mock template
        self.mock_template_data["config_schema"]["properties"]["custom_list"] = {
            "type": "array",
            "env_mapping": "MCP_CUSTOM_LIST",
            "env_separator": "|",
        }

        config_dict = {"custom_list": "item1|item2|item3"}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            assert config.config_dict["custom_list"] == ["item1", "item2", "item3"]

    def test_empty_template_data_no_coercion(self):
        """Test that no coercion happens when template_data is empty."""
        config_dict = {"debug_mode": "true", "max_connections": "15"}

        with patch.object(DemoServerConfig, "_load_template", return_value={}):
            config = DemoServerConfig(config_dict)

            # Should remain as strings since no schema is available
            assert config.config_dict["debug_mode"] == "true"
            assert config.config_dict["max_connections"] == "15"


class TestTemplateOverrides:
    """Test template.json structure overrides using double underscore notation."""

    def setup_method(self):
        """Setup for each test method."""
        self.mock_template_data = {
            "name": "Demo Server",
            "version": "1.0.0",
            "tools": [
                {"name": "say_hello", "description": "Say hello", "enabled": True},
                {"name": "get_info", "description": "Get info", "enabled": False},
            ],
            "metadata": {"author": "Original Author", "category": "demo"},
            "config_schema": {
                "properties": {
                    "hello_from": {
                        "type": "string",
                        "default": "MCP Platform",
                        "env_mapping": "MCP_HELLO_FROM",
                    }
                }
            },
        }

    def test_simple_template_override(self):
        """Test simple template property override."""
        config_dict = {"name": "Custom Demo Server", "version": "2.0.0"}

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            assert template_data["name"] == "Custom Demo Server"
            assert template_data["version"] == "2.0.0"

    def test_nested_tool_override(self):
        """Test overriding nested tool properties."""
        config_dict = {
            "tools__0__name": "custom_hello",
            "tools__0__enabled": "false",
            "tools__1__description": "Custom get info tool",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            assert template_data["tools"][0]["name"] == "custom_hello"
            assert template_data["tools"][0]["enabled"] is False
            assert template_data["tools"][1]["description"] == "Custom get info tool"

    def test_deep_nested_metadata_override(self):
        """Test deep nested metadata overrides."""
        config_dict = {
            "metadata__author": "New Author",
            "metadata__custom__field": "custom_value",
            "metadata__nested__deep__property": "deep_value",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            assert template_data["metadata"]["author"] == "New Author"
            assert template_data["metadata"]["custom"]["field"] == "custom_value"
            assert (
                template_data["metadata"]["nested"]["deep"]["property"] == "deep_value"
            )

    def test_array_creation_override(self):
        """Test creating new array elements through overrides."""
        config_dict = {
            "tools__2__name": "new_tool",
            "tools__2__description": "A newly created tool",
            "tools__2__enabled": "true",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            # Should extend the tools array to include index 2
            assert len(template_data["tools"]) >= 3
            assert template_data["tools"][2]["name"] == "new_tool"
            assert template_data["tools"][2]["description"] == "A newly created tool"
            assert template_data["tools"][2]["enabled"] is True

    def test_json_object_override(self):
        """Test overriding with JSON object strings."""
        config_dict = {
            "metadata__config": '{"timeout": 30, "retry": true}',
            "tools__0__parameters": '["name", "message"]',
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            expected_config = {"timeout": 30, "retry": True}
            expected_params = ["name", "message"]

            assert template_data["metadata"]["config"] == expected_config
            assert template_data["tools"][0]["parameters"] == expected_params

    def test_numeric_type_inference(self):
        """Test automatic type inference for numeric values."""
        config_dict = {
            "metadata__version_number": "123",
            "metadata__rating": "4.5",
            "tools__0__timeout": "30",
            "tools__0__max_retries": "5",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            assert template_data["metadata"]["version_number"] == 123
            assert abs(template_data["metadata"]["rating"] - 4.5) < 0.001
            assert template_data["tools"][0]["timeout"] == 30
            assert template_data["tools"][0]["max_retries"] == 5

    def test_boolean_type_inference(self):
        """Test automatic type inference for boolean values."""
        config_dict = {
            "tools__0__enabled": "false",
            "tools__1__enabled": "true",
            "metadata__deprecated": "false",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            assert template_data["tools"][0]["enabled"] is False
            assert template_data["tools"][1]["enabled"] is True
            assert template_data["metadata"]["deprecated"] is False

    def test_complex_nested_structure_creation(self):
        """Test creating complex nested structures."""
        config_dict = {
            "servers__0__config__database__host": "localhost",
            "servers__0__config__database__port": "5432",
            "servers__0__config__redis__host": "redis-server",
            "servers__0__config__redis__port": "6379",
            "servers__1__name": "backup_server",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            # Should create the entire nested structure
            assert (
                template_data["servers"][0]["config"]["database"]["host"] == "localhost"
            )
            assert template_data["servers"][0]["config"]["database"]["port"] == 5432
            assert (
                template_data["servers"][0]["config"]["redis"]["host"] == "redis-server"
            )
            assert template_data["servers"][0]["config"]["redis"]["port"] == 6379
            assert template_data["servers"][1]["name"] == "backup_server"

    def test_override_vs_config_distinction(self):
        """Test that template overrides and config properties are handled separately."""
        config_dict = {
            # This should be treated as a config property (has env_mapping)
            "hello_from": "Config Hello",
            # This should be treated as template override (no env_mapping)
            "tools__0__custom_field": "template_override",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)

            # Config property should be in config_dict
            assert config.config_dict["hello_from"] == "Config Hello"

            # Template override should modify the template structure
            template_data = config.get_template_data()
            assert template_data["tools"][0]["custom_field"] == "template_override"

    def test_malformed_json_fallback(self):
        """Test that malformed JSON falls back to string value."""
        config_dict = {
            "metadata__config": '{"malformed": json}',
            "tools__0__bad_array": "[invalid, json]",
        }

        with patch.object(
            DemoServerConfig, "_load_template", return_value=self.mock_template_data
        ):
            config = DemoServerConfig(config_dict)
            template_data = config.get_template_data()

            # Should fall back to string values
            assert template_data["metadata"]["config"] == '{"malformed": json}'
            assert template_data["tools"][0]["bad_array"] == "[invalid, json]"
