import asyncio
import os
from unittest.mock import MagicMock

import aiomqtt
import pytest
from aiomqtt.exceptions import MqttError as AioMqttError
from testcontainers.core.container import DockerContainer
from testcontainers.core.wait_strategies import LogMessageWaitStrategy

import aiospb.infra.paho as p
import tests as t
from aiospb.infra.paho import MQTTConfig, MQTTMessage, PahoMQTTClient
from aiospb.messages.ports import MQTTError

# Define the path to the configuration directory
CONFIG_DIR = os.path.join(os.path.dirname(t.__file__), "test_data", "mosquitto")


def given_a_mqtt_config(mosquitto: DockerContainer):
    return MQTTConfig(
        mosquitto.get_container_host_ip(),
        int(mosquitto.get_exposed_port(1883)),
        "test_user",
        "password",
    )


@pytest.fixture
def aiomqtt_client(monkeypatch):
    m = MagicMock()
    monkeypatch.setattr(p, "aiomqtt", m)
    return m.Client.return_value


@pytest.fixture(scope="module")
def mosquitto():
    # Initialize the Mosquitto container using the latest official image
    mosquitto = DockerContainer("eclipse-mosquitto:latest")

    mosquitto.with_volume_mapping(
        host=CONFIG_DIR, container="/mosquitto/config", mode="ro"  # Read-only mount
    )
    mosquitto.with_exposed_ports(1883)
    mosquitto.start()
    mosquitto.waiting_for(LogMessageWaitStrategy("mosquitto version .* running"))

    yield mosquitto

    mosquitto.stop()


@pytest.fixture
async def publisher(mosquitto):
    pub = PahoMQTTClient("publisher", given_a_mqtt_config(mosquitto))
    yield pub
    await pub.disconnect()


@pytest.fixture
def messages():
    return asyncio.Queue()


@pytest.fixture
def aiomqtt_cli(monkeypatch):
    module = MagicMock()
    module.Client.return_value = cli = MagicMock(spec=aiomqtt.Client)
    monkeypatch.setattr("aiospb.infra.paho.aiomqtt", module)
    return cli


WILL = MQTTMessage("will/topic", b"will-payload", qos=1, retain=True)


