"""Unit tests for RuntimeConnectionHandler action handlers."""

from __future__ import annotations

import base64
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock

import pytest
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction, RuntimeToLangBotAction


def make_handler(app):
    """Create a RuntimeConnectionHandler with mocked external connection."""
    from langbot.pkg.plugin.handler import RuntimeConnectionHandler

    return RuntimeConnectionHandler(Mock(), AsyncMock(return_value=True), app)


def make_result(first_item=None):
    result = Mock()
    result.first = Mock(return_value=first_item)
    return result


def compiled_params(statement):
    return statement.compile().params


class TestRagRerankAction:
    """Tests for RAG rerank action handler."""

    @pytest.fixture
    def app(self):
        mock_app = Mock()
        mock_app.model_mgr = Mock()
        mock_app.logger = Mock()
        return mock_app

    @pytest.mark.asyncio
    async def test_invokes_rerank_model_and_sorts_scores(self, app):
        """Rerank action uses the selected model and returns top scores."""
        provider = Mock()
        provider.invoke_rerank = AsyncMock(
            return_value=[
                {'index': 0, 'relevance_score': 0.2},
                {'index': 1, 'relevance_score': 0.9},
            ]
        )
        rerank_model = SimpleNamespace(provider=provider)
        app.model_mgr.get_rerank_model_by_uuid = AsyncMock(return_value=rerank_model)
        runtime_handler = make_handler(app)

        response = await runtime_handler.actions[PluginToRuntimeAction.INVOKE_RERANK.value](
            {
                'rerank_model_uuid': 'rerank-1',
                'query': 'hello',
                'documents': ['a', 'b'],
                'top_k': 1,
                'extra_args': {'return_documents': False},
            }
        )

        assert response.code == 0
        assert response.data['results'] == [{'index': 1, 'relevance_score': 0.9}]
        app.model_mgr.get_rerank_model_by_uuid.assert_awaited_once_with('rerank-1')
        provider.invoke_rerank.assert_awaited_once_with(
            model=rerank_model,
            query='hello',
            documents=['a', 'b'],
            extra_args={'return_documents': False},
        )

    @pytest.mark.asyncio
    async def test_returns_error_when_rerank_model_missing(self, app):
        """Missing rerank model returns an action error."""
        app.model_mgr.get_rerank_model_by_uuid = AsyncMock(side_effect=ValueError('not found'))
        runtime_handler = make_handler(app)

        response = await runtime_handler.actions[PluginToRuntimeAction.INVOKE_RERANK.value](
            {
                'rerank_model_uuid': 'missing',
                'query': 'hello',
                'documents': ['a'],
            }
        )

        assert response.code != 0
        assert 'Rerank model with rerank_model_uuid missing not found' in response.message


class TestInitializePluginSettings:
    """Tests for initialize_plugin_settings action handler."""

    @pytest.fixture
    def app(self):
        mock_app = Mock()
        mock_app.persistence_mgr = Mock()
        mock_app.persistence_mgr.execute_async = AsyncMock()
        mock_app.logger = Mock()
        return mock_app

    @pytest.mark.asyncio
    async def test_creates_new_setting_when_not_exists(self, app):
        """New plugin settings use default enabled, priority and config values."""
        runtime_handler = make_handler(app)
        app.persistence_mgr.execute_async.side_effect = [
            make_result(),
            Mock(),
        ]

        response = await runtime_handler.actions[RuntimeToLangBotAction.INITIALIZE_PLUGIN_SETTINGS.value](
            {
                'plugin_author': 'test-author',
                'plugin_name': 'test-plugin',
                'install_source': 'local',
                'install_info': {'path': '/test'},
            }
        )

        assert response.code == 0
        assert app.persistence_mgr.execute_async.await_count == 2
        insert_params = compiled_params(app.persistence_mgr.execute_async.await_args_list[1].args[0])
        assert insert_params == {
            'plugin_author': 'test-author',
            'plugin_name': 'test-plugin',
            'install_source': 'local',
            'install_info': {'path': '/test'},
            'enabled': True,
            'priority': 0,
            'config': {},
        }

    @pytest.mark.asyncio
    async def test_inherits_values_from_existing_setting(self, app):
        """Existing settings are replaced while preserving user-controlled values."""
        runtime_handler = make_handler(app)
        existing_setting = SimpleNamespace(
            enabled=False,
            priority=5,
            config={'key': 'value'},
        )
        app.persistence_mgr.execute_async.side_effect = [
            make_result(existing_setting),
            Mock(),
            Mock(),
        ]

        response = await runtime_handler.actions[RuntimeToLangBotAction.INITIALIZE_PLUGIN_SETTINGS.value](
            {
                'plugin_author': 'test-author',
                'plugin_name': 'test-plugin',
                'install_source': 'github',
                'install_info': {'repo': 'author/name'},
            }
        )

        assert response.code == 0
        assert app.persistence_mgr.execute_async.await_count == 3
        insert_params = compiled_params(app.persistence_mgr.execute_async.await_args_list[2].args[0])
        assert insert_params['enabled'] is False
        assert insert_params['priority'] == 5
        assert insert_params['config'] == {'key': 'value'}
        assert insert_params['install_source'] == 'github'
        assert insert_params['install_info'] == {'repo': 'author/name'}


