"""
Deep behavioral tests for configuration validation patterns.
Tests setting validation, type coercion, default values,
and constraint checking logic.
"""

from datetime import datetime, timezone
import re


# --- Type validation patterns ---


class TestTypeValidation:
    """Tests for configuration type validation."""

    def _validate_type(self, value, expected_type):
        """Validate value is of expected type."""
        if expected_type == "string":
            return isinstance(value, str)
        if expected_type == "int":
            return isinstance(value, int) and not isinstance(value, bool)
        if expected_type == "float":
            return isinstance(value, (int, float)) and not isinstance(
                value, bool
            )
        if expected_type == "bool":
            return isinstance(value, bool)
        if expected_type == "list":
            return isinstance(value, list)
        if expected_type == "dict":
            return isinstance(value, dict)
        return False

    def test_string_valid(self):
        assert self._validate_type("hello", "string") is True

    def test_string_not_int(self):
        assert self._validate_type(123, "string") is False

    def test_int_valid(self):
        assert self._validate_type(42, "int") is True

    def test_int_not_bool(self):
        assert self._validate_type(True, "int") is False

    def test_float_accepts_int(self):
        assert self._validate_type(42, "float") is True

    def test_float_accepts_float(self):
        assert self._validate_type(3.14, "float") is True

    def test_bool_valid(self):
        assert self._validate_type(True, "bool") is True

    def test_list_valid(self):
        assert self._validate_type([1, 2, 3], "list") is True

    def test_dict_valid(self):
        assert self._validate_type({"key": "value"}, "dict") is True


class TestTypeCoercion:
    """Tests for configuration type coercion."""

    def _coerce_to_int(self, value, default=0):
        """Coerce value to integer."""
        if isinstance(value, bool):
            return 1 if value else 0
        try:
            return int(value)
        except (ValueError, TypeError):
            return default

    def _coerce_to_bool(self, value):
        """Coerce value to boolean."""
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            return value.lower() in ("true", "yes", "1", "on")
        if isinstance(value, (int, float)):
            return value != 0
        return bool(value)

    def _coerce_to_list(self, value):
        """Coerce value to list."""
        if isinstance(value, list):
            return value
        if isinstance(value, str):
            return [v.strip() for v in value.split(",") if v.strip()]
        if value is None:
            return []
        return [value]

    def test_int_from_string(self):
        assert self._coerce_to_int("42") == 42

    def test_int_from_float(self):
        assert self._coerce_to_int(3.7) == 3

    def test_int_invalid_uses_default(self):
        assert self._coerce_to_int("not a number", default=-1) == -1

    def test_bool_from_true_string(self):
        assert self._coerce_to_bool("true") is True
        assert self._coerce_to_bool("yes") is True
        assert self._coerce_to_bool("1") is True

    def test_bool_from_false_string(self):
        assert self._coerce_to_bool("false") is False
        assert self._coerce_to_bool("no") is False

    def test_list_from_csv(self):
        result = self._coerce_to_list("a, b, c")
        assert result == ["a", "b", "c"]

    def test_list_from_single(self):
        result = self._coerce_to_list("single")
        assert result == ["single"]


# --- Range validation patterns ---


class TestRangeValidation:
    """Tests for numeric range validation."""

    def _validate_range(self, value, min_val=None, max_val=None):
        """Validate value is within range."""
        if min_val is not None and value < min_val:
            return False
        if max_val is not None and value > max_val:
            return False
        return True

    def _clamp(self, value, min_val, max_val):
        """Clamp value to range."""
        return max(min_val, min(max_val, value))

    def test_within_range(self):
        assert self._validate_range(50, min_val=0, max_val=100) is True

    def test_at_min(self):
        assert self._validate_range(0, min_val=0, max_val=100) is True

    def test_at_max(self):
        assert self._validate_range(100, min_val=0, max_val=100) is True

    def test_below_min(self):
        assert self._validate_range(-1, min_val=0, max_val=100) is False

    def test_above_max(self):
        assert self._validate_range(101, min_val=0, max_val=100) is False

    def test_clamp_below(self):
        assert self._clamp(-10, 0, 100) == 0

    def test_clamp_above(self):
        assert self._clamp(150, 0, 100) == 100

    def test_clamp_within(self):
        assert self._clamp(50, 0, 100) == 50


