"""
Unit tests for CommandHandler - REAL imports.

Tests the actual CommandHandler class from production code.
Uses tests.utils.import_isolation to break circular import chain safely.
"""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, Mock

from tests.factories import FakeApp, command_query


# ============== FIXTURE USING IMPORT ISOLATION UTILITY ==============


@pytest.fixture(scope='module')
def mock_circular_import_chain():
    """
    Break circular import chain using isolated_sys_modules.

    Chain: handler → core.app → pipeline.controller → http_controller → groups/plugins → taskmgr

    Uses tests.utils.import_isolation for safe, reversible sys.modules manipulation.
    """
    from tests.utils.import_isolation import (
        isolated_sys_modules,
        make_pipeline_handler_import_mocks,
        get_handler_modules_to_clear,
    )

    mocks = make_pipeline_handler_import_mocks()
    clear = get_handler_modules_to_clear('command')

    with isolated_sys_modules(mocks=mocks, clear=clear):
        yield


@pytest.fixture
def fake_app():
    """Create FakeApp instance."""
    return FakeApp()


@pytest.fixture
def mock_event_ctx():
    """Create mock event context."""
    ctx = Mock()
    ctx.is_prevented_default = Mock(return_value=False)
    ctx.event = Mock()
    ctx.event.reply_message_chain = None
    return ctx


@pytest.fixture
def mock_execute_factory():
    """Factory fixture to create mock cmd_mgr.execute generators."""

    def _create_execute(
        text: str | None = 'ok',
        error: str | None = None,
        image_url: str | None = None,
        image_base64: str | None = None,
        file_url: str | None = None,
    ):
        async def mock_execute(command_text, full_command_text, query, session):
            ret = Mock()
            ret.text = text
            ret.error = error
            ret.image_url = image_url
            ret.image_base64 = image_base64
            ret.file_url = file_url
            yield ret

        return mock_execute

    return _create_execute


# ============== CACHED LAZY IMPORTS ==============

_command_handler_module = None
_entities_module = None


def get_command_handler():
    """Import CommandHandler after circular import chain is mocked."""
    global _command_handler_module
    if _command_handler_module is None:
        from importlib import import_module

        _command_handler_module = import_module('langbot.pkg.pipeline.process.handlers.command')
    return _command_handler_module


def get_entities():
    """Import pipeline entities - uses real module."""
    global _entities_module
    if _entities_module is None:
        from importlib import import_module

        _entities_module = import_module('langbot.pkg.pipeline.entities')
    return _entities_module


