from unittest.mock import AsyncMock, MagicMock

from ui_router.rules.flags import FlagResolver
from ui_router.state.variables import VariableScope


def make_context(flags=None, user_id=1, chat_id=1, bot_id=1):
    ctx = MagicMock()
    _flags = flags if flags is not None else {}
    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.get_user_input = MagicMock(side_effect=lambda key, default=None: _flags.get(f"input_{key}", default))
    ctx.user_id = user_id
    ctx.chat_id = chat_id
    ctx.bot = MagicMock()
    ctx.bot.id = bot_id
    ctx.event_data = {}
    return ctx


def make_schema(global_flags=None, conditional_flags=None, variables=None):
    schema = MagicMock()
    schema.global_flags = global_flags or []
    schema.conditional_flags = conditional_flags or []
    schema.variables = variables or []
    return schema


def make_flag(name, getter_type="static", value=None, function=None, context_key=None, input_key=None, default=None):
    flag = MagicMock()
    flag.name = name
    flag.default = default
    flag.getter = MagicMock()
    flag.getter.type = getter_type
    flag.getter.value = value
    flag.getter.function = function
    flag.getter.context_key = context_key
    flag.getter.input_key = input_key
    return flag


def make_variable(name, scope=VariableScope.USER, default=None):
    var = MagicMock()
    var.name = name
    var.scope = scope
    var.default = default
    return var


def make_resolver(schema=None, registry=None, rule_engine=None, variable_repository=None):
    return FlagResolver(
        schema=schema or make_schema(),
        registry=registry or MagicMock(),
        rule_engine=rule_engine or MagicMock(),
        variable_repository=variable_repository or AsyncMock(),
    )


class TestGetFlagValue:
    async def test_static_getter_returns_value(self):
        resolver = make_resolver()
        flag = make_flag("color", getter_type="static", value="red")
        ctx = make_context()

        result = await resolver.get_flag_value(flag, ctx)

        assert result == "red"

    async def test_static_getter_returns_none_when_value_is_none(self):
        resolver = make_resolver()
        flag = make_flag("empty", getter_type="static", value=None)
        ctx = make_context()

        result = await resolver.get_flag_value(flag, ctx)

        assert result is None

    async def test_function_getter_calls_registry(self):
        registry = MagicMock()
        registry.getters.get_value = AsyncMock(return_value=42)
        resolver = make_resolver(registry=registry)
        flag = make_flag("count", getter_type="function", function="get_count")
        ctx = make_context()

        result = await resolver.get_flag_value(flag, ctx)

        assert result == 42
        registry.getters.get_value.assert_awaited_once_with("get_count", ctx)

    async def test_context_getter_calls_get_user_input_with_context_key(self):
        resolver = make_resolver()
        flag = make_flag("lang", getter_type="context", context_key="language", default="en")
        ctx = make_context(flags={"input_language": "ru"})

        result = await resolver.get_flag_value(flag, ctx)

        assert result == "ru"
        ctx.get_user_input.assert_called_once_with("language", "en")

    async def test_context_getter_returns_default_when_key_missing(self):
        resolver = make_resolver()
        flag = make_flag("lang", getter_type="context", context_key="language", default="en")
        ctx = make_context()

        result = await resolver.get_flag_value(flag, ctx)

        assert result == "en"

    async def test_user_input_getter_calls_get_user_input_with_input_key(self):
        resolver = make_resolver()
        flag = make_flag("name", getter_type="user_input", input_key="user_name", default="anon")
        ctx = make_context(flags={"input_user_name": "Alice"})

        result = await resolver.get_flag_value(flag, ctx)

        assert result == "Alice"
        ctx.get_user_input.assert_called_once_with("user_name", "anon")

    async def test_user_input_getter_returns_default_when_key_missing(self):
        resolver = make_resolver()
        flag = make_flag("name", getter_type="user_input", input_key="user_name", default="anon")
        ctx = make_context()

        result = await resolver.get_flag_value(flag, ctx)

        assert result == "anon"

    async def test_unknown_getter_type_returns_default(self):
        resolver = make_resolver()
        flag = make_flag("mystery", getter_type="unknown_type", default="fallback")
        ctx = make_context()

        result = await resolver.get_flag_value(flag, ctx)

        assert result == "fallback"

    async def test_unknown_getter_type_returns_none_when_no_default(self):
        resolver = make_resolver()
        flag = make_flag("mystery", getter_type="nonexistent")
        ctx = make_context()

        result = await resolver.get_flag_value(flag, ctx)

        assert result is None