class TestSetBinaryStorage:
    """Tests for set_binary_storage action handler with size limit validation."""

    @pytest.fixture
    def app(self):
        mock_app = Mock()
        mock_app.instance_config = Mock()
        mock_app.instance_config.data = {
            'plugin': {
                'binary_storage': {
                    'max_value_bytes': 1024,
                },
            },
        }
        mock_app.persistence_mgr = Mock()
        mock_app.persistence_mgr.execute_async = AsyncMock(return_value=make_result())
        mock_app.logger = Mock()
        return mock_app

    @staticmethod
    def payload(value: bytes):
        return {
            'key': 'test-key',
            'owner_type': 'plugin',
            'owner': 'test-owner',
            'value_base64': base64.b64encode(value).decode('utf-8'),
        }

    @pytest.mark.asyncio
    async def test_rejects_value_exceeding_limit(self, app):
        """Values larger than max_value_bytes are rejected before persistence writes."""
        runtime_handler = make_handler(app)

        response = await runtime_handler.actions[RuntimeToLangBotAction.SET_BINARY_STORAGE.value](
            self.payload(b'x' * 2048)
        )

        assert response.code != 0
        assert '2048 > 1024 bytes' in response.message
        app.persistence_mgr.execute_async.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_accepts_value_within_limit_and_inserts_storage(self, app):
        """A new small value is inserted into binary storage."""
        runtime_handler = make_handler(app)

        response = await runtime_handler.actions[RuntimeToLangBotAction.SET_BINARY_STORAGE.value](
            self.payload(b'x' * 512)
        )

        assert response.code == 0
        assert app.persistence_mgr.execute_async.await_count == 2
        insert_params = compiled_params(app.persistence_mgr.execute_async.await_args_list[1].args[0])
        assert insert_params['unique_key'] == 'plugin:test-owner:test-key'
        assert insert_params['value'] == b'x' * 512

    @pytest.mark.asyncio
    async def test_updates_existing_storage(self, app):
        """An existing binary storage row is updated instead of inserted."""
        runtime_handler = make_handler(app)
        app.persistence_mgr.execute_async.return_value = make_result(SimpleNamespace(value=b'old'))

        response = await runtime_handler.actions[RuntimeToLangBotAction.SET_BINARY_STORAGE.value](self.payload(b'new'))

        assert response.code == 0
        assert app.persistence_mgr.execute_async.await_count == 2
        update_params = compiled_params(app.persistence_mgr.execute_async.await_args_list[1].args[0])
        assert update_params['value'] == b'new'

    @pytest.mark.asyncio
    async def test_invalid_max_value_bytes_falls_back_to_default_limit(self, app):
        """Invalid max_value_bytes uses the 10MB default limit."""
        runtime_handler = make_handler(app)
        app.instance_config.data['plugin']['binary_storage']['max_value_bytes'] = 'invalid'

        response = await runtime_handler.actions[RuntimeToLangBotAction.SET_BINARY_STORAGE.value](
            self.payload(b'x' * (10 * 1024 * 1024 + 1))
        )

        assert response.code != 0
        assert '10485761 > 10485760 bytes' in response.message
        app.persistence_mgr.execute_async.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_negative_limit_disables_size_check(self, app):
        """Negative max_value_bytes allows values larger than the normal default."""
        runtime_handler = make_handler(app)
        app.instance_config.data['plugin']['binary_storage']['max_value_bytes'] = -1

        response = await runtime_handler.actions[RuntimeToLangBotAction.SET_BINARY_STORAGE.value](
            self.payload(b'x' * 2048)
        )

        assert response.code == 0
        assert app.persistence_mgr.execute_async.await_count == 2

    @pytest.mark.asyncio
    async def test_zero_limit_rejects_non_empty_values(self, app):
        """A zero byte limit rejects non-empty values."""
        runtime_handler = make_handler(app)
        app.instance_config.data['plugin']['binary_storage']['max_value_bytes'] = 0

        response = await runtime_handler.actions[RuntimeToLangBotAction.SET_BINARY_STORAGE.value](self.payload(b'x'))

        assert response.code != 0
        assert '1 > 0 bytes' in response.message
        app.persistence_mgr.execute_async.assert_not_awaited()


