"""
Extended tests for environment settings type classes.

Tests cover:
- IntegerSetting min/max validation and edge cases
- PathSetting path expansion and validation
- EnumSetting case sensitivity and matching
- SecretSetting value hiding
- Base EnvSetting functionality
"""

import os
from pathlib import Path
import pytest

from local_deep_research.settings.env_settings import (
    BooleanSetting,
    StringSetting,
    IntegerSetting,
    PathSetting,
    SecretSetting,
    EnumSetting,
    SettingsRegistry,
)


class TestIntegerSettingValidation:
    """Tests for IntegerSetting min/max validation."""

    @pytest.fixture(autouse=True)
    def clean_env(self):
        """Clean environment before each test."""
        original_env = {
            k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_")
        }
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        yield
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        for key, value in original_env.items():
            os.environ[key] = value

    def test_integer_setting_min_value_enforcement(self):
        """Test that values below min raise ValueError."""
        setting = IntegerSetting(
            key="test.min_value",
            description="Test setting",
            default=10,
            min_value=5,
            max_value=100,
        )

        os.environ["LDR_TEST_MIN_VALUE"] = "3"  # Below min

        with pytest.raises(ValueError) as exc_info:
            setting.get_value()

        assert "below minimum" in str(exc_info.value)

    def test_integer_setting_max_value_enforcement(self):
        """Test that values above max raise ValueError."""
        setting = IntegerSetting(
            key="test.max_value",
            description="Test setting",
            default=10,
            min_value=5,
            max_value=100,
        )

        os.environ["LDR_TEST_MAX_VALUE"] = "200"  # Above max

        with pytest.raises(ValueError) as exc_info:
            setting.get_value()

        assert "above maximum" in str(exc_info.value)

    def test_integer_setting_invalid_value_uses_default(self):
        """Test that non-numeric values fall back to default."""
        setting = IntegerSetting(
            key="test.invalid", description="Test setting", default=42
        )

        os.environ["LDR_TEST_INVALID"] = "not_a_number"

        result = setting.get_value()

        assert result == 42

    def test_integer_setting_float_string_truncates(self):
        """Test that float strings are converted to int (truncated)."""
        setting = IntegerSetting(
            key="test.float", description="Test setting", default=0
        )

        os.environ["LDR_TEST_FLOAT"] = "3.7"

        # This should fail since int("3.7") raises ValueError
        # The implementation should handle this as invalid
        result = setting.get_value()

        # Expect default since float string is invalid for int()
        assert result == 0

    def test_integer_setting_empty_string_uses_default(self):
        """Test that empty string uses default value."""
        setting = IntegerSetting(
            key="test.empty", description="Test setting", default=99
        )

        os.environ["LDR_TEST_EMPTY"] = ""

        result = setting.get_value()

        # Empty string is invalid, should use default
        assert result == 99

    def test_integer_setting_valid_value_in_range(self):
        """Test that valid value within range is returned."""
        setting = IntegerSetting(
            key="test.valid",
            description="Test setting",
            default=10,
            min_value=5,
            max_value=100,
        )

        os.environ["LDR_TEST_VALID"] = "50"

        result = setting.get_value()

        assert result == 50

    def test_integer_setting_boundary_values(self):
        """Test that boundary values are accepted."""
        setting = IntegerSetting(
            key="test.boundary",
            description="Test setting",
            default=10,
            min_value=5,
            max_value=100,
        )

        # Test min boundary
        os.environ["LDR_TEST_BOUNDARY"] = "5"
        assert setting.get_value() == 5

        # Test max boundary
        os.environ["LDR_TEST_BOUNDARY"] = "100"
        assert setting.get_value() == 100

    def test_integer_setting_negative_value(self):
        """Test that negative values work correctly."""
        setting = IntegerSetting(
            key="test.negative",
            description="Test setting",
            default=0,
            min_value=-100,
            max_value=100,
        )

        os.environ["LDR_TEST_NEGATIVE"] = "-50"

        result = setting.get_value()

        assert result == -50