class TestResolveSceneFlags:
    async def test_resolves_global_flags(self):
        global_flag = make_flag("theme", getter_type="static", value="dark")
        schema = make_schema(global_flags=[global_flag])
        resolver = make_resolver(schema=schema, rule_engine=MagicMock())

        scene = MagicMock()
        scene.flags = []
        ctx = make_context()

        await resolver.resolve_scene_flags(scene, ctx)

        ctx.set_flag.assert_any_call("theme", "dark")

    async def test_resolves_scene_flags(self):
        schema = make_schema()
        resolver = make_resolver(schema=schema)

        scene_flag = make_flag("page", getter_type="static", value=1)
        scene = MagicMock()
        scene.flags = [scene_flag]
        ctx = make_context()

        await resolver.resolve_scene_flags(scene, ctx)

        ctx.set_flag.assert_any_call("page", 1)

    async def test_scene_flag_overrides_global_flag(self):
        global_flag = make_flag("color", getter_type="static", value="blue")
        schema = make_schema(global_flags=[global_flag])
        resolver = make_resolver(schema=schema)

        scene_flag = make_flag("color", getter_type="static", value="red")
        scene = MagicMock()
        scene.flags = [scene_flag]
        flags_dict = {}
        ctx = make_context(flags=flags_dict)

        await resolver.resolve_scene_flags(scene, ctx)

        assert flags_dict["color"] == "red"

    async def test_calls_resolve_variables_as_flags(self):
        schema = make_schema()
        resolver = make_resolver(schema=schema)
        resolver.resolve_variables_as_flags = AsyncMock()

        scene = MagicMock()
        scene.flags = []
        ctx = make_context()

        await resolver.resolve_scene_flags(scene, ctx)

        resolver.resolve_variables_as_flags.assert_awaited_once_with(ctx)

    async def test_evaluates_conditional_flags(self):
        cond_flag = MagicMock()
        cond_flag.name = "is_premium"
        schema = make_schema(conditional_flags=[cond_flag])
        rule_engine = MagicMock()
        rule_engine.evaluate_conditional_flag = AsyncMock(return_value=True)
        resolver = make_resolver(schema=schema, rule_engine=rule_engine)

        scene = MagicMock()
        scene.flags = []
        ctx = make_context()

        await resolver.resolve_scene_flags(scene, ctx)

        rule_engine.evaluate_conditional_flag.assert_awaited_once_with(cond_flag, ctx)
        ctx.set_flag.assert_any_call("is_premium", True)

    async def test_sets_schema_in_event_data(self):
        schema = make_schema()
        resolver = make_resolver(schema=schema)

        scene = MagicMock()
        scene.flags = []
        ctx = make_context()

        await resolver.resolve_scene_flags(scene, ctx)

        assert ctx.event_data["ui_router_schema"] is schema

    async def test_resolution_order_global_then_scene_then_variables_then_conditional(self):
        call_order = []

        global_flag = make_flag("gf", getter_type="static", value="global")
        scene_flag = make_flag("sf", getter_type="static", value="scene")
        cond_flag = MagicMock()
        cond_flag.name = "cf"

        schema = make_schema(global_flags=[global_flag], conditional_flags=[cond_flag])

        variable_repo = AsyncMock()
        rule_engine = MagicMock()
        rule_engine.evaluate_conditional_flag = AsyncMock(return_value="cond_val")

        resolver = make_resolver(schema=schema, rule_engine=rule_engine, variable_repository=variable_repo)

        original_resolve_vars = resolver.resolve_variables_as_flags

        async def track_resolve_vars(context):
            call_order.append("variables")
            await original_resolve_vars(context)

        resolver.resolve_variables_as_flags = track_resolve_vars

        flags_dict = {}
        ctx = make_context(flags=flags_dict)
        original_set = ctx.set_flag.side_effect

        def tracking_set(name, value):
            call_order.append(name)
            return original_set(name, value)

        ctx.set_flag.side_effect = tracking_set

        scene = MagicMock()
        scene.flags = [scene_flag]

        await resolver.resolve_scene_flags(scene, ctx)

        gf_idx = call_order.index("gf")
        sf_idx = call_order.index("sf")
        var_idx = call_order.index("variables")
        cf_idx = call_order.index("cf")
        assert gf_idx < sf_idx < var_idx < cf_idx