class TestLengthValidation:
    """Tests for string length validation."""

    def _validate_length(self, value, min_len=None, max_len=None):
        """Validate string length."""
        if not isinstance(value, str):
            return False
        length = len(value)
        if min_len is not None and length < min_len:
            return False
        if max_len is not None and length > max_len:
            return False
        return True

    def test_valid_length(self):
        assert self._validate_length("hello", min_len=1, max_len=10) is True

    def test_too_short(self):
        assert self._validate_length("", min_len=1, max_len=10) is False

    def test_too_long(self):
        assert self._validate_length("a" * 20, min_len=1, max_len=10) is False

    def test_non_string(self):
        assert self._validate_length(123, min_len=1) is False


# --- Pattern validation patterns ---


class TestPatternValidation:
    """Tests for pattern-based validation."""

    def _validate_email(self, value):
        """Validate email format."""
        pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
        return bool(re.match(pattern, value))

    def _validate_url(self, value):
        """Validate URL format."""
        pattern = r"^https?://[^\s/$.?#].[^\s]*$"
        return bool(re.match(pattern, value))

    def _validate_identifier(self, value):
        """Validate identifier format (alphanumeric with underscores)."""
        pattern = r"^[a-zA-Z][a-zA-Z0-9_]*$"
        return bool(re.match(pattern, value))

    def test_valid_email(self):
        assert self._validate_email("user@example.com") is True

    def test_invalid_email_no_at(self):
        assert self._validate_email("userexample.com") is False

    def test_invalid_email_no_domain(self):
        assert self._validate_email("user@") is False

    def test_valid_url_http(self):
        assert self._validate_url("http://example.com") is True

    def test_valid_url_https(self):
        assert self._validate_url("https://example.com/path") is True

    def test_invalid_url(self):
        assert self._validate_url("not a url") is False

    def test_valid_identifier(self):
        assert self._validate_identifier("valid_name") is True

    def test_invalid_identifier_starts_digit(self):
        assert self._validate_identifier("123invalid") is False


# --- Enum validation patterns ---


class TestEnumValidation:
    """Tests for enum/choice validation."""

    def _validate_choice(self, value, choices):
        """Validate value is in choices."""
        return value in choices

    def _validate_choice_case_insensitive(self, value, choices):
        """Validate choice case-insensitively."""
        if not isinstance(value, str):
            return value in choices
        lower_value = value.lower()
        return any(
            c.lower() == lower_value if isinstance(c, str) else c == value
            for c in choices
        )

    def test_valid_choice(self):
        assert self._validate_choice("red", ["red", "green", "blue"]) is True

    def test_invalid_choice(self):
        assert (
            self._validate_choice("yellow", ["red", "green", "blue"]) is False
        )

    def test_case_insensitive_match(self):
        assert (
            self._validate_choice_case_insensitive("RED", ["red", "green"])
            is True
        )

    def test_case_insensitive_no_match(self):
        assert (
            self._validate_choice_case_insensitive("YELLOW", ["red", "green"])
            is False
        )


# --- Default value patterns ---


class TestDefaultValues:
    """Tests for default value handling."""

    def _get_with_default(self, config, key, default=None):
        """Get config value with default."""
        value = config.get(key)
        if value is None:
            return default
        return value

    def _apply_defaults(self, config, defaults):
        """Apply defaults to config."""
        result = dict(defaults)
        result.update({k: v for k, v in config.items() if v is not None})
        return result

    def test_returns_value_if_present(self):
        config = {"key": "value"}
        assert self._get_with_default(config, "key", "default") == "value"

    def test_returns_default_if_missing(self):
        config = {}
        assert self._get_with_default(config, "key", "default") == "default"

    def test_returns_default_if_none(self):
        config = {"key": None}
        assert self._get_with_default(config, "key", "default") == "default"

    def test_apply_defaults(self):
        config = {"a": 1}
        defaults = {"a": 0, "b": 2}
        result = self._apply_defaults(config, defaults)
        assert result["a"] == 1
        assert result["b"] == 2


# --- Nested config validation patterns ---


class TestNestedConfigValidation:
    """Tests for nested configuration validation."""

    def _validate_nested(self, config, schema):
        """Validate nested config against schema."""
        errors = []
        for key, rules in schema.items():
            if rules.get("required") and key not in config:
                errors.append(f"Missing required field: {key}")
                continue
            if key not in config:
                continue
            value = config[key]
            expected_type = rules.get("type")
            if expected_type == "dict" and isinstance(value, dict):
                nested_schema = rules.get("schema", {})
                nested_errors = self._validate_nested(value, nested_schema)
                errors.extend([f"{key}.{e}" for e in nested_errors])
            elif expected_type and not isinstance(
                value,
                {"str": str, "int": int, "bool": bool, "dict": dict}.get(
                    expected_type, type(None)
                ),
            ):
                errors.append(f"Invalid type for {key}")
        return errors

    def test_valid_nested(self):
        config = {"settings": {"enabled": True}}
        schema = {
            "settings": {
                "type": "dict",
                "schema": {"enabled": {"type": "bool"}},
            }
        }
        errors = self._validate_nested(config, schema)
        assert len(errors) == 0

    def test_missing_required(self):
        config = {}
        schema = {"name": {"required": True}}
        errors = self._validate_nested(config, schema)
        assert "Missing required field: name" in errors


