from unittest.mock import AsyncMock, MagicMock

import pytest

from ui_router.exceptions import RuleEngineError
from ui_router.rules.engine import ConditionEvaluator, RuleEngine
from ui_router.schema import (
    BotVariable,
    ConditionalFlag,
    ConditionOperator,
    Rule,
    RuleCondition,
    VariableScope,
    VariableType,
)


def _cond(variable: str, operator: ConditionOperator, value) -> RuleCondition:
    return RuleCondition(variable=variable, operator=operator, value=value)


def _rule(conditions: list[RuleCondition], result, *, priority: int = 0) -> Rule:
    return Rule(conditions=conditions, result=result, priority=priority)


def _flag(name: str, rules: list[Rule], default) -> ConditionalFlag:
    return ConditionalFlag(name=name, rules=rules, default=default)


def _make_context(
    flags: dict | None = None,
    user_id: int = 1,
    chat_id: int = 1,
    bot_id: int = 1,
    schema_vars: list | None = None,
) -> MagicMock:
    ctx = MagicMock()
    flags = flags or {}
    ctx.get_flag.side_effect = lambda name, default=None: flags.get(name, default)
    ctx.set_flag.side_effect = lambda name, value: flags.__setitem__(name, value)
    ctx.user_id = user_id
    ctx.chat_id = chat_id
    ctx.bot = MagicMock()
    ctx.bot.id = bot_id

    schema_mock = MagicMock()
    schema_mock.variables = schema_vars or []
    ctx.get_from_event.side_effect = lambda key, default=None: schema_mock if key == "ui_router_schema" else default
    return ctx


def _make_var_repo(get_return: None = None) -> AsyncMock:
    repo = AsyncMock()
    repo.get = AsyncMock(return_value=get_return)
    return repo


class TestApplyOperatorEq:
    def test_equal_ints(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.EQ, 5) is True

    def test_not_equal_ints(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.EQ, 3) is False

    def test_equal_strings(self):
        assert ConditionEvaluator._apply_operator("abc", ConditionOperator.EQ, "abc") is True

    def test_equal_none(self):
        assert ConditionEvaluator._apply_operator(None, ConditionOperator.EQ, None) is True


class TestApplyOperatorNe:
    def test_not_equal(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.NE, 3) is True

    def test_equal(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.NE, 5) is False


class TestApplyOperatorGt:
    def test_greater(self):
        assert ConditionEvaluator._apply_operator(10, ConditionOperator.GT, 5) is True

    def test_not_greater(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.GT, 10) is False

    def test_equal_not_greater(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.GT, 5) is False


class TestApplyOperatorGte:
    def test_greater(self):
        assert ConditionEvaluator._apply_operator(10, ConditionOperator.GTE, 5) is True

    def test_equal(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.GTE, 5) is True

    def test_less(self):
        assert ConditionEvaluator._apply_operator(3, ConditionOperator.GTE, 5) is False


class TestApplyOperatorLt:
    def test_less(self):
        assert ConditionEvaluator._apply_operator(3, ConditionOperator.LT, 5) is True

    def test_not_less(self):
        assert ConditionEvaluator._apply_operator(10, ConditionOperator.LT, 5) is False

    def test_equal_not_less(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.LT, 5) is False


class TestApplyOperatorLte:
    def test_less(self):
        assert ConditionEvaluator._apply_operator(3, ConditionOperator.LTE, 5) is True

    def test_equal(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.LTE, 5) is True

    def test_greater(self):
        assert ConditionEvaluator._apply_operator(10, ConditionOperator.LTE, 5) is False


class TestApplyOperatorIn:
    def test_in_list(self):
        assert ConditionEvaluator._apply_operator("a", ConditionOperator.IN, ["a", "b", "c"]) is True

    def test_not_in_list(self):
        assert ConditionEvaluator._apply_operator("x", ConditionOperator.IN, ["a", "b", "c"]) is False

    def test_in_string(self):
        assert ConditionEvaluator._apply_operator("bc", ConditionOperator.IN, "abcdef") is True


class TestApplyOperatorNotIn:
    def test_not_in_list(self):
        assert ConditionEvaluator._apply_operator("x", ConditionOperator.NOT_IN, ["a", "b"]) is True

    def test_in_list(self):
        assert ConditionEvaluator._apply_operator("a", ConditionOperator.NOT_IN, ["a", "b"]) is False


