"""Tests for NavigationState, ExecutionContext, and NavigationManager."""

from unittest.mock import AsyncMock, MagicMock

import pytest
from aiogram.types import CallbackQuery

from ui_router.state.context import EventMode, ExecutionContext, NavigationManager, NavigationState
from ui_router.state.storage import InMemoryNavigationStorage


class TestNavigationState:
    def test_to_dict_includes_version(self):
        state = NavigationState(current_scene="main", history=["start"], transitions=["send"])
        result = state.to_dict()

        assert result["version"] == 1
        assert result["current_scene"] == "main"
        assert result["history"] == ["start"]
        assert result["transitions"] == ["send"]
        assert result["user_input"] == {}
        assert result["flags_cache"] == {}

    def test_from_dict_version_0_migration(self):
        data = {"version": 0, "current_scene": "old_scene", "history": ["a", "b"]}
        state = NavigationState.from_dict(data)

        assert state.current_scene == "old_scene"
        assert state.history == ["a", "b"]
        assert state.transitions == []
        assert state.user_input == {}
        assert state.flags_cache == {}

    def test_from_dict_version_1(self):
        data = {
            "version": 1,
            "current_scene": "scene_x",
            "history": ["scene_a"],
            "transitions": ["send"],
            "user_input": {"name": "Alice"},
            "flags_cache": {"premium": True},
        }
        state = NavigationState.from_dict(data)

        assert state.current_scene == "scene_x"
        assert state.history == ["scene_a"]
        assert state.transitions == ["send"]
        assert state.user_input == {"name": "Alice"}
        assert state.flags_cache == {"premium": True}

    def test_from_dict_missing_fields_uses_defaults(self):
        state = NavigationState.from_dict({})

        assert state.current_scene == ""
        assert state.history == []
        assert state.transitions == []
        assert state.user_input == {}
        assert state.flags_cache == {}


class TestExecutionContext:
    def _make_context(self, **overrides) -> ExecutionContext:
        defaults = {
            "navigation_manager": MagicMock(),
            "navigation_state": NavigationState(current_scene="test_scene"),
            "scene_id": "test_scene",
            "bot": None,
            "user_id": None,
            "chat_id": None,
            "event": None,
            "event_mode": EventMode.MESSAGE,
            "event_data": {},
            "flag_resolver": None,
        }
        defaults.update(overrides)
        return ExecutionContext(**defaults)

    async def test_get_flag_async_returns_cached(self):
        ctx = self._make_context()
        ctx.set_flag("cached_flag", 42)

        result = await ctx.get_flag_async("cached_flag", default=0)

        assert result == 42

    async def test_get_flag_async_calls_resolver_and_caches(self):
        resolver = AsyncMock(return_value="resolved_value")
        ctx = self._make_context(flag_resolver=resolver)

        result = await ctx.get_flag_async("dynamic_flag", default="fallback")

        assert result == "resolved_value"
        resolver.assert_awaited_once_with("dynamic_flag")
        assert ctx.get_flag("dynamic_flag") == "resolved_value"

    async def test_get_flag_async_resolver_returns_none_uses_default(self):
        resolver = AsyncMock(return_value=None)
        ctx = self._make_context(flag_resolver=resolver)

        result = await ctx.get_flag_async("missing_flag", default="fallback")

        assert result == "fallback"
        assert ctx.get_flag("missing_flag") is None

    async def test_get_flag_async_resolver_raises_exception_uses_default(self):
        resolver = AsyncMock(side_effect=RuntimeError("resolver failed"))
        ctx = self._make_context(flag_resolver=resolver)

        result = await ctx.get_flag_async("broken_flag", default="safe")

        assert result == "safe"

    async def test_get_flag_async_no_resolver_uses_default(self):
        ctx = self._make_context(flag_resolver=None)

        result = await ctx.get_flag_async("any_flag", default="default_val")

        assert result == "default_val"

    def test_can_send_message_with_bot_and_user_id(self):
        bot = MagicMock()
        bot.id = 1
        ctx = self._make_context(bot=bot, user_id=1)

        assert ctx.can_send_message() is True

    def test_can_send_message_no_bot(self):
        ctx = self._make_context(bot=None, user_id=1)

        assert ctx.can_send_message() is False

    def test_can_send_message_no_user_id(self):
        bot = MagicMock()
        bot.id = 1
        ctx = self._make_context(bot=bot, user_id=None)

        assert ctx.can_send_message() is False

    def test_can_edit_message_callback_mode_with_callback_query(self):
        event = MagicMock(spec=CallbackQuery)
        ctx = self._make_context(event_mode=EventMode.CALLBACK, event=event)

        assert ctx.can_edit_message() is True

    def test_can_edit_message_message_mode(self):
        ctx = self._make_context(event_mode=EventMode.MESSAGE)

        assert ctx.can_edit_message() is False

    def test_can_edit_message_callback_mode_non_callback_event(self):
        event = MagicMock()
        ctx = self._make_context(event_mode=EventMode.CALLBACK, event=event)

        assert ctx.can_edit_message() is False

    async def test_save_to_storage_no_user_id(self):
        nav_manager = MagicMock()
        nav_manager.save_state = AsyncMock()
        ctx = self._make_context(navigation_manager=nav_manager, user_id=None)

        await ctx.save_to_storage()

        nav_manager.save_state.assert_not_awaited()

    async def test_save_to_storage_no_bot_no_bot_id_in_event_data(self):
        nav_manager = MagicMock()
        nav_manager.save_state = AsyncMock()
        ctx = self._make_context(navigation_manager=nav_manager, user_id=1, bot=None, event_data={})

        await ctx.save_to_storage()

        nav_manager.save_state.assert_not_awaited()

    async def test_save_to_storage_no_bot_but_bot_id_in_event_data(self):
        nav_manager = MagicMock()
        nav_manager.save_state = AsyncMock()
        ctx = self._make_context(
            navigation_manager=nav_manager,
            user_id=100,
            chat_id=200,
            bot=None,
            event_data={"bot_id": 999},
        )

        await ctx.save_to_storage()

        nav_manager.save_state.assert_awaited_once()
        call_args = nav_manager.save_state.call_args
        assert call_args[0][0] == 999
        assert call_args[0][1] == 100
        assert call_args[0][2] == 200

    async def test_save_to_storage_valid(self):
        bot = MagicMock()
        bot.id = 42
        nav_manager = MagicMock()
        nav_manager.save_state = AsyncMock()
        nav_state = NavigationState(current_scene="scene_a")
        ctx = self._make_context(
            navigation_manager=nav_manager,
            navigation_state=nav_state,
            bot=bot,
            user_id=100,
            chat_id=200,
        )

        await ctx.save_to_storage()

        nav_manager.save_state.assert_awaited_once_with(42, 100, 200, nav_state)

    async def test_save_to_storage_uses_user_id_as_chat_id_fallback(self):
        bot = MagicMock()
        bot.id = 42
        nav_manager = MagicMock()
        nav_manager.save_state = AsyncMock()
        ctx = self._make_context(
            navigation_manager=nav_manager,
            bot=bot,
            user_id=100,
            chat_id=None,
        )

        await ctx.save_to_storage()

        call_args = nav_manager.save_state.call_args[0]
        assert call_args[2] == 100


