import pytest

pytest.skip("Skipping legacy Messager tests due to new API", allow_module_level=True)
from unittest.mock import AsyncMock, patch, MagicMock
from typing import Dict, Any
import sys
import os

# Add src to path to fix import issues
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))

from argentic.core.messager.messager import Messager
from argentic.core.messager.protocols import MessagerProtocol
from argentic.core.protocol.message import BaseMessage
from argentic.core.logger import LogLevel


class MockBaseMessage(BaseMessage):
    """Mock implementation of BaseMessage for testing"""

    message: str = "test_message"


@pytest.fixture
def messager_config() -> Dict[str, Any]:
    """Default configuration for testing Messager"""
    return {
        "broker_address": "localhost",
        "port": 1883,
        "protocol": MessagerProtocol.MQTT,
        "client_id": "test-client",
        "username": "test_user",
        "password": "test_pass",
        "keepalive": 60,
        "pub_log_topic": "logs/test",
        "log_level": LogLevel.DEBUG.value,
    }


@pytest.mark.asyncio
class TestMessager:
    """Tests for the Messager class"""

    def setup_method(self):
        """Setup before each test method"""
        # Create a mock driver that we'll use for all tests
        self.driver = AsyncMock()

        # Make is_connected a synchronous method
        self.driver.is_connected = MagicMock()
        self.driver.is_connected.return_value = False

        # Set up return values for async methods
        self.driver.connect.return_value = None
        self.driver.disconnect.return_value = None
        self.driver.publish.return_value = None
        self.driver.subscribe.return_value = None
        self.driver.unsubscribe.return_value = None

    @patch("argentic.core.messager.messager.create_driver")
    async def test_init(self, mock_create_driver, messager_config):
        """Test Messager initialization"""
        mock_create_driver.return_value = self.driver

        messager = Messager(**messager_config)

        # Verify driver was created with correct config
        mock_create_driver.assert_called_once()
        assert messager.broker_address == messager_config["broker_address"]
        assert messager.port == messager_config["port"]
        assert messager.client_id == messager_config["client_id"]
        assert messager.username == messager_config["username"]
        assert messager.password == messager_config["password"]
        assert messager.log_level == messager_config["log_level"]

    @patch("argentic.core.messager.messager.create_driver")
    @patch("argentic.core.messager.messager.ssl")
    async def test_init_with_tls(self, mock_ssl, mock_create_driver, messager_config):
        """Test Messager initialization with TLS configuration"""
        mock_create_driver.return_value = self.driver

        # Configure SSL mocks
        mock_ssl.CERT_REQUIRED = "CERT_REQUIRED_VALUE"
        mock_ssl.PROTOCOL_TLS = "PROTOCOL_TLS_VALUE"

        # Add TLS parameters to config
        tls_config = messager_config.copy()
        tls_config["tls_params"] = {
            "ca_certs": "/path/to/ca.crt",
            "certfile": "/path/to/client.crt",
            "keyfile": "/path/to/client.key",
            "cert_reqs": "CERT_REQUIRED",
            "tls_version": "PROTOCOL_TLS",
            "ciphers": "HIGH:!aNULL:!MD5",
        }

        messager = Messager(**tls_config)

        # Verify TLS parameters were properly configured
        assert messager._tls_params["ca_certs"] == "/path/to/ca.crt"
        assert messager._tls_params["certfile"] == "/path/to/client.crt"
        assert messager._tls_params["keyfile"] == "/path/to/client.key"
        assert messager._tls_params["cert_reqs"] == "CERT_REQUIRED_VALUE"
        assert messager._tls_params["tls_version"] == "PROTOCOL_TLS_VALUE"
        assert messager._tls_params["ciphers"] == "HIGH:!aNULL:!MD5"

    @patch("argentic.core.messager.messager.create_driver")
    @patch("argentic.core.messager.messager.ssl")
    async def test_init_with_invalid_tls(self, mock_ssl, mock_create_driver, messager_config):
        """Test Messager initialization with invalid TLS configuration"""
        mock_create_driver.return_value = self.driver

        # Configure ssl module to raise AttributeError for invalid attributes
        # We can't directly set __getattr__, so use a specific technique for this test
        mock_ssl.CERT_REQUIRED = "CERT_REQUIRED_VALUE"
        mock_ssl.PROTOCOL_TLS = "PROTOCOL_TLS_VALUE"

        # When getattr is called with invalid options, it should raise AttributeError
        def getattr_side_effect(obj, name, default):
            if name == "INVALID_CERT_OPTION" or name == "INVALID_TLS_VERSION":
                raise AttributeError(f"module 'ssl' has no attribute '{name}'")
            return default

        # Patch the getattr function that's used in the TLS params code
        with patch("argentic.core.messager.messager.getattr", side_effect=getattr_side_effect):
            # Add invalid TLS parameters to config
            invalid_tls_config = messager_config.copy()
            invalid_tls_config["tls_params"] = {
                "cert_reqs": "INVALID_CERT_OPTION",  # Invalid option
                "tls_version": "INVALID_TLS_VERSION",  # Invalid option
            }

            # Invalid TLS config should raise ValueError
            with pytest.raises(ValueError) as excinfo:
                Messager(**invalid_tls_config)

            # Verify error message contains useful information
            assert "Invalid TLS configuration" in str(excinfo.value)

    @patch("argentic.core.messager.messager.create_driver")
    async def test_is_connected(self, mock_create_driver, messager_config):
        """Test is_connected method"""
        mock_create_driver.return_value = self.driver
        self.driver.is_connected.return_value = True

        messager = Messager(**messager_config)

        assert messager.is_connected() is True
        self.driver.is_connected.assert_called_once()

    @patch("argentic.core.messager.messager.create_driver")
    async def test_connect_success(self, mock_create_driver, messager_config):
        """Test successful connection"""
        mock_create_driver.return_value = self.driver
        self.driver.connect = AsyncMock(return_value=None)  # Successful connection

        messager = Messager(**messager_config)
        result = await messager.connect()

        assert result is True
        self.driver.connect.assert_called_once()

    @patch("argentic.core.messager.messager.create_driver")
    async def test_connect_failure(self, mock_create_driver, messager_config):
        """Test failed connection"""
        mock_create_driver.return_value = self.driver
        self.driver.connect = AsyncMock(side_effect=Exception("Connection failed"))

        messager = Messager(**messager_config)
        result = await messager.connect()

        assert result is False
        self.driver.connect.assert_called_once()

    @patch("argentic.core.messager.messager.create_driver")
    async def test_disconnect(self, mock_create_driver, messager_config):
        """Test disconnect method"""
        mock_create_driver.return_value = self.driver

        # Make sure disconnect returns properly when awaited
        self.driver.disconnect.return_value = None
        # Make it return immediately when awaited
        self.driver.disconnect.__await__ = MagicMock(return_value=iter([None]))

        messager = Messager(**messager_config)
        await messager.disconnect()

        self.driver.disconnect.assert_called_once()

    @patch("argentic.core.messager.messager.create_driver")
    async def test_publish(self, mock_create_driver, messager_config):
        """Test publish method"""
        mock_create_driver.return_value = self.driver

        messager = Messager(**messager_config)
        test_topic = "test/topic"
        test_message = MockBaseMessage()
        test_qos = 1
        test_retain = True

        await messager.publish(test_topic, test_message, test_qos, test_retain)

        self.driver.publish.assert_called_once_with(
            test_topic, test_message, qos=test_qos, retain=test_retain
        )

    @patch("argentic.core.messager.messager.create_driver")
    @patch("argentic.core.messager.messager.asyncio.create_task")
    async def test_subscribe(self, mock_create_task, mock_create_driver, messager_config):
        """Test subscribe method"""
        mock_create_driver.return_value = self.driver
        # Make create_task return the coroutine itself for simplicity
        mock_create_task.side_effect = lambda coro: coro

        # Make sure subscribe resolves properly
        self.driver.subscribe.return_value = None
        # Make it return immediately when awaited
        self.driver.subscribe.__await__ = MagicMock(return_value=iter([None]))

        messager = Messager(**messager_config)
        test_topic = "test/subscribe"
        test_handler = AsyncMock()
        test_handler.return_value = None  # Ensure it returns immediately when awaited

        await messager.subscribe(test_topic, test_handler, MockBaseMessage)

        # Verify the subscribe call
        self.driver.subscribe.assert_called_once()
        assert self.driver.subscribe.call_args[0][0] == test_topic
        # The second arg is the handler_adapter function, which we can't directly test

        # Test the handler_adapter by calling it with valid payload
        handler_adapter = self.driver.subscribe.call_args[0][1]

        # Create a mock message payload
        message = MockBaseMessage(message="test message")
        payload = message.model_dump_json().encode("utf-8")

        # Call the handler_adapter with the mock payload
        await handler_adapter(payload)

        # Verify create_task was called with the handler and message
        mock_create_task.assert_called_once()

    @patch("argentic.core.messager.messager.create_driver")
    @patch("argentic.core.messager.messager.asyncio.create_task")
    @pytest.mark.parametrize(
        "payload,should_match_specific",
        [
            ('{"id":"123","type":"MockBaseMessage","message":"test_message"}', True),
            ('{"id":"123","type":"NotTheRightType","message":"test_message"}', False),
            ('{"id":"123","message":"test_message"}', False),  # Missing type field
            (
                '{"id":"123","type":"MockBaseMessage","message":"test_message","extra":"field"}',
                True,
            ),
        ],
    )
    async def test_subscribe_message_type_handling(
        self, mock_create_task, mock_create_driver, payload, should_match_specific, messager_config
    ):
        """Test subscribe method's handler_adapter message type processing"""
        mock_create_driver.return_value = self.driver

        # Save the task that would be created so we can examine it
        created_tasks = []
        mock_create_task.side_effect = lambda coro: created_tasks.append(coro) or coro

        messager = Messager(**messager_config)
        test_topic = "test/subscribe/types"

        # Create handler for specific type
        specific_handler = AsyncMock()
        specific_handler.__name__ = "specific_handler"

        # Mock the driver.subscribe to capture the handler_adapter
        async def capture_handler_adapter(topic, handler):
            # Store the handler for later use
            self.captured_handler = handler

        self.driver.subscribe.side_effect = capture_handler_adapter

        # Subscribe with specific message type
        await messager.subscribe(test_topic, specific_handler, MockBaseMessage)

        # Now we can call the handler_adapter directly with different payloads
        await self.captured_handler(payload.encode("utf-8"))

        # The handler itself should not be called directly, but rather passed to create_task
        if should_match_specific:
            assert len(created_tasks) > 0
            # Execute the task that would have been created
            for task in created_tasks:
                await task
            specific_handler.assert_awaited_once()
        else:
            specific_handler.assert_not_awaited()

        # Reset for next test
        specific_handler.reset_mock()

    @patch("argentic.core.messager.messager.create_driver")
    @patch("argentic.core.messager.messager.asyncio.create_task")
    async def test_subscribe_with_invalid_json(
        self, mock_create_task, mock_create_driver, messager_config
    ):
        """Test subscribe method's handler_adapter with invalid JSON"""
        mock_create_driver.return_value = self.driver

        # Save the task that would be created so we can examine it
        created_tasks = []
        mock_create_task.side_effect = lambda coro: created_tasks.append(coro) or coro

        messager = Messager(**messager_config)
        test_topic = "test/subscribe/invalid"

        # Create a handler
        handler = AsyncMock()
        handler.__name__ = "test_handler"

        # Mock the driver.subscribe to capture the handler_adapter
        async def capture_handler_adapter(topic, handler):
            # Store the handler for later use
            self.captured_handler = handler

        self.driver.subscribe.side_effect = capture_handler_adapter

        # Subscribe with specific message type
        await messager.subscribe(test_topic, handler, MockBaseMessage)

        # Invalid JSON should be silently ignored (no exceptions)
        invalid_payloads = [
            b"This is not JSON",
            b"{invalid: json}",
            b"[]",  # Empty array
            b"null",  # JSON null
            b"",  # Empty string
        ]

        for invalid_payload in invalid_payloads:
            # This should not raise exceptions
            await self.captured_handler(invalid_payload)

            # Handler should not be called for invalid payloads
            assert not created_tasks
            handler.assert_not_awaited()

        # Reset created tasks list
        created_tasks.clear()

        # Valid payload should work
        valid_payload = b'{"id":"123","type":"MockBaseMessage","message":"test_message"}'
        await self.captured_handler(valid_payload)

        # Verify a task was created and execute it
        assert len(created_tasks) > 0
        for task in created_tasks:
            await task

        # Now the handler should have been called
        handler.assert_awaited_once()

    @patch("argentic.core.messager.messager.create_driver")
    async def test_unsubscribe(self, mock_create_driver, messager_config):
        """Test unsubscribe method"""
        mock_create_driver.return_value = self.driver

        # Make sure unsubscribe resolves properly when awaited
        self.driver.unsubscribe.return_value = None
        # Make it return immediately when awaited
        self.driver.unsubscribe.__await__ = MagicMock(return_value=iter([None]))

        messager = Messager(**messager_config)
        test_topic = "test/unsubscribe"

        await messager.unsubscribe(test_topic)

        self.driver.unsubscribe.assert_called_once_with(test_topic)

    @patch("argentic.core.messager.messager.create_driver")
    async def test_log_with_topic(self, mock_create_driver, messager_config):
        """Test log method with pub_log_topic set"""
        mock_create_driver.return_value = self.driver

        messager = Messager(**messager_config)
        test_message = "Test log message"
        test_level = "warning"

        await messager.log(test_message, test_level)

        self.driver.publish.assert_called_once()
        assert self.driver.publish.call_args[0][0] == messager_config["pub_log_topic"]

        # Verify payload contains expected data
        payload = self.driver.publish.call_args[0][1]
        assert "timestamp" in payload
        assert payload["level"] == test_level
        assert payload["source"] == messager_config["client_id"]
        assert payload["message"] == test_message

    @patch("argentic.core.messager.messager.create_driver")
    async def test_log_without_topic(self, mock_create_driver, messager_config):
        """Test log method without pub_log_topic set"""
        mock_create_driver.return_value = self.driver

        # Remove log topic from config
        config_without_log = messager_config.copy()
        config_without_log["pub_log_topic"] = None

        messager = Messager(**config_without_log)
        test_message = "Test log message"

        await messager.log(test_message)

        # Verify publish was not called since no log topic is set
        self.driver.publish.assert_not_called()

    @patch("argentic.core.messager.messager.create_driver")
    async def test_stop(self, mock_create_driver, messager_config):
        """Test stop method"""
        mock_create_driver.return_value = self.driver

        # Patch the messager's disconnect method instead of driver's
        messager = Messager(**messager_config)

        # Create a simpler disconnect method that doesn't rely on async mocks
        async def mock_disconnect():
            # Just record that disconnect was called
            self.driver.disconnect.assert_not_called()  # Should not have been called yet

        # Replace the messager's disconnect method to avoid async mock issues
        with patch.object(messager, "disconnect", mock_disconnect):
            await messager.stop()

        # No need to verify driver.disconnect since we're testing at messager level

    @patch("argentic.core.messager.messager.create_driver")
    @pytest.mark.parametrize(
        "protocol,expected_driver",
        [
            (MessagerProtocol.MQTT, "MQTTDriver"),
            (MessagerProtocol.KAFKA, "KafkaDriver"),
            (MessagerProtocol.REDIS, "RedisDriver"),
            (MessagerProtocol.RABBITMQ, "RabbitMQDriver"),
        ],
    )
    async def test_protocol_selection(
        self, mock_create_driver, protocol, expected_driver, messager_config
    ):
        """Test that different protocols create the corresponding drivers"""
        # Set up a driver mock that will record which driver was created
        driver_mock = AsyncMock()
        driver_mock.__class__.__name__ = expected_driver
        mock_create_driver.return_value = driver_mock

        # Update config with protocol
        config = messager_config.copy()
        config["protocol"] = protocol

        messager = Messager(**config)

        # Verify the right driver type was created
        mock_create_driver.assert_called_once()
        assert mock_create_driver.call_args[0][0] == protocol
        assert messager._driver.__class__.__name__ == expected_driver

    @patch("argentic.core.messager.messager.create_driver")
    async def test_reconnect_handling(self, mock_create_driver, messager_config):
        """Test handling of reconnection after a disconnection"""
        mock_create_driver.return_value = self.driver

        messager = Messager(**messager_config)

        # First connection (successful)
        self.driver.connect.return_value = None
        result = await messager.connect()
        assert result is True
        assert self.driver.connect.call_count == 1

        # Simulate disconnection
        self.driver.is_connected.return_value = False
        assert messager.is_connected() is False

        # Reconnection (successful)
        self.driver.connect.reset_mock()
        self.driver.connect.return_value = None
        result = await messager.connect()
        assert result is True
        assert self.driver.connect.call_count == 1

        # Reconnection (failure)
        self.driver.connect.reset_mock()
        self.driver.connect.side_effect = Exception("Connection failed")
        result = await messager.connect()
        assert result is False
        assert self.driver.connect.call_count == 1

    @patch("argentic.core.messager.messager.create_driver")
    async def test_exception_handling_during_publish(self, mock_create_driver, messager_config):
        """Test handling of exceptions during message publishing"""
        mock_create_driver.return_value = self.driver

        # Configure the driver to raise an exception on publish
        self.driver.publish.side_effect = Exception("Publish failed")

        messager = Messager(**messager_config)
        test_topic = "test/topic"
        test_message = MockBaseMessage()

        # Publishing should re-raise the exception since there's no special handling
        with pytest.raises(Exception) as excinfo:
            await messager.publish(test_topic, test_message)

        assert "Publish failed" in str(excinfo.value)
        self.driver.publish.assert_called_once_with(test_topic, test_message, qos=0, retain=False)
