"""Tests for TinyMessageService and TinyMessageServiceSync."""

import pytest

from tinysdk import TinyMessageService, TinyMessageServiceSync


def test_tinymessageservice_import():
  """Test that TinyMessageService can be imported."""
  assert TinyMessageService is not None
  assert TinyMessageServiceSync is not None


def test_tinymessageservice_initialization_with_params(rabbitmq_credentials):
  """Test TinyMessageService initialization with explicit parameters."""
  queue_settings = [("test-queue", lambda msg: None)]
  msg_service = TinyMessageService(queue_settings=queue_settings, **rabbitmq_credentials)
  assert msg_service.host == rabbitmq_credentials["host"]
  assert msg_service.port == rabbitmq_credentials["port"]
  assert msg_service.user == rabbitmq_credentials["user"]
  assert msg_service.password == rabbitmq_credentials["password"]


def test_tinymessageservicesync_initialization_with_params(rabbitmq_credentials):
  """Test TinyMessageServiceSync initialization with explicit parameters."""
  msg_service = TinyMessageServiceSync(**rabbitmq_credentials)
  assert msg_service.host == rabbitmq_credentials["host"]
  assert msg_service.port == rabbitmq_credentials["port"]
  assert msg_service.user == rabbitmq_credentials["user"]
  assert msg_service.password == rabbitmq_credentials["password"]


def test_tinymessageservice_initialization_without_params_fails():
  """Test that TinyMessageService initialization fails without credentials."""
  queue_settings = [("test-queue", lambda msg: None)]
  with pytest.raises(AssertionError):
    TinyMessageService(queue_settings=queue_settings)


def test_tinymessageservicesync_initialization_without_params_fails():
  """Test that TinyMessageServiceSync initialization fails without credentials."""
  with pytest.raises(AssertionError):
    TinyMessageServiceSync()


def test_tinymessageservice_has_expected_methods():
  """Test that TinyMessageService has expected methods."""
  assert hasattr(TinyMessageService, "connect")
  assert hasattr(TinyMessageService, "publish")
  assert hasattr(TinyMessageService, "close")
  assert hasattr(TinyMessageService, "run")


def test_tinymessageservicesync_has_expected_methods():
  """Test that TinyMessageServiceSync has expected methods."""
  assert hasattr(TinyMessageServiceSync, "connect")
  assert hasattr(TinyMessageServiceSync, "publish")
  assert hasattr(TinyMessageServiceSync, "consume")
  assert hasattr(TinyMessageServiceSync, "close")


from unittest.mock import MagicMock


def test_message_nacked_when_callback_raises():
  """When a message callback raises, the message must be nacked — not silently acked."""
  from tinysdk import TinyMessageService

  msg = TinyMessageService(queue_settings=[], host="h", port=5672, user="u", password="p")

  mock_channel = MagicMock()
  mock_method = MagicMock()
  mock_method.delivery_tag = "tag42"

  def bad_callback(msg_dict):
    raise ValueError("processing failed")

  msg.message_handler(mock_channel, mock_method, MagicMock(), b'{"key": "val"}', bad_callback)

  mock_channel.basic_ack.assert_not_called()
  mock_channel.basic_nack.assert_called_once_with(delivery_tag="tag42", requeue=False)


def test_message_acked_when_callback_succeeds():
  """When a message callback succeeds, the message must be acked normally."""
  from tinysdk import TinyMessageService

  msg = TinyMessageService(queue_settings=[], host="h", port=5672, user="u", password="p")

  mock_channel = MagicMock()
  mock_method = MagicMock()
  mock_method.delivery_tag = "tag99"

  def good_callback(msg_dict):
    pass

  msg.message_handler(mock_channel, mock_method, MagicMock(), b'{"key": "val"}', good_callback)

  mock_channel.basic_ack.assert_called_once_with(delivery_tag="tag99")
  mock_channel.basic_nack.assert_not_called()