class TestGetVariableForCondition:
    async def test_returns_value_from_context_flags(self):
        resolver = make_resolver()
        ctx = make_context(flags={"score": 100})

        result = await resolver.get_variable_for_condition("score", ctx)

        assert result == 100

    async def test_returns_from_user_scope_when_not_in_flags(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=lambda bot_id, name, scope, user_id=None, chat_id=None: (
            "user_val" if scope == "user" else None
        ))
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.get_variable_for_condition("pref", ctx)

        assert result == "user_val"

    async def test_returns_from_chat_scope_when_not_in_user(self):
        async def repo_get(bot_id, name, scope, user_id=None, chat_id=None):
            if scope == "chat":
                return "chat_val"
            return None

        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=repo_get)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.get_variable_for_condition("setting", ctx)

        assert result == "chat_val"

    async def test_returns_from_bot_scope_when_not_in_chat(self):
        async def repo_get(bot_id, name, scope, user_id=None, chat_id=None):
            if scope == "bot":
                return "bot_val"
            return None

        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=repo_get)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.get_variable_for_condition("config", ctx)

        assert result == "bot_val"

    async def test_returns_none_when_not_found_anywhere(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value=None)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.get_variable_for_condition("missing", ctx)

        assert result is None

    async def test_continues_to_next_scope_on_exception(self):
        call_count = 0

        async def repo_get(bot_id, name, scope, user_id=None, chat_id=None):
            nonlocal call_count
            call_count += 1
            if scope == "user":
                raise RuntimeError("db error")
            if scope == "chat":
                return "recovered"
            return None

        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=repo_get)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.get_variable_for_condition("robust", ctx)

        assert result == "recovered"

    async def test_passes_user_id_only_for_user_scope(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value=None)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context(user_id=42, chat_id=99, bot_id=7)

        await resolver.get_variable_for_condition("x", ctx)

        calls = variable_repo.get.call_args_list
        user_call = [c for c in calls if c.kwargs.get("scope") == "user" or (len(c.args) > 2 and c.args[2] == "user")]
        chat_call = [c for c in calls if c.kwargs.get("scope") == "chat" or (len(c.args) > 2 and c.args[2] == "chat")]
        bot_call = [c for c in calls if c.kwargs.get("scope") == "bot" or (len(c.args) > 2 and c.args[2] == "bot")]

        assert len(user_call) == 1
        assert user_call[0].kwargs.get("user_id") == 42
        assert chat_call[0].kwargs.get("chat_id") == 99
        assert bot_call[0].kwargs.get("user_id") is None
        assert bot_call[0].kwargs.get("chat_id") is None

    async def test_uses_bot_id_from_event_data_when_bot_is_none(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value=None)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()
        ctx.bot = None
        ctx.event_data = {"bot_id": 555}

        await resolver.get_variable_for_condition("x", ctx)

        first_call = variable_repo.get.call_args_list[0]
        assert first_call.kwargs.get("bot_id") == 555


class TestResolveVariablesAsFlags:
    async def test_sets_variable_value_as_flag(self):
        var = make_variable("user_lang", scope=VariableScope.USER, default="en")
        schema = make_schema(variables=[var])
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value="fr")
        resolver = make_resolver(schema=schema, variable_repository=variable_repo)
        flags_dict = {}
        ctx = make_context(flags=flags_dict)

        await resolver.resolve_variables_as_flags(ctx)

        assert flags_dict["user_lang"] == "fr"

    async def test_uses_default_when_repo_returns_none(self):
        var = make_variable("theme", scope=VariableScope.USER, default="light")
        schema = make_schema(variables=[var])
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value=None)
        resolver = make_resolver(schema=schema, variable_repository=variable_repo)
        flags_dict = {}
        ctx = make_context(flags=flags_dict)

        await resolver.resolve_variables_as_flags(ctx)

        assert flags_dict["theme"] == "light"

    async def test_resolves_multiple_variables(self):
        var_a = make_variable("a", scope=VariableScope.USER, default=1)
        var_b = make_variable("b", scope=VariableScope.CHAT, default=2)
        var_c = make_variable("c", scope=VariableScope.BOT, default=3)
        schema = make_schema(variables=[var_a, var_b, var_c])

        async def repo_get(bot_id, name, scope, user_id=None, chat_id=None):
            return {"a": 10, "b": 20}.get(name)

        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=repo_get)
        resolver = make_resolver(schema=schema, variable_repository=variable_repo)
        flags_dict = {}
        ctx = make_context(flags=flags_dict)

        await resolver.resolve_variables_as_flags(ctx)

        assert flags_dict["a"] == 10
        assert flags_dict["b"] == 20
        assert flags_dict["c"] == 3

    async def test_passes_correct_scope_and_ids(self):
        var = make_variable("x", scope=VariableScope.CHAT, default=None)
        schema = make_schema(variables=[var])
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value="val")
        resolver = make_resolver(schema=schema, variable_repository=variable_repo)
        ctx = make_context(user_id=11, chat_id=22, bot_id=33)

        await resolver.resolve_variables_as_flags(ctx)

        variable_repo.get.assert_awaited_once_with(
            bot_id=33,
            name="x",
            scope=VariableScope.CHAT,
            user_id=11,
            chat_id=22,
        )

    async def test_no_variables_does_nothing(self):
        schema = make_schema(variables=[])
        variable_repo = AsyncMock()
        resolver = make_resolver(schema=schema, variable_repository=variable_repo)
        ctx = make_context()

        await resolver.resolve_variables_as_flags(ctx)

        variable_repo.get.assert_not_awaited()