class TestConfigMerging:
    """Tests for config merging patterns."""

    def _deep_merge(self, base, override):
        """Deep merge two configs."""
        result = dict(base)
        for key, value in override.items():
            if (
                key in result
                and isinstance(result[key], dict)
                and isinstance(value, dict)
            ):
                result[key] = self._deep_merge(result[key], value)
            else:
                result[key] = value
        return result

    def test_shallow_merge(self):
        base = {"a": 1, "b": 2}
        override = {"b": 3, "c": 4}
        result = self._deep_merge(base, override)
        assert result == {"a": 1, "b": 3, "c": 4}

    def test_deep_merge(self):
        base = {"outer": {"a": 1, "b": 2}}
        override = {"outer": {"b": 3}}
        result = self._deep_merge(base, override)
        assert result["outer"]["a"] == 1
        assert result["outer"]["b"] == 3


# --- Required field patterns ---


class TestRequiredFields:
    """Tests for required field validation."""

    def _check_required(self, config, required_fields):
        """Check that required fields are present."""
        missing = []
        for field in required_fields:
            if field not in config or config[field] is None:
                missing.append(field)
        return missing

    def test_all_present(self):
        config = {"name": "test", "value": 42}
        missing = self._check_required(config, ["name", "value"])
        assert len(missing) == 0

    def test_some_missing(self):
        config = {"name": "test"}
        missing = self._check_required(config, ["name", "value"])
        assert missing == ["value"]

    def test_none_treated_as_missing(self):
        config = {"name": None}
        missing = self._check_required(config, ["name"])
        assert missing == ["name"]


# --- Environment variable patterns ---


class TestEnvVarPatterns:
    """Tests for environment variable handling patterns."""

    def _expand_env_vars(self, config, env):
        """Expand environment variable references in config."""
        result = {}
        for key, value in config.items():
            if (
                isinstance(value, str)
                and value.startswith("${")
                and value.endswith("}")
            ):
                env_key = value[2:-1]
                result[key] = env.get(env_key, value)
            elif isinstance(value, dict):
                result[key] = self._expand_env_vars(value, env)
            else:
                result[key] = value
        return result

    def _build_env_var_name(self, prefix, key):
        """Build environment variable name from config key."""
        return f"{prefix}_{key}".upper().replace("-", "_")

    def test_expands_env_var(self):
        config = {"api_key": "${API_KEY}"}
        env = {"API_KEY": "secret123"}
        result = self._expand_env_vars(config, env)
        assert result["api_key"] == "secret123"

    def test_keeps_original_if_missing(self):
        config = {"api_key": "${MISSING_KEY}"}
        env = {}
        result = self._expand_env_vars(config, env)
        assert result["api_key"] == "${MISSING_KEY}"

    def test_env_var_name_building(self):
        assert self._build_env_var_name("APP", "api-key") == "APP_API_KEY"


# --- Validation result patterns ---


class TestValidationResult:
    """Tests for validation result building patterns."""

    def _create_validation_result(self, errors=None, warnings=None):
        """Create a validation result."""
        return {
            "valid": len(errors or []) == 0,
            "errors": errors or [],
            "warnings": warnings or [],
        }

    def _merge_results(self, *results):
        """Merge multiple validation results."""
        all_errors = []
        all_warnings = []
        for result in results:
            all_errors.extend(result.get("errors", []))
            all_warnings.extend(result.get("warnings", []))
        return self._create_validation_result(all_errors, all_warnings)

    def test_valid_with_no_errors(self):
        result = self._create_validation_result()
        assert result["valid"] is True

    def test_invalid_with_errors(self):
        result = self._create_validation_result(errors=["Error 1"])
        assert result["valid"] is False

    def test_merge_combines_errors(self):
        r1 = self._create_validation_result(errors=["E1"])
        r2 = self._create_validation_result(errors=["E2"])
        merged = self._merge_results(r1, r2)
        assert len(merged["errors"]) == 2