class TestPathSettingValidation:
    """Tests for PathSetting path expansion and validation."""

    @pytest.fixture(autouse=True)
    def clean_env(self):
        """Clean environment before each test."""
        original_env = {
            k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_")
        }
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        yield
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        for key, value in original_env.items():
            os.environ[key] = value

    def test_path_setting_expands_tilde(self):
        """Test that ~/path is expanded to full home path."""
        setting = PathSetting(
            key="test.tilde_path", description="Test setting", default=None
        )

        os.environ["LDR_TEST_TILDE_PATH"] = "~/test_dir"

        result = setting.get_value()

        assert result.startswith(str(Path.home()))
        assert "test_dir" in result

    def test_path_setting_expands_env_vars(self):
        """Test that $HOME/path is expanded to the correct absolute path."""
        setting = PathSetting(
            key="test.env_path", description="Test setting", default=None
        )

        os.environ["LDR_TEST_ENV_PATH"] = "$HOME/test_env_dir"

        result = setting.get_value()

        expected = str(Path(os.environ["HOME"]) / "test_env_dir")
        assert result == expected

    def test_path_setting_create_if_missing(self, tmp_path):
        """Test that directory is created when create_if_missing=True."""
        test_dir = tmp_path / "new_directory"

        setting = PathSetting(
            key="test.create_path",
            description="Test setting",
            default=None,
            create_if_missing=True,
        )

        os.environ["LDR_TEST_CREATE_PATH"] = str(test_dir)

        result = setting.get_value()

        assert test_dir.exists()
        assert result == str(test_dir)

    def test_path_setting_must_exist_raises(self, tmp_path):
        """Test that ValueError is raised when path doesn't exist and must_exist=True."""
        nonexistent_path = tmp_path / "nonexistent_dir"

        setting = PathSetting(
            key="test.must_exist",
            description="Test setting",
            default=None,
            must_exist=True,
        )

        os.environ["LDR_TEST_MUST_EXIST"] = str(nonexistent_path)

        with pytest.raises(ValueError) as exc_info:
            setting.get_value()

        assert "does not exist" in str(exc_info.value)

    def test_path_setting_none_returns_none(self):
        """Test that unset path returns None."""
        setting = PathSetting(
            key="test.unset_path", description="Test setting", default=None
        )

        # Don't set the env var
        result = setting.get_value()

        assert result is None

    def test_path_setting_absolute_path_unchanged(self, tmp_path):
        """Test that absolute path is returned as-is."""
        setting = PathSetting(
            key="test.absolute", description="Test setting", default=None
        )

        os.environ["LDR_TEST_ABSOLUTE"] = str(tmp_path)

        result = setting.get_value()

        assert result == str(tmp_path)

    def test_path_setting_default_value(self):
        """Test that default value is used when env var not set."""
        setting = PathSetting(
            key="test.default",
            description="Test setting",
            default="/default/path",
        )

        result = setting.get_value()

        assert "/default/path" in result


class TestEnumSettingValidation:
    """Tests for EnumSetting case sensitivity and matching."""

    @pytest.fixture(autouse=True)
    def clean_env(self):
        """Clean environment before each test."""
        original_env = {
            k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_")
        }
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        yield
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        for key, value in original_env.items():
            os.environ[key] = value

    def test_enum_setting_case_insensitive(self):
        """Test that matching is case-insensitive by default."""
        setting = EnumSetting(
            key="test.enum_ci",
            description="Test setting",
            allowed_values={"DEBUG", "INFO", "WARNING"},
            default="INFO",
            case_sensitive=False,
        )

        os.environ["LDR_TEST_ENUM_CI"] = "debug"

        result = setting.get_value()

        assert result == "DEBUG"  # Returns canonical form

    def test_enum_setting_case_sensitive(self):
        """Test that case-sensitive matching works."""
        setting = EnumSetting(
            key="test.enum_cs",
            description="Test setting",
            allowed_values={"DEBUG", "INFO", "WARNING"},
            default="INFO",
            case_sensitive=True,
        )

        os.environ["LDR_TEST_ENUM_CS"] = "debug"  # lowercase

        with pytest.raises(ValueError) as exc_info:
            setting.get_value()

        assert "not in allowed values" in str(exc_info.value)

    def test_enum_setting_canonical_form(self):
        """Test that returned value is in canonical form."""
        setting = EnumSetting(
            key="test.enum_canon",
            description="Test setting",
            allowed_values={"WAL", "TRUNCATE", "DELETE"},
            default="WAL",
            case_sensitive=False,
        )

        os.environ["LDR_TEST_ENUM_CANON"] = "wal"

        result = setting.get_value()

        # Should return uppercase canonical form
        assert result == "WAL"

    def test_enum_setting_invalid_uses_default(self):
        """Test that invalid values return default via registry."""
        setting = EnumSetting(
            key="test.enum_invalid",
            description="Test setting",
            allowed_values={"A", "B", "C"},
            default="A",
        )

        os.environ["LDR_TEST_ENUM_INVALID"] = "X"  # Not in allowed

        # Direct get_value raises, but through registry returns default
        with pytest.raises(ValueError):
            setting.get_value()

    def test_enum_setting_valid_value(self):
        """Test that valid value is accepted."""
        setting = EnumSetting(
            key="test.enum_valid",
            description="Test setting",
            allowed_values={"OPTION1", "OPTION2", "OPTION3"},
            default="OPTION1",
        )

        os.environ["LDR_TEST_ENUM_VALID"] = "OPTION2"

        result = setting.get_value()

        assert result == "OPTION2"

    def test_enum_setting_default_when_not_set(self):
        """Test that default is used when env var not set."""
        setting = EnumSetting(
            key="test.enum_default",
            description="Test setting",
            allowed_values={"X", "Y", "Z"},
            default="Y",
        )

        result = setting.get_value()

        assert result == "Y"