class TestApplyOperatorContains:
    def test_contains_substring(self):
        assert ConditionEvaluator._apply_operator("hello world", ConditionOperator.CONTAINS, "world") is True

    def test_not_contains(self):
        assert ConditionEvaluator._apply_operator("hello world", ConditionOperator.CONTAINS, "xyz") is False

    def test_contains_coerces_to_str(self):
        assert ConditionEvaluator._apply_operator(12345, ConditionOperator.CONTAINS, 234) is True


class TestApplyOperatorStartsWith:
    def test_starts_with(self):
        assert ConditionEvaluator._apply_operator("hello world", ConditionOperator.STARTS_WITH, "hello") is True

    def test_not_starts_with(self):
        assert ConditionEvaluator._apply_operator("hello world", ConditionOperator.STARTS_WITH, "world") is False

    def test_starts_with_coerces_to_str(self):
        assert ConditionEvaluator._apply_operator(12345, ConditionOperator.STARTS_WITH, 123) is True


class TestApplyOperatorEndsWith:
    def test_ends_with(self):
        assert ConditionEvaluator._apply_operator("hello world", ConditionOperator.ENDS_WITH, "world") is True

    def test_not_ends_with(self):
        assert ConditionEvaluator._apply_operator("hello world", ConditionOperator.ENDS_WITH, "hello") is False

    def test_ends_with_coerces_to_str(self):
        assert ConditionEvaluator._apply_operator(12345, ConditionOperator.ENDS_WITH, 345) is True


class TestApplyOperatorTypeError:
    def test_none_gt_int_returns_false(self):
        assert ConditionEvaluator._apply_operator(None, ConditionOperator.GT, 5) is False

    def test_none_lt_int_returns_false(self):
        assert ConditionEvaluator._apply_operator(None, ConditionOperator.LT, 5) is False

    def test_none_in_none_returns_false(self):
        assert ConditionEvaluator._apply_operator(None, ConditionOperator.IN, None) is False

    def test_int_in_int_returns_false(self):
        assert ConditionEvaluator._apply_operator(5, ConditionOperator.IN, 10) is False


class TestEvaluateSimpleCondition:
    def test_basic_eq(self):
        cond = _cond("status", ConditionOperator.EQ, "active")
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"status": "active"}) is True

    def test_basic_ne(self):
        cond = _cond("status", ConditionOperator.NE, "active")
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"status": "banned"}) is True

    def test_missing_variable_returns_none_comparison(self):
        cond = _cond("missing_key", ConditionOperator.EQ, None)
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"other": "val"}) is True

    def test_missing_variable_ne_value(self):
        cond = _cond("missing_key", ConditionOperator.EQ, "something")
        assert ConditionEvaluator.evaluate_simple_condition(cond, {}) is False

    def test_variable_reference_in_value(self):
        cond = _cond("score", ConditionOperator.GT, "{threshold}")
        data = {"score": 100, "threshold": 50}
        assert ConditionEvaluator.evaluate_simple_condition(cond, data) is True

    def test_variable_reference_equal(self):
        cond = _cond("a", ConditionOperator.EQ, "{b}")
        data = {"a": 42, "b": 42}
        assert ConditionEvaluator.evaluate_simple_condition(cond, data) is True

    def test_variable_reference_missing_ref(self):
        cond = _cond("a", ConditionOperator.EQ, "{missing}")
        data = {"a": None}
        assert ConditionEvaluator.evaluate_simple_condition(cond, data) is True

    def test_variable_reference_missing_ref_ne(self):
        cond = _cond("a", ConditionOperator.EQ, "{missing}")
        data = {"a": 42}
        assert ConditionEvaluator.evaluate_simple_condition(cond, data) is False

    def test_gt_numeric(self):
        cond = _cond("age", ConditionOperator.GT, 18)
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"age": 25}) is True

    def test_in_operator(self):
        cond = _cond("role", ConditionOperator.IN, ["admin", "moderator"])
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"role": "admin"}) is True

    def test_contains_operator(self):
        cond = _cond("name", ConditionOperator.CONTAINS, "ohn")
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"name": "John"}) is True

    def test_starts_with_operator(self):
        cond = _cond("email", ConditionOperator.STARTS_WITH, "admin")
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"email": "admin@example.com"}) is True

    def test_ends_with_operator(self):
        cond = _cond("file", ConditionOperator.ENDS_WITH, ".py")
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"file": "main.py"}) is True

    def test_non_string_value_not_dereferenced(self):
        cond = _cond("count", ConditionOperator.EQ, 5)
        assert ConditionEvaluator.evaluate_simple_condition(cond, {"count": 5}) is True