# --- Config file path patterns ---


class TestConfigPathPatterns:
    """Tests for config file path resolution patterns."""

    def _resolve_path(self, base_path, relative_path):
        """Resolve relative path against base."""
        if relative_path.startswith("/"):
            return relative_path
        return f"{base_path.rstrip('/')}/{relative_path}"

    def _get_config_locations(self, app_name):
        """Get standard config locations."""
        return [
            f"/etc/{app_name}/config.yaml",
            f"~/.config/{app_name}/config.yaml",
            f"./{app_name}.yaml",
            "./config.yaml",
        ]

    def test_absolute_path_unchanged(self):
        result = self._resolve_path("/base", "/absolute/path")
        assert result == "/absolute/path"

    def test_relative_path_resolved(self):
        result = self._resolve_path("/base", "relative/path")
        assert result == "/base/relative/path"

    def test_config_locations(self):
        locations = self._get_config_locations("myapp")
        assert len(locations) == 4
        assert "/etc/myapp/config.yaml" in locations


# --- Secret masking patterns ---


class TestSecretMasking:
    """Tests for secret value masking patterns."""

    def _mask_secrets(self, config, secret_keys):
        """Mask secret values in config."""
        result = {}
        for key, value in config.items():
            if key in secret_keys:
                if isinstance(value, str) and len(value) > 0:
                    result[key] = value[0] + "*" * (len(value) - 1)
                else:
                    result[key] = "***"
            elif isinstance(value, dict):
                result[key] = self._mask_secrets(value, secret_keys)
            else:
                result[key] = value
        return result

    def _is_secret_key(self, key):
        """Check if key name suggests a secret."""
        secret_patterns = ["password", "secret", "key", "token", "credential"]
        key_lower = key.lower()
        return any(p in key_lower for p in secret_patterns)

    def test_masks_password(self):
        config = {"password": "secret123"}
        result = self._mask_secrets(config, ["password"])
        assert result["password"] == "s********"

    def test_preserves_non_secret(self):
        config = {"name": "test", "password": "secret"}
        result = self._mask_secrets(config, ["password"])
        assert result["name"] == "test"

    def test_is_secret_key(self):
        assert self._is_secret_key("api_key") is True
        assert self._is_secret_key("password") is True
        assert self._is_secret_key("username") is False


# --- Config serialization patterns ---


class TestConfigSerialization:
    """Tests for config serialization patterns."""

    def _prepare_for_json(self, config):
        """Prepare config for JSON serialization."""
        result = {}
        for key, value in config.items():
            if isinstance(value, datetime):
                result[key] = value.isoformat()
            elif isinstance(value, set):
                result[key] = list(value)
            elif isinstance(value, dict):
                result[key] = self._prepare_for_json(value)
            else:
                result[key] = value
        return result

    def test_converts_datetime(self):
        config = {"updated": datetime(2025, 6, 15, 12, 0, tzinfo=timezone.utc)}
        result = self._prepare_for_json(config)
        assert isinstance(result["updated"], str)

    def test_converts_set(self):
        config = {"tags": {"a", "b", "c"}}
        result = self._prepare_for_json(config)
        assert isinstance(result["tags"], list)


# --- Config diff patterns ---


class TestConfigDiff:
    """Tests for config difference detection patterns."""

    def _get_changes(self, old_config, new_config):
        """Get changes between configs."""
        changes = {"added": {}, "removed": {}, "modified": {}}
        all_keys = set(old_config.keys()) | set(new_config.keys())
        for key in all_keys:
            if key not in old_config:
                changes["added"][key] = new_config[key]
            elif key not in new_config:
                changes["removed"][key] = old_config[key]
            elif old_config[key] != new_config[key]:
                changes["modified"][key] = {
                    "old": old_config[key],
                    "new": new_config[key],
                }
        return changes

    def test_detects_added(self):
        old = {"a": 1}
        new = {"a": 1, "b": 2}
        changes = self._get_changes(old, new)
        assert "b" in changes["added"]

    def test_detects_removed(self):
        old = {"a": 1, "b": 2}
        new = {"a": 1}
        changes = self._get_changes(old, new)
        assert "b" in changes["removed"]

    def test_detects_modified(self):
        old = {"a": 1}
        new = {"a": 2}
        changes = self._get_changes(old, new)
        assert "a" in changes["modified"]
        assert changes["modified"]["a"]["old"] == 1
        assert changes["modified"]["a"]["new"] == 2
