import warnings

import paho.mqtt.client as mqtt

from a2a_agent.client import MqttClient


class FakePahoClient:
    def __init__(self) -> None:
        self.on_connect = None
        self.subscribe_calls: list[tuple[str, int]] = []
        self.callback_topics: list[str] = []

    def message_callback_add(self, topic: str, callback) -> None:
        self.callback_topics.append(topic)

    def subscribe(self, topic: str, *, qos: int):
        self.subscribe_calls.append((topic, qos))
        return mqtt.MQTT_ERR_SUCCESS, len(self.subscribe_calls)


class FakeReasonCode:
    def __init__(self, *, is_failure: bool = False) -> None:
        self.is_failure = is_failure

    def __str__(self) -> str:
        return "failure" if self.is_failure else "success"


def mqtt_client_with_fake_paho(monkeypatch) -> tuple[MqttClient, FakePahoClient]:
    fake = FakePahoClient()
    monkeypatch.setattr(mqtt, "Client", lambda **kwargs: fake)
    return MqttClient(), fake


def test_mqtt_client_import_and_creation_do_not_connect(monkeypatch) -> None:
    def fail_connect(*args, **kwargs) -> None:
        raise AssertionError("connect must not be called during client creation")

    monkeypatch.setattr(mqtt.Client, "connect", fail_connect)

    MqttClient()


def test_mqtt_client_creation_has_no_deprecation_warning() -> None:
    with warnings.catch_warnings():
        warnings.simplefilter("error", DeprecationWarning)
        MqttClient()


def test_on_connect_restores_remembered_subscriptions(monkeypatch) -> None:
    client, fake = mqtt_client_with_fake_paho(monkeypatch)
    client.subscribe("a2a/team/one", lambda *args: None, qos=1)
    client.subscribe("a2a/team/two", lambda *args: None, qos=2)
    fake.subscribe_calls.clear()

    assert callable(fake.on_connect)
    fake.on_connect(fake, None, None, FakeReasonCode(), None)
    fake.on_connect(fake, None, None, FakeReasonCode(), None)

    assert fake.subscribe_calls == [
        ("a2a/team/one", 1),
        ("a2a/team/two", 2),
        ("a2a/team/one", 1),
        ("a2a/team/two", 2),
    ]
    assert fake.callback_topics == ["a2a/team/one", "a2a/team/two"]


def test_on_connect_calls_registered_hooks_each_time(monkeypatch) -> None:
    client, fake = mqtt_client_with_fake_paho(monkeypatch)
    calls: list[str] = []
    client.add_on_connect_hook(lambda: calls.append("called"))

    assert callable(fake.on_connect)
    fake.on_connect(fake, None, None, FakeReasonCode(), None)
    fake.on_connect(fake, None, None, FakeReasonCode(), None)

    assert calls == ["called", "called"]


def test_on_connect_hook_exception_is_reported_and_does_not_escape(
    monkeypatch,
    capsys,
) -> None:
    client, fake = mqtt_client_with_fake_paho(monkeypatch)
    calls: list[str] = []

    def fail_hook() -> None:
        raise RuntimeError("hook crashed")

    client.add_on_connect_hook(fail_hook)
    client.add_on_connect_hook(lambda: calls.append("after failure"))

    assert callable(fake.on_connect)
    fake.on_connect(fake, None, None, FakeReasonCode(), None)

    assert calls == ["after failure"]
    assert "MQTT on-connect hook failed: hook crashed" in capsys.readouterr().out


def test_failed_connect_does_not_restore_or_call_hooks(monkeypatch, capsys) -> None:
    client, fake = mqtt_client_with_fake_paho(monkeypatch)
    calls: list[str] = []
    client.subscribe("a2a/team/one", lambda *args: None)
    client.add_on_connect_hook(lambda: calls.append("called"))
    fake.subscribe_calls.clear()

    assert callable(fake.on_connect)
    fake.on_connect(fake, None, None, FakeReasonCode(is_failure=True), None)

    assert fake.subscribe_calls == []
    assert calls == []
    assert client.wait_connected(timeout=0) is False
    assert "MQTT connection failed: failure" in capsys.readouterr().out
