"""
Tests for service components.

This module tests all service classes including NavigationService,
which provides navigation-related functionality.
"""

import pytest

from ui_router.state.context import NavigationManager, NavigationState
from ui_router.state.storage import InMemoryNavigationStorage
from ui_router.schema import TransitionType, LocalizedTemplate, DynamicContent
from ui_router.services import NavigationService


class TestNavigationService:
    """Tests for NavigationService."""

    @pytest.fixture
    def storage(self) -> InMemoryNavigationStorage:
        """Create an in-memory navigation storage."""
        return InMemoryNavigationStorage()

    @pytest.fixture
    def manager(self, storage: InMemoryNavigationStorage) -> NavigationManager:
        """Create a NavigationManager."""
        return NavigationManager(navigation_storage=storage)

    @pytest.fixture
    def service(self, manager: NavigationManager) -> NavigationService:
        """Create a NavigationService."""
        return NavigationService(navigation_manager=manager)

    @pytest.mark.asyncio
    async def test_initialize(self, service: NavigationService) -> None:
        """NavigationService should initialize navigation state."""
        state = await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

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

    @pytest.mark.asyncio
    async def test_get_current_state_after_init(
        self,
        service: NavigationService,
    ) -> None:
        """NavigationService should retrieve initialized state."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        state = await service.get_current_state(
            bot_id=123,
            user_id=456,
            chat_id=456,
        )

        assert state.current_scene == "start"

    @pytest.mark.asyncio
    async def test_navigate_to(self, service: NavigationService) -> None:
        """NavigationService should navigate to a new scene."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        state = await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="menu",
            save_to_history=True,
            transition_type=TransitionType.SEND,
        )

        assert state.current_scene == "menu"
        assert "start" in state.history

    @pytest.mark.asyncio
    async def test_navigate_without_history(self, service: NavigationService) -> None:
        """NavigationService should navigate without saving to history."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        state = await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="menu",
            save_to_history=False,
        )

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

    @pytest.mark.asyncio
    async def test_navigate_multiple_scenes(self, service: NavigationService) -> None:
        """NavigationService should handle multiple navigations."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="menu",
            save_to_history=True,
        )

        state = await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="settings",
            save_to_history=True,
        )

        assert state.current_scene == "settings"
        assert state.history == ["start", "menu"]

    @pytest.mark.asyncio
    async def test_go_back_single_step(self, service: NavigationService) -> None:
        """NavigationService should go back one step."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="menu",
        )

        state, prev_scene, transition = await service.go_back(
            bot_id=123,
            user_id=456,
            chat_id=456,
        )

        assert prev_scene == "start"
        assert state.current_scene == "start"
        assert state.history == []

    @pytest.mark.asyncio
    async def test_go_back_multiple_steps(self, service: NavigationService) -> None:
        """NavigationService should handle multiple go_back operations."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="menu",
        )

        await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="settings",
        )

        # Go back to menu
        state, prev_scene, transition = await service.go_back(
            bot_id=123,
            user_id=456,
            chat_id=456,
        )

        assert prev_scene == "menu"
        assert state.current_scene == "menu"

        # Go back to start
        state, prev_scene, transition = await service.go_back(
            bot_id=123,
            user_id=456,
            chat_id=456,
        )

        assert prev_scene == "start"
        assert state.current_scene == "start"

    @pytest.mark.asyncio
    async def test_go_back_empty_history(self, service: NavigationService) -> None:
        """NavigationService should handle go_back with empty history."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        state, prev_scene, transition = await service.go_back(
            bot_id=123,
            user_id=456,
            chat_id=456,
        )

        assert prev_scene is None
        assert state.current_scene == "start"

    @pytest.mark.asyncio
    async def test_navigate_different_users(self, service: NavigationService) -> None:
        """NavigationService should keep separate states for different users."""
        # User 1
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="user1_scene",
        )

        # User 2
        await service.initialize(
            bot_id=123,
            user_id=789,
            chat_id=789,
            initial_scene="start",
        )

        await service.navigate_to(
            bot_id=123,
            user_id=789,
            chat_id=789,
            scene_id="user2_scene",
        )

        # Check user 1
        state1 = await service.get_current_state(
            bot_id=123,
            user_id=456,
            chat_id=456,
        )

        assert state1.current_scene == "user1_scene"

        # Check user 2
        state2 = await service.get_current_state(
            bot_id=123,
            user_id=789,
            chat_id=789,
        )

        assert state2.current_scene == "user2_scene"

    @pytest.mark.asyncio
    async def test_navigate_different_bots(self, service: NavigationService) -> None:
        """NavigationService should keep separate states for different bots."""
        # Bot 1
        await service.initialize(
            bot_id=111,
            user_id=456,
            chat_id=456,
            initial_scene="bot1_start",
        )

        # Bot 2
        await service.initialize(
            bot_id=222,
            user_id=456,
            chat_id=456,
            initial_scene="bot2_start",
        )

        # Check bot 1
        state1 = await service.get_current_state(
            bot_id=111,
            user_id=456,
            chat_id=456,
        )

        assert state1.current_scene == "bot1_start"

        # Check bot 2
        state2 = await service.get_current_state(
            bot_id=222,
            user_id=456,
            chat_id=456,
        )

        assert state2.current_scene == "bot2_start"

    @pytest.mark.asyncio
    async def test_save_state(self, service: NavigationService) -> None:
        """NavigationService should save state."""
        state = NavigationState(
            current_scene="menu",
            history=["start"],
        )

        await service.save_state(
            bot_id=123,
            user_id=456,
            chat_id=456,
            state=state,
        )

        retrieved = await service.get_current_state(
            bot_id=123,
            user_id=456,
            chat_id=456,
        )

        assert retrieved.current_scene == "menu"
        assert retrieved.history == ["start"]

    @pytest.mark.asyncio
    async def test_transition_types(self, service: NavigationService) -> None:
        """NavigationService should handle different transition types."""
        await service.initialize(
            bot_id=123,
            user_id=456,
            chat_id=456,
            initial_scene="start",
        )

        # Test SEND transition
        state = await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="menu",
            transition_type=TransitionType.SEND,
        )

        assert state.transitions == [TransitionType.SEND.value]

        # Test EDIT_SMART transition
        state = await service.navigate_to(
            bot_id=123,
            user_id=456,
            chat_id=456,
            scene_id="settings",
            transition_type=TransitionType.EDIT_SMART,
        )

        assert state.transitions == [TransitionType.SEND.value, TransitionType.EDIT_SMART.value]

    @pytest.mark.asyncio
    async def test_service_delegation(self, service: NavigationService) -> None:
        """NavigationService should delegate to NavigationManager."""
        assert service.navigation_manager is not None
        assert isinstance(service.navigation_manager, NavigationManager)


class TestContentService:
    """Tests for ContentService."""

    @pytest.fixture
    def content_service(self):
        """Create a ContentService instance."""
        from unittest.mock import MagicMock
        from ui_router.services import ContentService

        content_resolver = MagicMock()

        return ContentService(
            content_resolver=content_resolver,
        )

    @pytest.mark.asyncio
    async def test_can_edit_media_type_text_to_text(
        self,
        content_service,
    ) -> None:
        """ContentService should identify text-to-text as editable."""
        from unittest.mock import MagicMock

        old_message = MagicMock()
        old_message.photo = None
        old_message.video = None
        old_message.document = None
        old_message.audio = None

        new_media = MagicMock()
        new_media.type = None

        result = content_service._can_edit_media_type(old_message, new_media)
        assert result is False

    @pytest.mark.asyncio
    async def test_can_edit_media_type_photo_to_photo(
        self,
        content_service,
    ) -> None:
        """ContentService should allow edit for same media types."""
        from unittest.mock import MagicMock

        old_message = MagicMock()
        old_message.photo = MagicMock()
        old_message.video = None
        old_message.document = None
        old_message.audio = None
        old_message.animation = None

        new_media = MagicMock()
        new_media.type = "photo"

        result = content_service._can_edit_media_type(old_message, new_media)
        assert result is True

    @pytest.mark.asyncio
    async def test_can_edit_media_type_photo_to_video(
        self,
        content_service,
    ) -> None:
        """ContentService should disallow edit for different media types."""
        from unittest.mock import MagicMock

        old_message = MagicMock()
        old_message.photo = MagicMock()
        old_message.video = None
        old_message.document = None
        old_message.audio = None
        old_message.animation = None

        new_media = MagicMock()
        new_media.type = "video"

        result = content_service._can_edit_media_type(old_message, new_media)
        assert result is False

    @pytest.mark.asyncio
    async def test_send_content_text_only(
        self,
        content_service,
    ) -> None:
        """ContentService should send text-only messages."""
        from unittest.mock import AsyncMock, MagicMock

        bot = AsyncMock()
        context = MagicMock()

        content = MagicMock()
        content.media = None
        content.parse_mode = "HTML"

        await content_service._send_content(
            bot=bot,
            content=content,
            text="Hello",
            keyboard=None,
            user_id=123,
        )

        bot.send_message.assert_called_once()
        call_kwargs = bot.send_message.call_args[1]
        assert call_kwargs["text"] == "Hello"
        assert call_kwargs["chat_id"] == 123

    @pytest.mark.asyncio
    async def test_send_content_with_photo(
        self,
        content_service,
    ) -> None:
        """ContentService should send messages with photos."""
        from unittest.mock import AsyncMock, MagicMock
        from ui_router.schema import MediaContent

        bot = AsyncMock()
        context = MagicMock()

        media = MediaContent(
            type="photo",
            source="https://example.com/photo.jpg",
        )
        content = MagicMock()
        content.media = media
        content.parse_mode = "HTML"

        await content_service._send_content(
            bot=bot,
            content=content,
            text="Photo caption",
            keyboard=None,
            user_id=123,
        )

        bot.send_photo.assert_called_once()
        call_kwargs = bot.send_photo.call_args[1]
        assert call_kwargs["caption"] == "Photo caption"
        assert call_kwargs["chat_id"] == 123

    @pytest.mark.asyncio
    async def test_send_content_with_video(
        self,
        content_service,
    ) -> None:
        """ContentService should send messages with videos."""
        from unittest.mock import AsyncMock, MagicMock
        from ui_router.schema import MediaContent

        bot = AsyncMock()
        context = MagicMock()

        media = MediaContent(
            type="video",
            source="https://example.com/video.mp4",
        )
        content = MagicMock()
        content.media = media
        content.parse_mode = "HTML"

        await content_service._send_content(
            bot=bot,
            content=content,
            text="Video caption",
            keyboard=None,
            user_id=123,
        )

        bot.send_video.assert_called_once()
        call_kwargs = bot.send_video.call_args[1]
        assert call_kwargs["caption"] == "Video caption"

    @pytest.mark.asyncio
    async def test_resolve_scene_content_default(
        self,
        content_service,
    ) -> None:
        """ContentService should return default content when no conditions match."""
        from unittest.mock import AsyncMock, MagicMock
        from ui_router.schema import Scene, MessageContent

        default_content = MessageContent(text="Default")
        scene = MagicMock()
        scene.conditional_content = None
        scene.default_content = default_content
        scene.default_keyboard = None

        context = MagicMock()

        content, keyboard = await content_service.resolve_scene_content(
            scene=scene,
            context=context,
            get_variable_fn=AsyncMock(return_value="value"),
            resolve_condition_value_fn=AsyncMock(return_value="value"),
        )

        assert content == default_content
        assert keyboard is None

    @pytest.mark.asyncio
    async def test_send_additional_messages(
        self,
        content_service,
    ) -> None:
        """ContentService should send additional messages."""
        from unittest.mock import AsyncMock, MagicMock
        from ui_router.schema import MessageContent

        bot = AsyncMock()
        context = MagicMock()
        context.bot = bot

        messages = [
            MessageContent(text="Message 1"),
            MessageContent(text="Message 2"),
        ]

        content_service.content_resolver.resolve_string = MagicMock(
            side_effect=lambda x, y: x if isinstance(x, str) else x
        )

        await content_service.send_additional_messages(
            messages=messages,
            user_id=123,
            context=context,
        )

        assert bot.send_message.call_count == 2


class TestLocalizationService:
    """Tests for LocalizationService."""

    @pytest.fixture
    def schema_with_localization(self) -> "UIRouter":
        """Create a UIRouter schema with localization."""
        from ui_router.schema import (
            UIRouter,
            Scene,
            MessageContent,
            Keyboard,
            Button,
            LocalizedTemplate,
            DynamicContent,
        )

        # Create a localized template
        localized_template = LocalizedTemplate(
            translations={
                "en": "Hello, {name}!",
                "ru": "Привет, {name}!",
            },
            flags=["name"],
            locale_flag="locale",
            default_locale="en",
        )

        # Create dynamic content with localized template
        dynamic_content = DynamicContent(
            type="localized_template",
            localized_template=localized_template,
        )

        # Create scene with localized content
        scene = Scene(
            id="start",
            name="Start Scene",
            default_content=MessageContent(text=dynamic_content),
            default_keyboard=Keyboard(buttons=[[Button(text="OK")]]),
        )

        return UIRouter(
            name="test_bot",
            enable_fluent=False,  # We test with Fluent disabled first
            scenes=[scene],
            initial_scene="start",
        )

    @pytest.fixture
    def localization_service(
        self, schema_with_localization: "UIRouter"
    ) -> "LocalizationService":
        """Create a LocalizationService."""
        from ui_router.services import LocalizationService

        return LocalizationService(
            schema=schema_with_localization,
            enable_fluent=False,
        )

    def test_initialization_without_fluent(
        self, localization_service: "LocalizationService"
    ) -> None:
        """LocalizationService should initialize without Fluent."""
        assert localization_service.enable_fluent is False
        assert localization_service.fluent_bundles is None

    def test_get_fluent_id(self, localization_service: "LocalizationService") -> None:
        """LocalizationService should generate consistent Fluent IDs."""
        from ui_router.schema import LocalizedTemplate

        template = LocalizedTemplate(
            translations={
                "en": "Test message",
                "ru": "Тестовое сообщение",
            },
            flags=["var"],
        )

        id1 = localization_service._get_fluent_id(template)
        id2 = localization_service._get_fluent_id(template)

        # Should generate consistent IDs
        assert id1 == id2
        # ID should have the right format
        assert id1.startswith("msg_")
        assert len(id1) == 16  # msg_ + 12 hex chars

    def test_convert_to_fluent_format(
        self, localization_service: "LocalizationService"
    ) -> None:
        """LocalizationService should convert Python format to Fluent format."""
        text = "Hello, {name}! You have {count} messages."
        flags = ["name", "count"]

        result = localization_service._convert_to_fluent_format(text, flags)

        # Should replace {var} with { $var }
        assert "{ $name }" in result
        assert "{ $count }" in result
        assert "{name}" not in result
        assert "{count}" not in result

    def test_get_bundle_not_available(
        self, localization_service: "LocalizationService"
    ) -> None:
        """LocalizationService should return None for unavailable bundle."""
        bundle = localization_service.get_bundle("xx")
        assert bundle is None

    def test_reload_fluent_bundles_disabled(
        self, localization_service: "LocalizationService"
    ) -> None:
        """LocalizationService should handle reload when Fluent is disabled."""
        # Should not raise any errors
        localization_service.reload_fluent_bundles()
        assert localization_service.fluent_bundles is None
