"""Unit tests for the feature flags system."""

import json
import os
import tempfile
from pathlib import Path
from unittest.mock import patch

import pytest

from augint_library.exceptions import ConfigurationError, ValidationError
from augint_library.feature_flags import FeatureFlag, FeatureFlags, feature_flag, get_flags


class TestFeatureFlag:
    """Test the FeatureFlag class."""

    def test_init_default_values(self) -> None:
        """Test initialization with default values."""
        flag = FeatureFlag("test_flag")
        assert flag.name == "test_flag"
        assert flag.default == "disabled"
        assert flag.state == "disabled"
        assert flag.description is None
        assert flag.metadata == {}
        assert flag.evaluator is None

    def test_init_with_values(self) -> None:
        """Test initialization with custom values."""

        def custom_evaluator(context):
            return True

        flag = FeatureFlag(
            "test_flag",
            default="enabled",
            description="Test flag",
            metadata={"version": "1.0"},
            evaluator=custom_evaluator,
        )
        assert flag.name == "test_flag"
        assert flag.default == "enabled"
        assert flag.state == "enabled"
        assert flag.description == "Test flag"
        assert flag.metadata == {"version": "1.0"}
        assert flag.evaluator == custom_evaluator

    def test_validate_name_valid(self) -> None:
        """Test valid flag names."""
        valid_names = ["feature1", "feature_2", "feature-3", "FEATURE_4", "f"]
        for name in valid_names:
            flag = FeatureFlag(name)
            assert flag.name == name

    def test_validate_name_invalid(self) -> None:
        """Test invalid flag names."""
        invalid_names = ["feature.1", "feature 2", "feature@3", "", "feature/4"]
        for name in invalid_names:
            with pytest.raises(ValidationError, match="Invalid flag name"):
                FeatureFlag(name)

    def test_validate_state_valid(self) -> None:
        """Test valid states."""
        valid_states = [
            "enabled",
            "disabled",
            "conditional",
            "percentage:0",
            "percentage:50",
            "percentage:100",
        ]
        for state in valid_states:
            flag = FeatureFlag("test", default=state)
            assert flag.state == state

    def test_validate_state_invalid(self) -> None:
        """Test invalid states."""
        invalid_states = [
            "on",
            "off",
            "percentage",
            "percentage:101",
            "percentage:-1",
            "percentage:abc",
        ]
        for state in invalid_states:
            with pytest.raises(ValidationError, match="Invalid state"):
                FeatureFlag("test", default=state)

    def test_evaluate_enabled(self) -> None:
        """Test evaluation of enabled state."""
        flag = FeatureFlag("test", default="enabled")
        assert flag.evaluate() is True
        assert flag.evaluate({"user_id": "123"}) is True

    def test_evaluate_disabled(self) -> None:
        """Test evaluation of disabled state."""
        flag = FeatureFlag("test", default="disabled")
        assert flag.evaluate() is False
        assert flag.evaluate({"user_id": "123"}) is False

    def test_evaluate_percentage_deterministic(self) -> None:
        """Test percentage evaluation is deterministic."""
        flag = FeatureFlag("test", default="percentage:50")

        # Same user should always get same result
        user1_results = [flag.evaluate({"user_id": "user1"}) for _ in range(10)]
        assert all(r == user1_results[0] for r in user1_results)

        user2_results = [flag.evaluate({"user_id": "user2"}) for _ in range(10)]
        assert all(r == user2_results[0] for r in user2_results)

    def test_evaluate_percentage_distribution(self) -> None:
        """Test percentage distribution across users."""
        flag = FeatureFlag("test", default="percentage:30")

        # Test with many users
        enabled_count = sum(flag.evaluate({"user_id": f"user_{i}"}) for i in range(1000))

        # Should be roughly 30% enabled (allow some variance)
        assert 250 < enabled_count < 350

    def test_evaluate_percentage_edge_cases(self) -> None:
        """Test percentage edge cases."""
        flag0 = FeatureFlag("test0", default="percentage:0")
        flag100 = FeatureFlag("test100", default="percentage:100")

        # 0% should always be disabled
        for i in range(100):
            assert flag0.evaluate({"user_id": f"user_{i}"}) is False

        # 100% should always be enabled
        for i in range(100):
            assert flag100.evaluate({"user_id": f"user_{i}"}) is True

    def test_evaluate_conditional_with_evaluator(self) -> None:
        """Test conditional evaluation with evaluator."""

        def is_premium_user(context):
            return context.get("subscription") == "premium"

        flag = FeatureFlag("test", default="conditional", evaluator=is_premium_user)

        assert flag.evaluate({"subscription": "premium"}) is True
        assert flag.evaluate({"subscription": "free"}) is False
        assert flag.evaluate({}) is False

    def test_evaluate_conditional_without_evaluator(self) -> None:
        """Test conditional evaluation without evaluator."""
        flag = FeatureFlag("test", default="conditional")
        assert flag.evaluate() is False

    def test_evaluate_conditional_with_error(self) -> None:
        """Test conditional evaluation with evaluator error."""

        def failing_evaluator(context):
            raise ValueError("Evaluator error")

        flag = FeatureFlag("test", default="conditional", evaluator=failing_evaluator)
        assert flag.evaluate() is False

    def test_override_state(self) -> None:
        """Test temporary state override."""
        flag = FeatureFlag("test", default="disabled")
        assert flag.evaluate() is False

        # Override to enabled
        flag._override_state = "enabled"
        assert flag.evaluate() is True

        # Clear override
        flag._override_state = None
        assert flag.evaluate() is False


