"""Tests for IMAP IDLE stream handler."""

from email.message import EmailMessage
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from flowire_sdk import StreamSubscription

from fw_nodes_email.handlers.imap_idle import ImapIdleStreamHandler, _parse_email_message


def _make_subscription(**overrides):
    """Create a StreamSubscription for testing."""
    defaults = {
        "id": "sub_123",
        "workflow_id": "wf_456",
        "node_id": "node_789",
        "stream_type": "imap_idle",
        "credential_id": "cred_imap_123",
        "connection_config": {"folder": "INBOX"},
        "event_filter": None,
        "status": "active",
        "project_id": "proj_abc",
    }
    defaults.update(overrides)
    return StreamSubscription(**defaults)


def _make_handler(subscription=None, credential=None):
    """Create an ImapIdleStreamHandler with mocked dependencies."""
    sub = subscription or _make_subscription()
    cred = credential or {
        "host": "imap.example.com",
        "port": 993,
        "username": "user@example.com",
        "password": "password123",
        "use_ssl": True,
        "readonly": False,
    }

    event_callback = AsyncMock()
    credential_resolver = AsyncMock(return_value=cred)

    handler = ImapIdleStreamHandler(
        subscription=sub,
        event_callback=event_callback,
        credential_resolver=credential_resolver,
    )
    return handler


def _build_raw_email(
    subject="Test Subject",
    from_addr="sender@example.com",
    to_addr="recipient@example.com",
    body="Hello, this is a test.",
    message_id="<test123@example.com>",
) -> bytes:
    """Build a raw email as bytes."""
    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = from_addr
    msg["To"] = to_addr
    msg["Message-ID"] = message_id
    msg["Date"] = "Mon, 01 Jan 2025 12:00:00 +0000"
    msg.set_content(body)
    return msg.as_bytes()


# --- _parse_email_message tests ---


def test_parse_email_message_basic():
    """Test parsing a simple email message."""
    raw = _build_raw_email(subject="Hello World", from_addr="alice@example.com")
    result = _parse_email_message(raw)

    assert result["subject"] == "Hello World"
    assert result["from"] == "alice@example.com"
    assert result["to"] == "recipient@example.com"
    assert result["message_id"] == "<test123@example.com>"
    assert result["body_plain"] == "Hello, this is a test.\n"
    assert result["date"] == "2025-01-01T12:00:00+00:00"


def test_parse_email_message_no_date():
    """Test parsing email with no Date header."""
    msg = EmailMessage()
    msg["Subject"] = "No Date"
    msg["From"] = "sender@example.com"
    msg["To"] = "recipient@example.com"
    msg.set_content("Body text")
    raw = msg.as_bytes()

    result = _parse_email_message(raw)
    assert result["date"] == ""
    assert result["subject"] == "No Date"


def test_parse_email_message_html_body():
    """Test parsing email with HTML body."""
    msg = EmailMessage()
    msg["Subject"] = "HTML Email"
    msg["From"] = "sender@example.com"
    msg["To"] = "recipient@example.com"
    msg["Date"] = "Mon, 01 Jan 2025 12:00:00 +0000"
    msg.set_content("Plain text version")
    msg.add_alternative("<h1>HTML version</h1>", subtype="html")
    raw = msg.as_bytes()

    result = _parse_email_message(raw)
    assert "Plain text version" in result["body_plain"]
    assert "<h1>HTML version</h1>" in result["body_html"]


# --- _get_latest_uid tests ---


def test_get_latest_uid_with_messages():
    """Test getting latest UID when messages exist."""
    handler = _make_handler()
    mock_imap = MagicMock()
    mock_imap.uid.return_value = ("OK", [b"1 5 10 15"])
    handler._imap = mock_imap

    result = handler._get_latest_uid()
    assert result == 15
    mock_imap.uid.assert_called_once_with("search", None, "ALL")


def test_get_latest_uid_empty_folder():
    """Test getting latest UID from empty folder."""
    handler = _make_handler()
    mock_imap = MagicMock()
    mock_imap.uid.return_value = ("OK", [b""])
    handler._imap = mock_imap

    result = handler._get_latest_uid()
    assert result == 0


def test_get_latest_uid_search_fails():
    """Test getting latest UID when search fails."""
    handler = _make_handler()
    mock_imap = MagicMock()
    mock_imap.uid.return_value = ("NO", [b""])
    handler._imap = mock_imap

    result = handler._get_latest_uid()
    assert result == 0


# --- _fetch_new_since_uid tests ---


def test_fetch_new_since_uid_with_new_messages():
    """Test fetching new messages since a given UID."""
    handler = _make_handler()
    mock_imap = MagicMock()
    raw_email = _build_raw_email(subject="New Email")

    mock_imap.uid.side_effect = [
        ("OK", [b"11 12"]),  # search result
        ("OK", [(b"11 (RFC822 {1234})", raw_email)]),  # fetch uid 11
        ("OK", [(b"12 (RFC822 {1234})", raw_email)]),  # fetch uid 12
    ]
    handler._imap = mock_imap

    emails, new_last_uid = handler._fetch_new_since_uid(10)

    assert len(emails) == 2
    assert new_last_uid == 12
    assert emails[0]["subject"] == "New Email"
    assert emails[0]["uid"] == 11
    assert emails[1]["uid"] == 12