@pytest.mark.integration
class A_PahoMQTTClient:
    # async def should_connect_with_a_timeout(self, aiomqtt_cli):
    #     cfg = MQTTConfig("hostname", 1883, conn_timeout=0.0)
    #     cli = PahoMQTTClient("client", cfg)

    #     async def delay():
    #         await asyncio.sleep(1)

    #     aiomqtt_cli.__aenter__.side_effect = delay

    #     with pytest.raises(TimeoutError):
    #         await cli.connect(will=WILL)

    async def set_subscription(self, mosquitto, messages):
        sub = PahoMQTTClient("subscriber", given_a_mqtt_config(mosquitto))
        await sub.connect(will=WILL)
        await sub.subscribe("#", 1)
        try:
            async for msg in sub.messages:
                await messages.put(msg)
        finally:
            await sub.disconnect()

    def force_accidental_disconnect(self, client):
        client = client._client
        transport = client._client.socket()
        if transport:
            transport.close()

    async def clean_client(self, client):
        client = client._client
        if client._misc_task is not None:
            client._misc_task.cancel()
            try:
                await client._misc_task  # Wait for cancellation
            except asyncio.CancelledError:
                pass

    async def should_publish_and_subscribe(self, mosquitto: DockerContainer):
        publisher = PahoMQTTClient("publisher", given_a_mqtt_config(mosquitto))
        try:
            await publisher.connect(will=WILL)
        except Exception as e:
            print(mosquitto.get_logs())
            raise e
        assert publisher.is_connected()

        messages = asyncio.Queue()
        subscription = asyncio.create_task(self.set_subscription(mosquitto, messages))
        await asyncio.sleep(1)

        msg = MQTTMessage("pub/topic", b"pub-payload", qos=1, retain=False)

        await publisher.publish(msg)

        assert await messages.get() == msg

        self.force_accidental_disconnect(publisher)
        WILL.retain = False
        assert await messages.get() == WILL
        WILL.retain = True

        await self.clean_client(publisher)

        subscription.cancel()
        try:
            await subscription
        except asyncio.CancelledError:
            pass

    async def should_repeat_publish_several_times(self, aiomqtt_cli):
        cfg = MQTTConfig("hostname", 1883, try_wait=0.0, trys=3)
        cli = PahoMQTTClient("client", cfg)
        await cli.connect(WILL)

        aiomqtt_cli.publish.side_effect = TimeoutError()

        with pytest.raises(TimeoutError):
            await cli.publish(MQTTMessage("topic", b"payload"))

        assert len(aiomqtt_cli.publish.mock_calls) == 3

    # async def should_repeat_message_receipt_several_times(self, aiomqtt_cli):
    #     cfg = MQTTConfig("hostname", 1883, try_wait=0.0, trys=3)
    #     cli = PahoMQTTClient("client", cfg)
    #     await cli.connect(WILL)

    #     aiomqtt_cli.messages.__anext__.side_effect = AioMqttError()
    #     aiomqtt_cli._connected = True

    #     with pytest.raises(MQTTError):
    #         async for _ in cli.messages:
    #             return

    #     assert len(aiomqtt_cli.messages.__anext__.mock_calls) == 3

    async def should_disconenct_twice(self, mosquitto):
        publisher = PahoMQTTClient("publisher", given_a_mqtt_config(mosquitto))
        will = MQTTMessage("will/topic", b"will-payload", qos=1, retain=True)
        await publisher.connect(will=will)
        assert publisher.is_connected()

        await publisher.disconnect()
        await publisher.disconnect()

    async def should_raise_mqtt_error_when_publish(self, mosquitto, aiomqtt_client):
        publisher = PahoMQTTClient("publisher", given_a_mqtt_config(mosquitto))
        will = MQTTMessage("will/topic", b"will-payload", qos=1, retain=True)
        await publisher.connect(will=will)
        assert publisher.is_connected()

        aiomqtt_client.publish.side_effect = AioMqttError()

        with pytest.raises(MQTTError):
            await publisher.publish(MQTTMessage("topic", b"payload"))

    async def should_raise_mqtt_error_when_recovering_messages(
        self, mosquitto, aiomqtt_client
    ):
        subscriber = PahoMQTTClient("subscriber", given_a_mqtt_config(mosquitto))
        will = MQTTMessage("will/topic", b"will-payload", qos=1, retain=True)
        await subscriber.connect(will=will)
        assert subscriber.is_connected()

        aiomqtt_client.messages.__anext__.side_effect = AioMqttError()

        with pytest.raises(MQTTError):
            await subscriber.messages.__anext__()

    async def should_raise_mqtt_error_when_subscription(
        self, mosquitto, aiomqtt_client
    ):
        subscriber = PahoMQTTClient("subscriber", given_a_mqtt_config(mosquitto))
        will = MQTTMessage("will/topic", b"will-payload", qos=1, retain=True)
        await subscriber.connect(will=will)
        assert subscriber.is_connected()

        aiomqtt_client.subscribe.side_effect = AioMqttError()

        with pytest.raises(MQTTError):
            await subscriber.subscribe("#", 1)

    async def should_raise_mqtt_error_when_unsubscription(
        self, mosquitto, aiomqtt_client
    ):
        subscriber = PahoMQTTClient("subscriber", given_a_mqtt_config(mosquitto))
        will = MQTTMessage("will/topic", b"will-payload", qos=1, retain=True)
        await subscriber.connect(will=will)
        assert subscriber.is_connected()

        aiomqtt_client.unsubscribe.side_effect = AioMqttError()

        with pytest.raises(MQTTError):
            await subscriber.unsubscribe("#")

    async def should_raise_mqtt_error_when_connection(self, mosquitto, aiomqtt_client):
        subscriber = PahoMQTTClient("subscriber", given_a_mqtt_config(mosquitto))
        aiomqtt_client.__aenter__.side_effect = AioMqttError()

        will = MQTTMessage("will/topic", b"will-payload", qos=1, retain=True)
        with pytest.raises(MQTTError):
            await subscriber.connect(will=will)