class TestRuleEngineEvaluateConditionalFlag:
    async def test_single_matching_rule(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="premium")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        flag = _flag(
            "access",
            [_rule([_cond("plan", ConditionOperator.EQ, "premium")], "full_access")],
            "limited_access",
        )

        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "full_access"

    async def test_no_matching_rule_returns_default(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="free")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        flag = _flag(
            "access",
            [_rule([_cond("plan", ConditionOperator.EQ, "premium")], "full_access")],
            "limited_access",
        )

        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "limited_access"

    async def test_rules_sorted_by_priority_highest_first(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="vip")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        low_priority_rule = _rule(
            [_cond("level", ConditionOperator.EQ, "vip")],
            "low_result",
            priority=1,
        )
        high_priority_rule = _rule(
            [_cond("level", ConditionOperator.EQ, "vip")],
            "high_result",
            priority=10,
        )

        flag = _flag("level_flag", [low_priority_rule, high_priority_rule], "default")

        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "high_result"

    async def test_first_matching_rule_wins_at_same_priority(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="active")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule_a = _rule([_cond("status", ConditionOperator.EQ, "active")], "result_a", priority=5)
        rule_b = _rule([_cond("status", ConditionOperator.EQ, "active")], "result_b", priority=5)

        flag = _flag("test", [rule_a, rule_b], "default")

        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result in ("result_a", "result_b")

    async def test_rule_with_false_condition_skipped(self):
        repo = _make_var_repo()

        call_count = 0
        original_get = repo.get

        async def selective_get(**kwargs):
            nonlocal call_count
            call_count += 1
            name = kwargs.get("name", "")
            if name == "status":
                return "inactive"
            if name == "plan":
                return "premium"
            return None

        repo.get = AsyncMock(side_effect=selective_get)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule_inactive = _rule(
            [_cond("status", ConditionOperator.EQ, "active")],
            "should_not_match",
            priority=10,
        )
        rule_premium = _rule(
            [_cond("plan", ConditionOperator.EQ, "premium")],
            "premium_result",
            priority=5,
        )

        flag = _flag("test", [rule_inactive, rule_premium], "default")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "premium_result"

    async def test_rule_with_multiple_conditions_and_logic(self):
        repo = _make_var_repo()

        async def multi_get(**kwargs):
            name = kwargs.get("name", "")
            if name == "role":
                return "admin"
            if name == "active":
                return True
            return None

        repo.get = AsyncMock(side_effect=multi_get)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule(
            [
                _cond("role", ConditionOperator.EQ, "admin"),
                _cond("active", ConditionOperator.EQ, True),
            ],
            "admin_active",
        )

        flag = _flag("test", [rule], "default")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "admin_active"

    async def test_rule_with_one_false_in_and_conditions(self):
        repo = _make_var_repo()

        async def multi_get(**kwargs):
            name = kwargs.get("name", "")
            if name == "role":
                return "admin"
            if name == "active":
                return False
            return None

        repo.get = AsyncMock(side_effect=multi_get)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule(
            [
                _cond("role", ConditionOperator.EQ, "admin"),
                _cond("active", ConditionOperator.EQ, True),
            ],
            "should_not_match",
        )

        flag = _flag("test", [rule], "fallback")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "fallback"

    async def test_rule_with_no_conditions_always_matches(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([], "always_true")
        flag = _flag("test", [rule], "default")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "always_true"

    async def test_empty_rules_returns_default(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        flag = _flag("test", [], "the_default")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "the_default"


class TestRuleEngineValueResolution:
    async def test_resolve_variable_reference_in_result(self):
        repo = _make_var_repo()

        async def get_var(**kwargs):
            name = kwargs.get("name", "")
            if name == "condition_var":
                return "yes"
            if name == "result_var":
                return "resolved_result"
            return None

        repo.get = AsyncMock(side_effect=get_var)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([_cond("condition_var", ConditionOperator.EQ, "yes")], "{result_var}")
        flag = _flag("test", [rule], "default")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "resolved_result"

    async def test_resolve_variable_reference_in_default(self):
        repo = _make_var_repo()

        async def get_var(**kwargs):
            name = kwargs.get("name", "")
            if name == "status":
                return "no_match"
            if name == "fallback_var":
                return "fallback_resolved"
            return None

        repo.get = AsyncMock(side_effect=get_var)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([_cond("status", ConditionOperator.EQ, "match_this")], "wont_match")
        flag = _flag("test", [rule], "{fallback_var}")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "fallback_resolved"

    async def test_resolve_plain_string_not_dereferenced(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="anything")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([], "plain_string")
        flag = _flag("test", [rule], "default")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "plain_string"

    async def test_resolve_non_string_value_returned_as_is(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([], 42)
        flag = _flag("test", [rule], "default")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == 42

    async def test_resolve_variable_reference_in_condition_value(self):
        repo = _make_var_repo()

        async def get_var(**kwargs):
            name = kwargs.get("name", "")
            if name == "score":
                return 100
            if name == "threshold":
                return 50
            return None

        repo.get = AsyncMock(side_effect=get_var)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([_cond("score", ConditionOperator.GT, "{threshold}")], "above_threshold")
        flag = _flag("test", [rule], "below")
        result = await engine.evaluate_conditional_flag(flag, ctx)
        assert result == "above_threshold"


class TestGetVariableValueWithSchema:
    async def test_user_scope_variable(self):
        schema_var = BotVariable(
            name="user_lang",
            scope=VariableScope.USER,
            type=VariableType.STR,
        )
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="en")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[schema_var])

        result = await engine._get_variable_value("user_lang", ctx)
        assert result == "en"
        repo.get.assert_called_once_with(
            bot_id=ctx.bot.id,
            name="user_lang",
            scope=VariableScope.USER,
            user_id=ctx.user_id,
            chat_id=None,
        )

    async def test_bot_scope_variable(self):
        schema_var = BotVariable(
            name="bot_mode",
            scope=VariableScope.BOT,
            type=VariableType.STR,
        )
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="production")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[schema_var])

        result = await engine._get_variable_value("bot_mode", ctx)
        assert result == "production"
        repo.get.assert_called_once_with(
            bot_id=ctx.bot.id,
            name="bot_mode",
            scope=VariableScope.BOT,
            user_id=None,
            chat_id=None,
        )

    async def test_chat_scope_variable(self):
        schema_var = BotVariable(
            name="chat_lang",
            scope=VariableScope.CHAT,
            type=VariableType.STR,
        )
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="ru")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[schema_var])

        result = await engine._get_variable_value("chat_lang", ctx)
        assert result == "ru"
        repo.get.assert_called_once_with(
            bot_id=ctx.bot.id,
            name="chat_lang",
            scope=VariableScope.CHAT,
            user_id=None,
            chat_id=ctx.chat_id,
        )

    async def test_schema_variable_with_default_when_none(self):
        schema_var = BotVariable(
            name="theme",
            scope=VariableScope.USER,
            type=VariableType.STR,
            default="light",
        )
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value=None)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[schema_var])

        result = await engine._get_variable_value("theme", ctx)
        assert result == "light"

    async def test_schema_variable_no_default_returns_none(self):
        schema_var = BotVariable(
            name="theme",
            scope=VariableScope.USER,
            type=VariableType.STR,
        )
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value=None)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[schema_var])

        result = await engine._get_variable_value("theme", ctx)
        assert result is None

    async def test_schema_variable_value_not_none_ignores_default(self):
        schema_var = BotVariable(
            name="theme",
            scope=VariableScope.USER,
            type=VariableType.STR,
            default="light",
        )
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="dark")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[schema_var])

        result = await engine._get_variable_value("theme", ctx)
        assert result == "dark"