def test_sync_message_nacked_when_callback_raises(rabbitmq_credentials):
  """TinyMessageServiceSync: failed callback must nack instead of silently acking."""
  from tinysdk import TinyMessageServiceSync

  msg = TinyMessageServiceSync(**rabbitmq_credentials)

  mock_channel = MagicMock()
  mock_method = MagicMock()
  mock_method.delivery_tag = "sync_tag"

  captured = {}

  def fake_basic_consume(queue, on_message_callback):
    captured["handler"] = on_message_callback

  mock_channel.basic_consume = fake_basic_consume
  mock_channel.start_consuming = MagicMock()
  mock_channel.queue_declare = MagicMock()
  mock_channel.basic_qos = MagicMock()
  mock_channel.is_open = True
  msg.channel = mock_channel

  def bad_callback(msg_dict):
    raise RuntimeError("sync failure")

  msg.consume("q", bad_callback)

  captured["handler"](mock_channel, mock_method, MagicMock(), b'{"x": 1}')

  mock_channel.basic_ack.assert_not_called()
  mock_channel.basic_nack.assert_called_once_with(delivery_tag="sync_tag", requeue=False)


def test_connection_drop_triggers_reconnect_when_not_closing():
  """Unexpected connection drop must schedule a reconnect via ioloop.call_later."""
  from tinysdk import TinyMessageService

  msg = TinyMessageService(queue_settings=[], host="h", port=5672, user="u", password="p")
  mock_connection = MagicMock()
  mock_connection.ioloop = MagicMock()
  msg.connection = mock_connection
  msg._closing = False

  # Method does not exist yet — will fail with AttributeError
  msg.on_connection_closed(mock_connection, Exception("Connection reset by peer"))

  mock_connection.ioloop.call_later.assert_called()


def test_connection_drop_does_not_reconnect_when_intentionally_closing():
  """Intentional close (_closing=True) must not trigger reconnect."""
  from tinysdk import TinyMessageService

  msg = TinyMessageService(queue_settings=[], host="h", port=5672, user="u", password="p")
  mock_connection = MagicMock()
  mock_connection.ioloop = MagicMock()
  msg.connection = mock_connection
  msg._closing = True

  msg.on_connection_closed(mock_connection, Exception("Connection reset by peer"))

  mock_connection.ioloop.call_later.assert_not_called()


def test_reconnect_schedules_non_blocking_method():
  """reconnect() must schedule _reconnect_async (not connect) to avoid blocking the IOLoop with time.sleep."""
  from tinysdk import TinyMessageService

  msg = TinyMessageService(queue_settings=[], host="h", port=5672, user="u", password="p")
  mock_connection = MagicMock()
  mock_connection.ioloop = MagicMock()
  msg.connection = mock_connection

  msg.reconnect()

  call_args = mock_connection.ioloop.call_later.call_args
  assert call_args is not None, "ioloop.call_later was not called"
  scheduled_callable = call_args[0][1]  # second positional arg
  assert scheduled_callable == msg._reconnect_async, f"Expected _reconnect_async to be scheduled, got {scheduled_callable}"


def test_publish_dispatches_via_add_callback_threadsafe(rabbitmq_credentials):
  """publish() must use add_callback_threadsafe — safe to call from any thread."""
  from tinysdk import TinyMessageService

  service = TinyMessageService(queue_settings=[], **rabbitmq_credentials)
  service.channel = MagicMock()
  service.channel.is_open = True
  service.logger = MagicMock()

  mock_ioloop = MagicMock()

  def execute_callback(cb):
    cb()  # execute synchronously to simulate IOLoop processing

  mock_ioloop.add_callback_threadsafe.side_effect = execute_callback
  mock_connection = MagicMock()
  mock_connection.ioloop = mock_ioloop
  service.connection = mock_connection

  service.publish("my-queue", {"data": "hello"})

  mock_ioloop.add_callback_threadsafe.assert_called_once()
  service.channel.basic_publish.assert_called_once()