# ============== REAL CommandHandler Tests ==============


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestCommandHandlerReal:
    """Tests for real CommandHandler class."""

    @pytest.mark.asyncio
    async def test_real_import_works(self):
        """Verify we can import the real handler class."""
        command = get_command_handler()
        assert hasattr(command, 'CommandHandler')
        handler_cls = command.CommandHandler
        assert handler_cls.__name__ == 'CommandHandler'

    @pytest.mark.asyncio
    async def test_handler_creation(self, fake_app):
        """CommandHandler can be instantiated."""
        command = get_command_handler()
        handler = command.CommandHandler(fake_app)
        assert handler.ap is fake_app

    @pytest.mark.asyncio
    async def test_command_parsing_extracts_command_name(self, fake_app, mock_event_ctx):
        """Command text is extracted after prefix."""
        command = get_command_handler()
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)

        executed_commands = []

        async def track_execute(command_text, full_command_text, query, session):
            executed_commands.append(command_text)
            ret = Mock()
            ret.text = 'ok'
            ret.error = None
            ret.image_url = None
            ret.image_base64 = None
            ret.file_url = None
            yield ret

        fake_app.cmd_mgr.execute = track_execute

        handler = command.CommandHandler(fake_app)
        query = command_query('help arg1 arg2')

        results = []
        async for result in handler.handle(query):
            results.append(result)

        assert executed_commands[0] == 'help arg1 arg2'

    @pytest.mark.asyncio
    async def test_admin_privilege_check(self, fake_app, mock_event_ctx, mock_execute_factory):
        """A per-bot admin from the database is marked as admin in command events."""
        from langbot_plugin.api.entities.builtin.provider.session import LauncherTypes

        command = get_command_handler()

        admin_result = Mock()
        admin_result.first.return_value = Mock()
        fake_app.persistence_mgr.execute_async = AsyncMock(return_value=admin_result)
        fake_app.instance_config.data = {}
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory()

        handler = command.CommandHandler(fake_app)
        query = command_query('status')
        query.bot_uuid = 'bot-1'
        query.launcher_type = LauncherTypes.PERSON
        query.launcher_id = 12345

        results = []
        async for result in handler.handle(query):
            results.append(result)

        fake_app.persistence_mgr.execute_async.assert_awaited_once()
        call_args = fake_app.plugin_connector.emit_event.call_args
        event = call_args[0][0]
        assert event.is_admin is True

    @pytest.mark.asyncio
    async def test_non_admin_privilege_check(self, fake_app, mock_event_ctx, mock_execute_factory):
        """A launcher absent from the per-bot admin table is not an admin."""
        from langbot_plugin.api.entities.builtin.provider.session import LauncherTypes

        command = get_command_handler()

        admin_result = Mock()
        admin_result.first.return_value = None
        fake_app.persistence_mgr.execute_async = AsyncMock(return_value=admin_result)
        fake_app.instance_config.data = {}
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory()

        handler = command.CommandHandler(fake_app)
        query = command_query('status')
        query.bot_uuid = 'bot-1'
        query.launcher_type = LauncherTypes.PERSON
        query.launcher_id = 67890

        results = []
        async for result in handler.handle(query):
            results.append(result)

        fake_app.persistence_mgr.execute_async.assert_awaited_once()
        call_args = fake_app.plugin_connector.emit_event.call_args
        event = call_args[0][0]
        assert event.is_admin is False

    @pytest.mark.asyncio
    async def test_prevent_default_with_reply_continues(self, fake_app, mock_event_ctx):
        """prevent_default with reply yields CONTINUE."""
        from tests.factories.message import text_chain

        command = get_command_handler()
        entities = get_entities()

        reply_chain = text_chain('plugin reply')
        mock_event_ctx.is_prevented_default.return_value = True
        mock_event_ctx.event.reply_message_chain = reply_chain
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)

        handler = command.CommandHandler(fake_app)
        query = command_query('test')
        query.resp_messages = []

        results = []
        async for result in handler.handle(query):
            results.append(result)

        assert len(results) == 1
        assert results[0].result_type == entities.ResultType.CONTINUE
        assert len(query.resp_messages) == 1
        assert query.resp_messages[0] == reply_chain

    @pytest.mark.asyncio
    async def test_prevent_default_without_reply_interrupts(self, fake_app, mock_event_ctx):
        """prevent_default without reply yields INTERRUPT."""
        command = get_command_handler()
        entities = get_entities()

        mock_event_ctx.is_prevented_default.return_value = True
        mock_event_ctx.event.reply_message_chain = None
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)

        handler = command.CommandHandler(fake_app)
        query = command_query('test')

        results = []
        async for result in handler.handle(query):
            results.append(result)

        assert len(results) == 1
        assert results[0].result_type == entities.ResultType.INTERRUPT

    @pytest.mark.asyncio
    async def test_event_type_person_command(self, fake_app, mock_event_ctx, mock_execute_factory):
        """Person launcher creates PersonCommandSent event."""
        from langbot_plugin.api.entities.builtin.provider.session import LauncherTypes
        from langbot_plugin.api.entities import events

        command = get_command_handler()
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory()

        handler = command.CommandHandler(fake_app)
        query = command_query('help')
        query.launcher_type = LauncherTypes.PERSON

        results = []
        async for result in handler.handle(query):
            results.append(result)

        call_args = fake_app.plugin_connector.emit_event.call_args
        event = call_args[0][0]
        assert isinstance(event, events.PersonCommandSent)

    @pytest.mark.asyncio
    async def test_event_type_group_command(self, fake_app, mock_event_ctx, mock_execute_factory):
        """Group launcher creates GroupCommandSent event."""
        from langbot_plugin.api.entities.builtin.provider.session import LauncherTypes
        from langbot_plugin.api.entities import events

        command = get_command_handler()
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory()

        handler = command.CommandHandler(fake_app)
        query = command_query('help')
        query.launcher_type = LauncherTypes.GROUP

        results = []
        async for result in handler.handle(query):
            results.append(result)

        call_args = fake_app.plugin_connector.emit_event.call_args
        event = call_args[0][0]
        assert isinstance(event, events.GroupCommandSent)

    @pytest.mark.asyncio
    async def test_command_result_text(self, fake_app, mock_event_ctx, mock_execute_factory):
        """Text result is added to resp_messages."""
        command = get_command_handler()
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory(text='Command output')

        handler = command.CommandHandler(fake_app)
        query = command_query('echo')
        query.resp_messages = []

        results = []
        async for result in handler.handle(query):
            results.append(result)

        assert len(query.resp_messages) == 1
        msg = query.resp_messages[0]
        assert msg.role == 'command'
        assert len(msg.content) == 1
        assert msg.content[0].type == 'text'
        assert msg.content[0].text == 'Command output'

    @pytest.mark.asyncio
    async def test_command_result_error(self, fake_app, mock_event_ctx, mock_execute_factory):
        """Error result creates error message."""
        command = get_command_handler()
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory(text=None, error='Command failed')

        handler = command.CommandHandler(fake_app)
        query = command_query('fail')
        query.resp_messages = []

        results = []
        async for result in handler.handle(query):
            results.append(result)

        assert len(query.resp_messages) == 1
        msg = query.resp_messages[0]
        assert msg.role == 'command'
        assert msg.content == 'Command failed'

    @pytest.mark.asyncio
    async def test_command_result_image_url(self, fake_app, mock_event_ctx, mock_execute_factory):
        """Image URL result is added to content."""
        command = get_command_handler()
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory(
            text='Here is the image:', image_url='https://example.com/image.png'
        )

        handler = command.CommandHandler(fake_app)
        query = command_query('image')
        query.resp_messages = []

        results = []
        async for result in handler.handle(query):
            results.append(result)

        msg = query.resp_messages[0]
        assert len(msg.content) == 2
        assert msg.content[0].type == 'text'
        assert msg.content[1].type == 'image_url'

    @pytest.mark.asyncio
    async def test_command_result_empty_interrupts(self, fake_app, mock_event_ctx, mock_execute_factory):
        """Empty result yields INTERRUPT."""
        command = get_command_handler()
        entities = get_entities()
        fake_app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
        fake_app.cmd_mgr.execute = mock_execute_factory(text=None)

        handler = command.CommandHandler(fake_app)
        query = command_query('empty')

        results = []
        async for result in handler.handle(query):
            results.append(result)

        assert results[0].result_type == entities.ResultType.INTERRUPT


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestCommandHandlerHelper:
    """Tests for helper methods."""

    def test_cut_str_short(self, fake_app):
        """cut_str returns short string unchanged."""
        command = get_command_handler()
        handler = command.CommandHandler(fake_app)
        result = handler.cut_str('short text')
        assert result == 'short text'

    def test_cut_str_long(self, fake_app):
        """cut_str truncates long string."""
        command = get_command_handler()
        handler = command.CommandHandler(fake_app)
        result = handler.cut_str('this is a very long string that exceeds twenty characters')
        assert '...' in result
        assert len(result) <= 23

    def test_cut_str_multiline(self, fake_app):
        """cut_str truncates multiline string."""
        command = get_command_handler()
        handler = command.CommandHandler(fake_app)
        result = handler.cut_str('first line\nsecond line')
        assert '...' in result
