"""
tests/test_feature_flags.py — Tests for FeatureFlagManager (#368).

All tests are self-contained (no external dependencies, no DB, no LLM).
They exercise the full public API of FeatureFlagManager:

    - Default flags all present
    - is_enabled returns correct value
    - Rollout percentage: deterministic bucketing
    - Same (flag, owner) always gets same result
    - Disabled flag returns false regardless of rollout
    - Unknown flag returns false (safe default)
    - Config overrides
    - Feature flags system disabled → everything enabled
"""
from __future__ import annotations

import pytest

from cortex.config import CortexConfig
from cortex.core.feature_flags import (
    DEFAULT_FLAGS,
    FeatureFlag,
    FeatureFlagManager,
    _hash_bucket,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def config() -> CortexConfig:
    return CortexConfig()


@pytest.fixture
def manager(config: CortexConfig) -> FeatureFlagManager:
    return FeatureFlagManager(config=config)


# ---------------------------------------------------------------------------
# Default flags
# ---------------------------------------------------------------------------

class TestDefaultFlags:
    def test_all_default_flags_present(self, manager: FeatureFlagManager):
        all_flags = manager.get_all()
        expected = {
            "working_memory", "task_mode_routing", "context_compiler",
            "boot_loader", "re_retrieval", "promotion_scoring",
            "model_routing", "sleep_pipeline", "dream_cycle", "curiosity_seeds",
        }
        assert set(all_flags.keys()) == expected

    def test_default_flag_count(self):
        assert len(DEFAULT_FLAGS) == 10

    def test_full_rollout_flags(self, manager: FeatureFlagManager):
        """Flags with rollout_pct=1.0 should always be enabled."""
        full_rollout = [
            "working_memory", "task_mode_routing", "context_compiler",
            "boot_loader", "re_retrieval", "promotion_scoring",
            "model_routing", "sleep_pipeline",
        ]
        for flag_name in full_rollout:
            assert manager.is_enabled(flag_name) is True
            assert manager.is_enabled(flag_name, owner_id="any-owner") is True

    def test_gradual_rollout_flags(self, manager: FeatureFlagManager):
        """Flags with rollout_pct=0.5 should exist and have correct pct."""
        flags = manager.get_all()
        assert flags["dream_cycle"].rollout_pct == 0.5
        assert flags["curiosity_seeds"].rollout_pct == 0.5


# ---------------------------------------------------------------------------
# is_enabled basic behavior
# ---------------------------------------------------------------------------

class TestIsEnabled:
    def test_enabled_flag_returns_true(self, manager: FeatureFlagManager):
        assert manager.is_enabled("working_memory") is True

    def test_unknown_flag_returns_false(self, manager: FeatureFlagManager):
        assert manager.is_enabled("nonexistent_flag") is False

    def test_unknown_flag_with_owner(self, manager: FeatureFlagManager):
        assert manager.is_enabled("nonexistent_flag", owner_id="user-1") is False


# ---------------------------------------------------------------------------
# Deterministic bucketing
# ---------------------------------------------------------------------------

class TestDeterministicBucketing:
    def test_same_inputs_same_result(self, manager: FeatureFlagManager):
        """Same (flag, owner) must always produce the same result."""
        results = [manager.is_enabled("dream_cycle", owner_id="user-test") for _ in range(100)]
        assert len(set(results)) == 1  # All identical

    def test_hash_bucket_deterministic(self):
        """_hash_bucket must be deterministic."""
        a = _hash_bucket("flag-x", "owner-y")
        b = _hash_bucket("flag-x", "owner-y")
        assert a == b

    def test_hash_bucket_range(self):
        """_hash_bucket should return values in [0.0, 1.0)."""
        for i in range(100):
            val = _hash_bucket(f"flag-{i}", f"owner-{i}")
            assert 0.0 <= val < 1.0

    def test_hash_bucket_distribution(self):
        """Buckets should be roughly uniform across many inputs."""
        buckets = [_hash_bucket("dream_cycle", f"owner-{i}") for i in range(1000)]
        below_half = sum(1 for b in buckets if b < 0.5)
        # Should be roughly 500 ± 100 for a fair hash
        assert 350 < below_half < 650

    def test_different_owners_can_get_different_results(self, manager: FeatureFlagManager):
        """With rollout_pct=0.5, different owners should get different results."""
        results = {manager.is_enabled("dream_cycle", owner_id=f"owner-{i}") for i in range(100)}
        # With 100 owners at 50% rollout, we should see both True and False
        assert True in results
        assert False in results


# ---------------------------------------------------------------------------
# Disabled flag
# ---------------------------------------------------------------------------

class TestDisabledFlag:
    def test_disabled_flag_returns_false(self):
        config = CortexConfig()
        manager = FeatureFlagManager(config=config)
        # Manually disable a flag
        manager._flags["working_memory"].enabled = False
        assert manager.is_enabled("working_memory") is False

    def test_disabled_flag_ignores_rollout(self):
        config = CortexConfig()
        manager = FeatureFlagManager(config=config)
        manager._flags["working_memory"].enabled = False
        manager._flags["working_memory"].rollout_pct = 1.0
        assert manager.is_enabled("working_memory") is False

    def test_disabled_flag_ignores_owner(self):
        config = CortexConfig()
        manager = FeatureFlagManager(config=config)
        manager._flags["working_memory"].enabled = False
        assert manager.is_enabled("working_memory", owner_id="any-user") is False


# ---------------------------------------------------------------------------
# Zero rollout
# ---------------------------------------------------------------------------

class TestZeroRollout:
    def test_zero_pct_always_off(self):
        config = CortexConfig()
        manager = FeatureFlagManager(config=config)
        manager._flags["working_memory"].rollout_pct = 0.0
        for i in range(50):
            assert manager.is_enabled("working_memory", owner_id=f"user-{i}") is False


# ---------------------------------------------------------------------------
# Feature flags system disabled
# ---------------------------------------------------------------------------

class TestSystemDisabled:
    def test_all_flags_enabled_when_system_off(self):
        """When feature_flags.enabled is False, everything returns True."""
        config = CortexConfig()
        object.__setattr__(config, "feature_flags_enabled", False)
        manager = FeatureFlagManager(config=config)
        # Even unknown flags should return True when system is disabled
        assert manager.is_enabled("working_memory") is True
        assert manager.is_enabled("dream_cycle") is True
        assert manager.is_enabled("totally_unknown") is True

    def test_disabled_system_ignores_flag_state(self):
        config = CortexConfig()
        object.__setattr__(config, "feature_flags_enabled", False)
        manager = FeatureFlagManager(config=config)
        manager._flags["working_memory"].enabled = False
        assert manager.is_enabled("working_memory") is True


# ---------------------------------------------------------------------------
# Config overrides
# ---------------------------------------------------------------------------

class TestConfigOverrides:
    def test_get_all_returns_copies(self, manager: FeatureFlagManager):
        """get_all should return flag objects."""
        flags = manager.get_all()
        assert isinstance(flags, dict)
        assert all(isinstance(f, FeatureFlag) for f in flags.values())

    def test_flag_descriptions_present(self, manager: FeatureFlagManager):
        flags = manager.get_all()
        for name, flag in flags.items():
            assert len(flag.description) > 0, f"Flag {name} has no description"


# ---------------------------------------------------------------------------
# FeatureFlag dataclass
# ---------------------------------------------------------------------------

class TestFeatureFlagDataclass:
    def test_defaults(self):
        flag = FeatureFlag(name="test")
        assert flag.enabled is True
        assert flag.rollout_pct == 1.0
        assert flag.description == ""

    def test_custom_values(self):
        flag = FeatureFlag(name="custom", enabled=False, rollout_pct=0.3, description="test flag")
        assert flag.name == "custom"
        assert flag.enabled is False
        assert flag.rollout_pct == 0.3
        assert flag.description == "test flag"


# ---------------------------------------------------------------------------
# Owner ID fallback
# ---------------------------------------------------------------------------

class TestOwnerFallback:
    def test_none_owner_uses_config_owner(self, config: CortexConfig, manager: FeatureFlagManager):
        """When owner_id is None, should use config.owner_id for bucketing."""
        # For 100% rollout flags, this always returns True regardless
        assert manager.is_enabled("working_memory", owner_id=None) is True

    def test_explicit_owner_overrides_config(self, manager: FeatureFlagManager):
        """Explicit owner_id should be used for bucketing."""
        r1 = manager.is_enabled("dream_cycle", owner_id="user-a")
        r2 = manager.is_enabled("dream_cycle", owner_id="user-a")
        assert r1 == r2  # Deterministic