class TestGetVariableValueWithoutSchema:
    async def test_tries_user_scope_first(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="user_value")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[])

        result = await engine._get_variable_value("some_var", ctx)
        assert result == "user_value"
        repo.get.assert_called_once_with(
            bot_id=ctx.bot.id,
            name="some_var",
            scope=VariableScope.USER,
            user_id=ctx.user_id,
        )

    async def test_falls_back_to_bot_scope(self):
        repo = _make_var_repo()

        call_count = 0

        async def scope_fallback(**kwargs):
            nonlocal call_count
            call_count += 1
            if kwargs.get("scope") == VariableScope.USER:
                return None
            if kwargs.get("scope") == VariableScope.BOT:
                return "bot_value"
            return None

        repo.get = AsyncMock(side_effect=scope_fallback)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[])

        result = await engine._get_variable_value("some_var", ctx)
        assert result == "bot_value"
        assert repo.get.call_count == 2

    async def test_returns_none_when_both_scopes_empty(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value=None)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context(schema_vars=[])

        result = await engine._get_variable_value("some_var", ctx)
        assert result is None
        assert repo.get.call_count == 2


class TestGetVariableValueNoSchema:
    async def test_no_schema_in_context_tries_user_then_bot(self):
        repo = _make_var_repo()

        async def scope_fallback(**kwargs):
            if kwargs.get("scope") == VariableScope.USER:
                return None
            if kwargs.get("scope") == VariableScope.BOT:
                return "from_bot"
            return None

        repo.get = AsyncMock(side_effect=scope_fallback)
        engine = RuleEngine(variable_repository=repo)

        ctx = MagicMock()
        ctx.user_id = 1
        ctx.chat_id = 1
        ctx.bot = MagicMock()
        ctx.bot.id = 1
        ctx.get_from_event.return_value = None

        result = await engine._get_variable_value("some_var", ctx)
        assert result == "from_bot"
        assert repo.get.call_count == 2