class TestResolveConditionValue:
    async def test_non_reference_string_returned_as_is(self):
        resolver = make_resolver()
        ctx = make_context()

        result = await resolver.resolve_condition_value("plain_text", ctx)

        assert result == "plain_text"

    async def test_integer_returned_as_is(self):
        resolver = make_resolver()
        ctx = make_context()

        result = await resolver.resolve_condition_value(42, ctx)

        assert result == 42

    async def test_boolean_returned_as_is(self):
        resolver = make_resolver()
        ctx = make_context()

        result = await resolver.resolve_condition_value(True, ctx)

        assert result is True

    async def test_none_returned_as_is(self):
        resolver = make_resolver()
        ctx = make_context()

        result = await resolver.resolve_condition_value(None, ctx)

        assert result is None

    async def test_reference_found_in_flags(self):
        resolver = make_resolver()
        ctx = make_context(flags={"my_var": "flag_value"})

        result = await resolver.resolve_condition_value("{my_var}", ctx)

        assert result == "flag_value"

    async def test_reference_not_in_flags_found_in_user_scope(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=lambda bot_id, name, scope, **kw: (
            "user_result" if scope == VariableScope.USER else None
        ))
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.resolve_condition_value("{pref}", ctx)

        assert result == "user_result"

    async def test_reference_found_in_chat_scope(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=lambda bot_id, name, scope, **kw: (
            "chat_result" if scope == VariableScope.CHAT else None
        ))
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.resolve_condition_value("{setting}", ctx)

        assert result == "chat_result"

    async def test_reference_found_in_bot_scope(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(side_effect=lambda bot_id, name, scope, **kw: (
            "bot_result" if scope == VariableScope.BOT else None
        ))
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.resolve_condition_value("{global_cfg}", ctx)

        assert result == "bot_result"

    async def test_reference_not_found_anywhere_returns_none(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value=None)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.resolve_condition_value("{missing}", ctx)

        assert result is None

    async def test_string_with_braces_but_not_reference_pattern(self):
        resolver = make_resolver()
        ctx = make_context()

        result = await resolver.resolve_condition_value("{not closed", ctx)

        assert result == "{not closed"

    async def test_empty_braces_treated_as_reference_with_empty_name(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value=None)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context()

        result = await resolver.resolve_condition_value("{}", ctx)

        assert result is None
        assert variable_repo.get.await_count == 3

    async def test_reference_passes_correct_ids_to_repository(self):
        variable_repo = AsyncMock()
        variable_repo.get = AsyncMock(return_value=None)
        resolver = make_resolver(variable_repository=variable_repo)
        ctx = make_context(user_id=5, chat_id=10, bot_id=15)

        await resolver.resolve_condition_value("{x}", ctx)

        calls = variable_repo.get.call_args_list
        assert calls[0] == (
            (),
            {"bot_id": 15, "name": "x", "scope": VariableScope.USER, "user_id": 5},
        )
        assert calls[1] == (
            (),
            {"bot_id": 15, "name": "x", "scope": VariableScope.CHAT, "chat_id": 10},
        )
        assert calls[2] == (
            (),
            {"bot_id": 15, "name": "x", "scope": VariableScope.BOT},
        )

    async def test_list_returned_as_is(self):
        resolver = make_resolver()
        ctx = make_context()

        result = await resolver.resolve_condition_value([1, 2, 3], ctx)

        assert result == [1, 2, 3]