class TestFeatureFlags:
    """Test the FeatureFlags class."""

    def setup_method(self) -> None:
        """Reset singleton before each test."""
        FeatureFlags._instance = None

    def test_singleton(self) -> None:
        """Test singleton pattern."""
        flags1 = FeatureFlags()
        flags2 = FeatureFlags()
        assert flags1 is flags2

    def test_register_flag(self) -> None:
        """Test registering a flag."""
        flags = FeatureFlags()
        flags.register("test_flag", default="enabled", description="Test flag")

        assert "test_flag" in flags._flags
        flag = flags._flags["test_flag"]
        assert flag.name == "test_flag"
        assert flag.default == "enabled"
        assert flag.description == "Test flag"

    def test_register_with_evaluator(self) -> None:
        """Test registering a flag with evaluator."""
        flags = FeatureFlags()

        def custom_evaluator(context):
            return True

        flags.register_evaluator("custom", custom_evaluator)
        flags.register("test_flag", default="conditional", evaluator="custom")

        assert flags.is_enabled("test_flag") is True

    def test_register_with_unknown_evaluator(self) -> None:
        """Test registering with unknown evaluator."""
        flags = FeatureFlags()

        with pytest.raises(ConfigurationError, match="Evaluator 'unknown' not registered"):
            flags.register("test_flag", evaluator="unknown")

    def test_is_enabled(self) -> None:
        """Test is_enabled method."""
        flags = FeatureFlags()
        flags.register("enabled_flag", default="enabled")
        flags.register("disabled_flag", default="disabled")

        assert flags.is_enabled("enabled_flag") is True
        assert flags.is_enabled("disabled_flag") is False

    def test_is_enabled_unknown_flag(self) -> None:
        """Test is_enabled with unknown flag."""
        flags = FeatureFlags()
        assert flags.is_enabled("unknown_flag") is False

    def test_set_state(self) -> None:
        """Test setting flag state."""
        flags = FeatureFlags()
        flags.register("test_flag", default="disabled")

        assert flags.is_enabled("test_flag") is False

        flags.set_state("test_flag", "enabled")
        assert flags.is_enabled("test_flag") is True

    def test_set_state_unknown_flag(self) -> None:
        """Test setting state of unknown flag."""
        flags = FeatureFlags()

        with pytest.raises(ValidationError, match="Unknown feature flag"):
            flags.set_state("unknown_flag", "enabled")

    def test_get_state(self) -> None:
        """Test getting flag state."""
        flags = FeatureFlags()
        flags.register("test_flag", default="percentage:50")

        assert flags.get_state("test_flag") == "percentage:50"

    def test_get_state_unknown_flag(self) -> None:
        """Test getting state of unknown flag."""
        flags = FeatureFlags()

        with pytest.raises(ValidationError, match="Unknown feature flag"):
            flags.get_state("unknown_flag")

    def test_get_all_flags(self) -> None:
        """Test getting all flags."""
        flags = FeatureFlags()
        flags.register("flag1", default="enabled", description="Flag 1")
        flags.register("flag2", default="disabled", metadata={"version": "2.0"})

        all_flags = flags.get_all_flags()
        assert len(all_flags) == 2

        assert all_flags["flag1"]["state"] == "enabled"
        assert all_flags["flag1"]["default"] == "enabled"
        assert all_flags["flag1"]["description"] == "Flag 1"

        assert all_flags["flag2"]["state"] == "disabled"
        assert all_flags["flag2"]["metadata"] == {"version": "2.0"}

    def test_override_context_manager(self) -> None:
        """Test override context manager."""
        flags = FeatureFlags()
        flags.register("test_flag", default="disabled")

        assert flags.is_enabled("test_flag") is False

        with flags.override("test_flag", "enabled"):
            assert flags.is_enabled("test_flag") is True

        assert flags.is_enabled("test_flag") is False

    def test_override_unknown_flag(self) -> None:
        """Test override with unknown flag."""
        flags = FeatureFlags()

        with (
            pytest.raises(ValidationError, match="Unknown feature flag"),
            flags.override("unknown_flag", "enabled"),
        ):
            pass

    def test_load_from_file(self) -> None:
        """Test loading configuration from file."""
        config = {
            "simple_flag": "enabled",
            "complex_flag": {
                "state": "percentage:75",
                "description": "Complex flag",
                "metadata": {"owner": "team-a"},
            },
        }

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(config, f)
            f.flush()

            flags = FeatureFlags()
            flags.load_from_file(f.name)

        Path(f.name).unlink()

        assert flags.get_state("simple_flag") == "enabled"
        assert flags.get_state("complex_flag") == "percentage:75"

        all_flags = flags.get_all_flags()
        assert all_flags["complex_flag"]["description"] == "Complex flag"
        assert all_flags["complex_flag"]["metadata"] == {"owner": "team-a"}

    def test_load_from_file_not_found(self) -> None:
        """Test loading from non-existent file."""
        flags = FeatureFlags()

        with pytest.raises(ConfigurationError, match="Configuration file not found"):
            flags.load_from_file("/non/existent/file.json")

    def test_load_from_file_invalid_json(self) -> None:
        """Test loading from invalid JSON file."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            f.write("invalid json{")
            f.flush()

            flags = FeatureFlags()
            with pytest.raises(ConfigurationError, match="Invalid JSON"):
                flags.load_from_file(f.name)

        Path(f.name).unlink()

    def test_load_from_env(self) -> None:
        """Test loading from environment variables."""
        with patch.dict(
            os.environ,
            {
                "FEATURE_FLAG_NEW_UI": "enabled",
                "FEATURE_FLAG_BETA_API": "percentage:25",
                "FEATURE_FLAG_DEBUG_MODE": "disabled",
                "OTHER_ENV_VAR": "ignored",
            },
        ):
            flags = FeatureFlags()
            flags.load_from_env()

            assert flags.is_enabled("new_ui") is True
            assert flags.get_state("beta_api") == "percentage:25"
            assert flags.is_enabled("debug_mode") is False

    def test_load_from_env_invalid_state(self) -> None:
        """Test loading from env with invalid state."""
        with patch.dict(os.environ, {"FEATURE_FLAG_INVALID": "invalid_state"}):
            flags = FeatureFlags()
            flags.load_from_env()

            # Invalid state should be ignored
            assert "invalid" not in flags._flags

    def test_auto_load_env(self) -> None:
        """Test automatic environment loading."""
        with patch.dict(os.environ, {"FEATURE_FLAG_AUTO_LOADED": "enabled"}):
            # Create new instance to test auto-loading
            FeatureFlags._instance = None
            flags = FeatureFlags(auto_load_env=True)

            assert flags.is_enabled("auto_loaded") is True

    def test_no_auto_load_env(self) -> None:
        """Test disabling automatic environment loading."""
        with patch.dict(os.environ, {"FEATURE_FLAG_NOT_LOADED": "enabled"}):
            # Create new instance
            FeatureFlags._instance = None
            flags = FeatureFlags(auto_load_env=False)

            assert "not_loaded" not in flags._flags


class TestFeatureFlagDecorator:
    """Test the feature_flag decorator."""

    def setup_method(self) -> None:
        """Reset singleton before each test."""
        FeatureFlags._instance = None

    def test_decorator_enabled(self) -> None:
        """Test decorator with enabled flag."""
        flags = FeatureFlags()
        flags.register("new_feature", default="enabled")

        @feature_flag("new_feature", flags=flags)
        def new_functionality():
            return "new"

        assert new_functionality() == "new"

    def test_decorator_disabled(self) -> None:
        """Test decorator with disabled flag."""
        flags = FeatureFlags()
        flags.register("new_feature", default="disabled")

        @feature_flag("new_feature", flags=flags)
        def new_functionality():
            return "new"

        assert new_functionality() is None

    def test_decorator_with_fallback_value(self) -> None:
        """Test decorator with fallback value."""
        flags = FeatureFlags()
        flags.register("new_feature", default="disabled")

        @feature_flag("new_feature", fallback="old", flags=flags)
        def new_functionality():
            return "new"

        assert new_functionality() == "old"

    def test_decorator_with_fallback_function(self) -> None:
        """Test decorator with fallback function."""
        flags = FeatureFlags()
        flags.register("new_feature", default="disabled")

        def old_functionality():
            return "old"

        @feature_flag("new_feature", fallback=old_functionality, flags=flags)
        def new_functionality():
            return "new"

        assert new_functionality() == "old"

    def test_decorator_with_args(self) -> None:
        """Test decorator with function arguments."""
        flags = FeatureFlags()
        flags.register("new_feature", default="enabled")

        @feature_flag("new_feature", flags=flags)
        def process_data(x, y):
            return x + y

        assert process_data(1, 2) == 3

    def test_decorator_with_context(self) -> None:
        """Test decorator with feature context."""
        flags = FeatureFlags()

        def is_premium(context):
            return context.get("tier") == "premium"

        flags.register_evaluator("premium_check", is_premium)
        flags.register("premium_feature", default="conditional", evaluator="premium_check")

        @feature_flag("premium_feature", fallback="basic", flags=flags)
        def get_features():
            return "premium"

        # Without context
        assert get_features() == "basic"

        # With context
        assert get_features(_feature_context={"tier": "premium"}) == "premium"
        assert get_features(_feature_context={"tier": "free"}) == "basic"

    def test_decorator_preserves_metadata(self) -> None:
        """Test decorator preserves function metadata."""
        flags = FeatureFlags()
        flags.register("test", default="enabled")

        @feature_flag("test", flags=flags)
        def documented_function():
            """This is a documented function."""
            return "result"

        assert documented_function.__name__ == "documented_function"
        assert documented_function.__doc__ == "This is a documented function."
        assert hasattr(documented_function, "__wrapped__")

    def test_decorator_uses_global_flags(self) -> None:
        """Test decorator uses global flags when not specified."""
        # Reset global instance
        FeatureFlags._instance = None
        global_flags = get_flags()
        global_flags.register("global_feature", default="enabled")

        @feature_flag("global_feature")
        def global_function():
            return "global"

        assert global_function() == "global"


class TestIntegration:
    """Integration tests for feature flags system."""

    def setup_method(self) -> None:
        """Reset singleton before each test."""
        FeatureFlags._instance = None

    def test_full_workflow(self) -> None:
        """Test complete feature flags workflow."""
        # Reset singleton
        FeatureFlags._instance = None

        # Create and configure flags
        flags = FeatureFlags()

        # Register evaluator
        def is_beta_user(context):
            return context.get("user_id", "").startswith("beta_")

        flags.register_evaluator("beta_check", is_beta_user)

        # Register flags
        flags.register("ui_redesign", default="percentage:50", description="New UI rollout")
        flags.register("beta_features", default="conditional", evaluator="beta_check")
        flags.register("maintenance_mode", default="disabled")

        # Check initial states
        assert flags.get_state("ui_redesign") == "percentage:50"
        assert flags.get_state("beta_features") == "conditional"
        assert flags.get_state("maintenance_mode") == "disabled"

        # Test percentage rollout
        user_a_enabled = flags.is_enabled("ui_redesign", {"user_id": "user_a"})
        flags.is_enabled("ui_redesign", {"user_id": "user_b"})
        # At least one should be different with 50% rollout

        # Test conditional
        assert flags.is_enabled("beta_features", {"user_id": "beta_tester"}) is True
        assert flags.is_enabled("beta_features", {"user_id": "regular_user"}) is False

        # Runtime state change
        flags.set_state("maintenance_mode", "enabled")
        assert flags.is_enabled("maintenance_mode") is True

        # Temporary override
        with flags.override("ui_redesign", "disabled"):
            assert flags.is_enabled("ui_redesign", {"user_id": "user_a"}) is False

        # Override reverted
        assert flags.is_enabled("ui_redesign", {"user_id": "user_a"}) == user_a_enabled

    def test_thread_safety(self) -> None:
        """Test basic thread safety of singleton."""
        import threading

        instances = []

        def create_instance():
            instances.append(FeatureFlags())

        threads = [threading.Thread(target=create_instance) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All instances should be the same
        assert all(inst is instances[0] for inst in instances)