class TestGetVariableValueErrors:
    async def test_no_variable_repository_raises(self):
        engine = RuleEngine(variable_repository=None)
        ctx = _make_context()

        with pytest.raises(RuleEngineError, match="VariableRepository not configured"):
            await engine._get_variable_value("any_var", ctx)


class TestCheckRule:
    async def test_empty_conditions_returns_true(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([], "result")
        assert await engine._check_rule(rule, ctx) is True

    async def test_all_conditions_true(self):
        repo = _make_var_repo()

        async def get_var(**kwargs):
            name = kwargs.get("name", "")
            if name == "a":
                return 10
            if name == "b":
                return "yes"
            return None

        repo.get = AsyncMock(side_effect=get_var)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([
            _cond("a", ConditionOperator.GT, 5),
            _cond("b", ConditionOperator.EQ, "yes"),
        ], "result")

        assert await engine._check_rule(rule, ctx) is True

    async def test_one_false_condition_returns_false(self):
        repo = _make_var_repo()

        async def get_var(**kwargs):
            name = kwargs.get("name", "")
            if name == "a":
                return 10
            if name == "b":
                return "no"
            return None

        repo.get = AsyncMock(side_effect=get_var)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        rule = _rule([
            _cond("a", ConditionOperator.GT, 5),
            _cond("b", ConditionOperator.EQ, "yes"),
        ], "result")

        assert await engine._check_rule(rule, ctx) is False


class TestCheckCondition:
    async def test_simple_eq_condition(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="active")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        cond = _cond("status", ConditionOperator.EQ, "active")
        assert await engine._check_condition(cond, ctx) is True

    async def test_simple_ne_condition(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="active")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        cond = _cond("status", ConditionOperator.NE, "banned")
        assert await engine._check_condition(cond, ctx) is True

    async def test_condition_with_variable_reference(self):
        repo = _make_var_repo()

        async def get_var(**kwargs):
            name = kwargs.get("name", "")
            if name == "current":
                return 100
            if name == "limit":
                return 50
            return None

        repo.get = AsyncMock(side_effect=get_var)
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        cond = _cond("current", ConditionOperator.GT, "{limit}")
        assert await engine._check_condition(cond, ctx) is True


class TestResolveValue:
    async def test_plain_string(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        result = await engine._resolve_value("plain", ctx)
        assert result == "plain"

    async def test_variable_reference(self):
        repo = _make_var_repo()
        repo.get = AsyncMock(return_value="resolved")
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        result = await engine._resolve_value("{my_var}", ctx)
        assert result == "resolved"

    async def test_non_string_returned_as_is(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        assert await engine._resolve_value(42, ctx) == 42
        assert await engine._resolve_value(None, ctx) is None
        assert await engine._resolve_value([1, 2], ctx) == [1, 2]
        assert await engine._resolve_value(True, ctx) is True

    async def test_string_without_braces_not_dereferenced(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        result = await engine._resolve_value("no_braces", ctx)
        assert result == "no_braces"
        repo.get.assert_not_called()

    async def test_partial_braces_not_dereferenced(self):
        repo = _make_var_repo()
        engine = RuleEngine(variable_repository=repo)
        ctx = _make_context()

        assert await engine._resolve_value("{open_only", ctx) == "{open_only"
        assert await engine._resolve_value("close_only}", ctx) == "close_only}"
        repo.get.assert_not_called()