def test_publish_raises_timeout_if_ioloop_does_not_process(rabbitmq_credentials):
  """publish() must raise TimeoutError if the IOLoop never processes the callback."""
  import pytest
  from tinysdk import TinyMessageService

  service = TinyMessageService(queue_settings=[], **rabbitmq_credentials)
  service.channel = MagicMock()
  service.channel.is_open = True
  service.logger = MagicMock()

  mock_ioloop = MagicMock()
  mock_ioloop.add_callback_threadsafe = MagicMock()  # swallows callback, never calls it
  mock_connection = MagicMock()
  mock_connection.ioloop = mock_ioloop
  service.connection = mock_connection

  with pytest.raises(TimeoutError):
    service.publish("my-queue", {"data": "hello"}, timeout=0.05)


def test_publish_raises_when_not_connected(rabbitmq_credentials):
  """publish() must raise immediately if not connected."""
  import pytest
  from pika.exceptions import AMQPConnectionError
  from tinysdk import TinyMessageService

  service = TinyMessageService(queue_settings=[], **rabbitmq_credentials)
  service.connection = None

  with pytest.raises(AMQPConnectionError):
    service.publish("my-queue", {"data": "hello"})


from unittest.mock import patch


def test_on_channel_closed_does_not_reconnect_when_connection_also_dropped(rabbitmq_credentials):
  """on_channel_closed must NOT trigger reconnect when the connection is also gone."""
  from tinysdk import TinyMessageService

  service = TinyMessageService(queue_settings=[], **rabbitmq_credentials)
  mock_connection = MagicMock()
  mock_connection.is_open = False  # connection also down
  mock_connection.ioloop = MagicMock()
  service.connection = mock_connection
  service._closing = False

  service.on_channel_closed(MagicMock(), Exception("channel gone"))

  mock_connection.ioloop.call_later.assert_not_called()


def test_on_channel_closed_does_reconnect_when_connection_is_alive(rabbitmq_credentials):
  """on_channel_closed MUST reconnect when connection is still open (channel-only failure)."""
  from tinysdk import TinyMessageService

  service = TinyMessageService(queue_settings=[], **rabbitmq_credentials)
  mock_connection = MagicMock()
  mock_connection.is_open = True  # connection still alive
  mock_connection.ioloop = MagicMock()
  service.connection = mock_connection
  service._closing = False

  service.on_channel_closed(MagicMock(), Exception("channel closed by broker"))

  mock_connection.ioloop.call_later.assert_called()


def test_ensure_connection_does_not_sleep_on_success(rabbitmq_credentials):
  """ensure_connection must call connect() once with no sleep — no nested @retry loop."""
  from tinysdk import TinyMessageServiceSync

  service = TinyMessageServiceSync(**rabbitmq_credentials)
  service.channel = None

  with patch("time.sleep") as mock_sleep:
    with patch.object(service, "connect"):
      service.ensure_connection()

  mock_sleep.assert_not_called()


def test_sync_context_manager_connects_and_closes(rabbitmq_credentials):
  """TinyMessageServiceSync must connect on __enter__ and close on __exit__."""
  from tinysdk import TinyMessageServiceSync

  with patch("tinysdk.messaging.pika") as mock_pika:
    mock_conn = MagicMock()
    mock_conn.is_open = True
    mock_pika.BlockingConnection.return_value = mock_conn
    mock_conn.channel.return_value = MagicMock()

    with TinyMessageServiceSync(**rabbitmq_credentials) as svc:
      assert svc.connection is not None

  mock_conn.close.assert_called_once()


def test_sync_context_manager_closes_on_exception(rabbitmq_credentials):
  """TinyMessageServiceSync context manager must call close() even when body raises."""
  import pytest
  from tinysdk import TinyMessageServiceSync

  with patch("tinysdk.messaging.pika") as mock_pika:
    mock_conn = MagicMock()
    mock_conn.is_open = True
    mock_pika.BlockingConnection.return_value = mock_conn
    mock_conn.channel.return_value = MagicMock()

    with pytest.raises(RuntimeError):
      with TinyMessageServiceSync(**rabbitmq_credentials):
        raise RuntimeError("something failed in the body")

  mock_conn.close.assert_called_once()