class TestNavigationManager:
    @pytest.fixture
    def storage(self):
        return InMemoryNavigationStorage()

    @pytest.fixture
    def nav_manager(self, storage):
        return NavigationManager(navigation_storage=storage, max_history_depth=5)

    async def test_navigate_to_adds_current_scene_to_history(self, nav_manager):
        await nav_manager.initialize(bot_id=1, user_id=1, chat_id=1, initial_scene="scene_a")

        state = await nav_manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="scene_b")

        assert state.current_scene == "scene_b"
        assert state.history == ["scene_a"]
        assert state.transitions == ["send"]

    async def test_navigate_to_history_trimming(self, storage):
        manager = NavigationManager(navigation_storage=storage, max_history_depth=3)
        await manager.initialize(bot_id=1, user_id=1, chat_id=1, initial_scene="s0")

        await manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="s1")
        await manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="s2")
        await manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="s3")
        state = await manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="s4")

        assert len(state.history) == 3
        assert state.history == ["s1", "s2", "s3"]
        assert len(state.transitions) == 3

    async def test_navigate_to_cycle_detection_logs_warning(self, nav_manager, caplog):
        import logging

        await nav_manager.initialize(bot_id=1, user_id=1, chat_id=1, initial_scene="loop")
        await nav_manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="loop")
        await nav_manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="loop")
        await nav_manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="loop")

        with caplog.at_level(logging.WARNING, logger="ui_router.context"):
            await nav_manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="loop")

        assert any("cycle" in record.message.lower() for record in caplog.records)

    async def test_navigate_to_add_to_history_false(self, nav_manager):
        await nav_manager.initialize(bot_id=1, user_id=1, chat_id=1, initial_scene="scene_a")

        state = await nav_manager.navigate_to(
            bot_id=1, user_id=1, chat_id=1, scene_id="scene_b", add_to_history=False
        )

        assert state.current_scene == "scene_b"
        assert state.history == []

    async def test_go_back_with_history(self, nav_manager):
        await nav_manager.initialize(bot_id=1, user_id=1, chat_id=1, initial_scene="scene_a")
        await nav_manager.navigate_to(bot_id=1, user_id=1, chat_id=1, scene_id="scene_b")

        state, scene_id, transition = await nav_manager.go_back(bot_id=1, user_id=1, chat_id=1)

        assert scene_id == "scene_a"
        assert state.current_scene == "scene_a"
        assert transition == "send"
        assert state.history == []

    async def test_go_back_empty_history(self, nav_manager):
        await nav_manager.initialize(bot_id=1, user_id=1, chat_id=1, initial_scene="scene_a")

        state, scene_id, transition = await nav_manager.go_back(bot_id=1, user_id=1, chat_id=1)

        assert scene_id is None
        assert transition == "send"

    def test_make_bot_id_with_prefix(self):
        manager = NavigationManager(navigation_storage=MagicMock(), prefix="router1")

        assert manager._make_bot_id(42) == "router1:42"

    def test_make_bot_id_without_prefix(self):
        manager = NavigationManager(navigation_storage=MagicMock(), prefix=None)

        assert manager._make_bot_id(42) == "42"