class TestSecretSettingHiding:
    """Tests for SecretSetting value hiding."""

    @pytest.fixture(autouse=True)
    def clean_env(self):
        """Clean environment before each test."""
        original_env = {
            k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_")
        }
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        yield
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        for key, value in original_env.items():
            os.environ[key] = value

    def test_secret_setting_repr_hides_value(self):
        """Test that repr() hides the actual value."""
        setting = SecretSetting(
            key="test.secret_repr", description="Test setting", default=None
        )

        os.environ["LDR_TEST_SECRET_REPR"] = "super_secret_value"

        repr_str = repr(setting)

        assert "super_secret_value" not in repr_str
        assert "***" in repr_str

    def test_secret_setting_str_hides_value(self):
        """Test that str() hides the actual value."""
        setting = SecretSetting(
            key="test.secret_str", description="Test setting", default=None
        )

        os.environ["LDR_TEST_SECRET_STR"] = "super_secret_value"

        str_result = str(setting)

        assert "super_secret_value" not in str_result
        assert "SET" in str_result

    def test_secret_setting_get_value_returns_actual(self):
        """Test that get_value() returns the actual secret."""
        setting = SecretSetting(
            key="test.secret_get", description="Test setting", default=None
        )

        os.environ["LDR_TEST_SECRET_GET"] = "actual_secret_value"

        result = setting.get_value()

        assert result == "actual_secret_value"

    def test_secret_setting_unset_shows_not_set(self):
        """Test that str() shows NOT SET when unset."""
        setting = SecretSetting(
            key="test.secret_unset", description="Test setting", default=None
        )

        str_result = str(setting)

        assert "NOT SET" in str_result


class TestBaseEnvSetting:
    """Tests for base EnvSetting functionality."""

    @pytest.fixture(autouse=True)
    def clean_env(self):
        """Clean environment before each test."""
        original_env = {
            k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_")
        }
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        yield
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        for key, value in original_env.items():
            os.environ[key] = value

    def test_env_setting_env_var_auto_generation(self):
        """Test that env_var is auto-generated from key."""
        setting = BooleanSetting(
            key="test.nested.setting", description="Test setting", default=False
        )

        assert setting.env_var == "LDR_TEST_NESTED_SETTING"

    def test_env_setting_is_set_property_true(self):
        """Test is_set returns True when env var is set."""
        setting = BooleanSetting(
            key="test.is_set", description="Test setting", default=False
        )

        os.environ["LDR_TEST_IS_SET"] = "true"

        assert setting.is_set is True

    def test_env_setting_is_set_property_false(self):
        """Test is_set returns False when env var is not set."""
        setting = BooleanSetting(
            key="test.not_set", description="Test setting", default=False
        )

        assert setting.is_set is False

    def test_env_setting_required_raises_when_missing(self):
        """Test that required setting raises when not set."""
        setting = StringSetting(
            key="test.required",
            description="Test setting",
            default=None,
            required=True,
        )

        with pytest.raises(ValueError) as exc_info:
            setting.get_value()

        assert "Required environment variable" in str(exc_info.value)

    def test_env_setting_repr(self):
        """Test __repr__ method."""
        setting = BooleanSetting(
            key="test.repr", description="Test setting", default=False
        )

        repr_str = repr(setting)

        assert "BooleanSetting" in repr_str
        assert "test.repr" in repr_str
        assert "LDR_TEST_REPR" in repr_str