class TestGetPluginSettings:
    """Tests for get_plugin_settings action handler with defaults."""

    @pytest.fixture
    def app(self):
        mock_app = Mock()
        mock_app.persistence_mgr = Mock()
        mock_app.persistence_mgr.execute_async = AsyncMock()
        return mock_app

    @pytest.mark.asyncio
    async def test_returns_defaults_when_setting_not_found(self, app):
        """Default plugin settings are returned when no persisted row exists."""
        runtime_handler = make_handler(app)
        app.persistence_mgr.execute_async.return_value = make_result()

        response = await runtime_handler.actions[RuntimeToLangBotAction.GET_PLUGIN_SETTINGS.value](
            {
                'plugin_author': 'test-author',
                'plugin_name': 'test-plugin',
            }
        )

        assert response.code == 0
        assert response.data == {
            'enabled': True,
            'priority': 0,
            'plugin_config': {},
            'install_source': 'local',
            'install_info': {},
        }

    @pytest.mark.asyncio
    async def test_returns_actual_values_when_setting_exists(self, app):
        """Persisted plugin setting values override defaults."""
        runtime_handler = make_handler(app)
        setting = SimpleNamespace(
            enabled=False,
            priority=10,
            config={'custom': 'config'},
            install_source='github',
            install_info={'repo': 'test/repo'},
        )
        app.persistence_mgr.execute_async.return_value = make_result(setting)

        response = await runtime_handler.actions[RuntimeToLangBotAction.GET_PLUGIN_SETTINGS.value](
            {
                'plugin_author': 'test-author',
                'plugin_name': 'test-plugin',
            }
        )

        assert response.code == 0
        assert response.data == {
            'enabled': False,
            'priority': 10,
            'plugin_config': {'custom': 'config'},
            'install_source': 'github',
            'install_info': {'repo': 'test/repo'},
        }


class TestGetBinaryStorage:
    """Tests for get_binary_storage action handler."""

    @pytest.fixture
    def app(self):
        mock_app = Mock()
        mock_app.persistence_mgr = Mock()
        mock_app.persistence_mgr.execute_async = AsyncMock()
        return mock_app

    @pytest.mark.asyncio
    async def test_returns_base64_encoded_value(self, app):
        """Stored bytes are returned as base64."""
        runtime_handler = make_handler(app)
        app.persistence_mgr.execute_async.return_value = make_result(SimpleNamespace(value=b'test binary content'))

        response = await runtime_handler.actions[RuntimeToLangBotAction.GET_BINARY_STORAGE.value](
            {
                'key': 'test-key',
                'owner_type': 'plugin',
                'owner': 'test-owner',
            }
        )

        assert response.code == 0
        assert response.data == {
            'value_base64': base64.b64encode(b'test binary content').decode('utf-8'),
        }

    @pytest.mark.asyncio
    async def test_returns_error_when_not_found(self, app):
        """Missing binary storage rows return an error response."""
        runtime_handler = make_handler(app)
        app.persistence_mgr.execute_async.return_value = make_result()

        response = await runtime_handler.actions[RuntimeToLangBotAction.GET_BINARY_STORAGE.value](
            {
                'key': 'test-key',
                'owner_type': 'plugin',
                'owner': 'test-owner',
            }
        )

        assert response.code != 0
        assert 'Storage with key test-key not found' in response.message


class TestHandlerQueryLookup:
    """Tests for query lookup in cached_queries."""

    @pytest.fixture
    def app(self):
        mock_app = Mock()
        mock_app.query_pool = Mock()
        mock_app.query_pool.cached_queries = {}
        mock_app.logger = Mock()
        return mock_app

    @pytest.mark.asyncio
    async def test_query_not_found_returns_error(self, app):
        """Query-bound actions return error when query_id is not cached."""
        runtime_handler = make_handler(app)

        response = await runtime_handler.actions[PluginToRuntimeAction.GET_BOT_UUID.value](
            {
                'query_id': 'nonexistent-query',
            }
        )

        assert response.code != 0
        assert 'nonexistent-query' in response.message

    @pytest.mark.asyncio
    async def test_query_found_returns_success(self, app):
        """Query-bound actions read data from the cached query object."""
        runtime_handler = make_handler(app)
        query = SimpleNamespace(variables={}, bot_uuid='test-bot-uuid')
        app.query_pool.cached_queries['existing-query'] = query

        response = await runtime_handler.actions[PluginToRuntimeAction.GET_BOT_UUID.value](
            {
                'query_id': 'existing-query',
            }
        )

        assert response.code == 0
        assert response.data == {'bot_uuid': 'test-bot-uuid'}