def test_fetch_new_since_uid_no_new_messages():
    """Test when there are no new messages."""
    handler = _make_handler()
    mock_imap = MagicMock()
    mock_imap.uid.return_value = ("OK", [b""])
    handler._imap = mock_imap

    emails, new_last_uid = handler._fetch_new_since_uid(10)

    assert emails == []
    assert new_last_uid == 10


def test_fetch_new_since_uid_filters_old_uids():
    """Test that UIDs <= last_uid are filtered out (IMAP range is inclusive)."""
    handler = _make_handler()
    mock_imap = MagicMock()
    raw_email = _build_raw_email()

    # Search returns uid 10 (same as last_uid) and 11 (new)
    mock_imap.uid.side_effect = [
        ("OK", [b"10 11"]),  # search — inclusive range returns 10 too
        ("OK", [(b"11 (RFC822 {1234})", raw_email)]),  # fetch uid 11
    ]
    handler._imap = mock_imap

    emails, new_last_uid = handler._fetch_new_since_uid(10)

    assert len(emails) == 1
    assert new_last_uid == 11


# --- connect tests ---


@pytest.mark.asyncio
async def test_connect_success_ssl():
    """Test successful IMAP SSL connection."""
    handler = _make_handler()

    mock_imap = MagicMock()
    mock_imap.login.return_value = ("OK", [b"LOGIN completed"])
    mock_imap.select.return_value = ("OK", [b"5"])
    mock_imap.capability.return_value = ("OK", [b"IMAP4rev1 IDLE"])

    with patch("fw_nodes_email.handlers.imap_idle.imaplib.IMAP4_SSL", return_value=mock_imap):
        await handler.connect()

    assert handler._imap is mock_imap
    mock_imap.login.assert_called_once_with("user@example.com", "password123")
    mock_imap.select.assert_called_once_with("INBOX", readonly=False)


@pytest.mark.asyncio
async def test_connect_no_idle_capability():
    """Test connection fails when server doesn't support IDLE."""
    handler = _make_handler()

    mock_imap = MagicMock()
    mock_imap.login.return_value = ("OK", [b"LOGIN completed"])
    mock_imap.select.return_value = ("OK", [b"5"])
    mock_imap.capability.return_value = ("OK", [b"IMAP4rev1"])

    with (
        patch("fw_nodes_email.handlers.imap_idle.imaplib.IMAP4_SSL", return_value=mock_imap),
        pytest.raises(RuntimeError, match="does not support IDLE"),
    ):
        await handler.connect()


@pytest.mark.asyncio
async def test_connect_non_ssl():
    """Test connection without SSL."""
    cred = {
        "host": "imap.example.com",
        "port": 143,
        "username": "user@example.com",
        "password": "password123",
        "use_ssl": False,
        "readonly": False,
    }
    handler = _make_handler(credential=cred)

    mock_imap = MagicMock()
    mock_imap.login.return_value = ("OK", [b"LOGIN completed"])
    mock_imap.select.return_value = ("OK", [b"5"])
    mock_imap.capability.return_value = ("OK", [b"IMAP4rev1 IDLE"])

    with patch("fw_nodes_email.handlers.imap_idle.imaplib.IMAP4", return_value=mock_imap):
        await handler.connect()

    assert handler._imap is mock_imap


@pytest.mark.asyncio
async def test_connect_readonly_credential():
    """Test connection with readonly credential."""
    cred = {
        "host": "imap.example.com",
        "port": 993,
        "username": "user@example.com",
        "password": "password123",
        "use_ssl": True,
        "readonly": True,
    }
    handler = _make_handler(credential=cred)

    mock_imap = MagicMock()
    mock_imap.login.return_value = ("OK", [b"LOGIN completed"])
    mock_imap.select.return_value = ("OK", [b"5"])
    mock_imap.capability.return_value = ("OK", [b"IMAP4rev1 IDLE"])

    with patch("fw_nodes_email.handlers.imap_idle.imaplib.IMAP4_SSL", return_value=mock_imap):
        await handler.connect()

    mock_imap.select.assert_called_once_with("INBOX", readonly=True)


# --- disconnect tests ---


@pytest.mark.asyncio
async def test_disconnect_cleans_up():
    """Test that disconnect shuts down socket and closes connection."""
    handler = _make_handler()
    mock_imap = MagicMock()
    mock_sock = MagicMock()
    mock_imap.sock = mock_sock
    handler._imap = mock_imap

    await handler.disconnect()

    assert handler._stop_idle is True
    assert handler._imap is None
    mock_sock.shutdown.assert_called_once_with(2)
    mock_imap.close.assert_called_once()
    mock_imap.logout.assert_called_once()


@pytest.mark.asyncio
async def test_disconnect_when_not_connected():
    """Test that disconnect is safe when not connected."""
    handler = _make_handler()
    handler._imap = None

    await handler.disconnect()  # Should not raise

    assert handler._stop_idle is True