class TestBooleanSettingConversion:
    """Tests for BooleanSetting value conversion."""

    @pytest.fixture(autouse=True)
    def clean_env(self):
        """Clean environment before each test."""
        original_env = {
            k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_")
        }
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        yield
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        for key, value in original_env.items():
            os.environ[key] = value

    @pytest.mark.parametrize(
        "value", ["true", "True", "TRUE", "1", "yes", "on", "enabled"]
    )
    def test_boolean_setting_truthy_values(self, value):
        """Test that truthy values convert to True."""
        setting = BooleanSetting(
            key="test.bool", description="Test setting", default=False
        )

        os.environ["LDR_TEST_BOOL"] = value

        assert setting.get_value() is True

    @pytest.mark.parametrize(
        "value", ["false", "False", "FALSE", "0", "no", "off", ""]
    )
    def test_boolean_setting_falsy_values(self, value):
        """Test that falsy values convert to False."""
        setting = BooleanSetting(
            key="test.bool_false", description="Test setting", default=True
        )

        os.environ["LDR_TEST_BOOL_FALSE"] = value

        assert setting.get_value() is False


class TestSettingsRegistryExtended:
    """Extended tests for SettingsRegistry functionality."""

    @pytest.fixture(autouse=True)
    def clean_env(self):
        """Clean environment before each test."""
        original_env = {
            k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_")
        }
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        yield
        for key in list(os.environ.keys()):
            if key.startswith("LDR_TEST_"):
                os.environ.pop(key, None)
        for key, value in original_env.items():
            os.environ[key] = value

    def test_registry_register_category(self):
        """Test registering a category of settings."""
        registry = SettingsRegistry()

        settings = [
            BooleanSetting("cat1.setting1", "Test 1", default=False),
            BooleanSetting("cat1.setting2", "Test 2", default=True),
        ]

        registry.register_category("cat1", settings)

        assert "cat1.setting1" in registry.list_all_settings()
        assert "cat1.setting2" in registry.list_all_settings()

    def test_registry_get_returns_default_for_unknown(self):
        """Test that get() returns default for unknown keys."""
        registry = SettingsRegistry()

        result = registry.get("unknown.key", default="fallback")

        assert result == "fallback"

    def test_registry_get_setting_object(self):
        """Test get_setting_object returns the EnvSetting instance."""
        registry = SettingsRegistry()
        setting = BooleanSetting("test.obj", "Test", default=False)
        registry.register_category("test", [setting])

        result = registry.get_setting_object("test.obj")

        assert result is setting

    def test_registry_is_env_only(self):
        """Test is_env_only returns correct values."""
        registry = SettingsRegistry()
        setting = BooleanSetting("test.env_only", "Test", default=False)
        registry.register_category("test", [setting])

        assert registry.is_env_only("test.env_only") is True
        assert registry.is_env_only("unknown.key") is False

    def test_registry_get_env_var(self):
        """Test get_env_var returns correct env var name."""
        registry = SettingsRegistry()
        setting = BooleanSetting("test.env_var", "Test", default=False)
        registry.register_category("test", [setting])

        result = registry.get_env_var("test.env_var")

        assert result == "LDR_TEST_ENV_VAR"

    def test_registry_get_all_env_vars(self):
        """Test get_all_env_vars returns all registered env vars."""
        registry = SettingsRegistry()
        settings = [
            BooleanSetting("test.s1", "Desc 1", default=False),
            StringSetting("test.s2", "Desc 2", default="val"),
        ]
        registry.register_category("test", settings)

        result = registry.get_all_env_vars()

        assert "LDR_TEST_S1" in result
        assert "LDR_TEST_S2" in result
        assert result["LDR_TEST_S1"] == "Desc 1"

    def test_registry_list_all_settings(self):
        """Test list_all_settings returns all registered keys."""
        registry = SettingsRegistry()
        settings = [
            BooleanSetting("test.a", "A", default=False),
            BooleanSetting("test.b", "B", default=False),
        ]
        registry.register_category("test", settings)

        result = registry.list_all_settings()

        assert "test.a" in result
        assert "test.b" in result
        assert len(result) == 2
