"""Tests for app/services/email_service.py."""

import pytest
from unittest.mock import MagicMock, patch

import app.services.email_service as es
from app.services.email_service import (
    send_email,
    build_daily_digest,
    build_invite_email,
)

# ---------------------------------------------------------------------------
# send_email — async SMTP / dev-mode
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_send_email_dev_mode():
    """Returns True immediately when SMTP_HOST is empty (dev mode)."""
    with patch.object(es, "SMTP_HOST", ""):
        result = await send_email("dev@test.com", "Dev", "<p>body</p>")
        assert result is True


@pytest.mark.asyncio
async def test_send_email_smtp_success():
    """SMTP configured — connects, starts TLS, logs in, and sends mail."""
    with (
        patch.object(es, "SMTP_HOST", "smtp.test.com"),
        patch.object(es, "SMTP_USER", "smtp_user"),
        patch.object(es, "SMTP_PASSWORD", "smtp_pass"),
        patch.object(es.smtplib, "SMTP") as mock_ctor,
    ):
        mock_server = MagicMock()
        mock_ctor.return_value.__enter__.return_value = mock_server

        result = await send_email("user@test.com", "Subj", "<p>hi</p>")

        assert result is True
        mock_ctor.assert_called_once_with("smtp.test.com", 587, timeout=10)
        mock_server.starttls.assert_called_once()
        mock_server.login.assert_called_once_with("smtp_user", "smtp_pass")
        mock_server.sendmail.assert_called_once()


@pytest.mark.asyncio
async def test_send_email_smtp_failure():
    """SMTP raises an exception — returns False."""
    with (
        patch.object(es, "SMTP_HOST", "smtp.test.com"),
        patch.object(es, "SMTP_USER", "u"),
        patch.object(es, "SMTP_PASSWORD", "p"),
        patch.object(es.smtplib, "SMTP") as mock_ctor,
    ):
        mock_server = MagicMock()
        mock_ctor.return_value.__enter__.return_value = mock_server
        mock_server.starttls.side_effect = Exception("Connection refused")

        result = await send_email("fail@test.com", "Fail", "<p>oops</p>")
        assert result is False


# ---------------------------------------------------------------------------
# send_email — verify message structure
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_send_email_html_body():
    """The SMTP sendmail call carries correct from / to / subject / HTML body."""
    with (
        patch.object(es, "SMTP_HOST", "smtp.test.com"),
        patch.object(es.smtplib, "SMTP") as mock_ctor,
    ):
        mock_server = MagicMock()
        mock_ctor.return_value.__enter__.return_value = mock_server

        await send_email("recipient@example.com", "My Subject", "<h1>Hello</h1>")

        from email import policy
        from email.parser import BytesParser

        mock_server.sendmail.assert_called_once()
        args = mock_server.sendmail.call_args[0]
        assert args[0] == "service@oncolit.gonsun.com"  # FROM_EMAIL
        assert args[1] == ["recipient@example.com"]

        # Parse the MIME message to verify body and subject
        msg_str: str = args[2]
        msg = BytesParser(policy=policy.default).parsebytes(msg_str.encode())
        assert msg["Subject"] == "My Subject"
        assert msg["To"] == "recipient@example.com"
        assert msg.get_body(("html",)).get_content() == "<h1>Hello</h1>"


# ---------------------------------------------------------------------------
# build_daily_digest
# ---------------------------------------------------------------------------


def test_build_daily_digest_no_items():
    """Empty feed list — HTML contains user name and zero counters."""
    html = build_daily_digest([], "张三")
    assert "张三" in html
    assert "今日必读 0 篇" in html
    assert "推荐 0 篇" in html


def _feed(
    priority: str,
    title: str,
    author: str = "",
    journal: str = "",
    tags: list | None = None,
) -> dict:
    return {
        "priority": priority,
        "title": title,
        "first_author": author,
        "journal": journal,
        "tags": tags or [],
    }


def test_build_daily_digest_mixed():
    """Must-read and recommended items both appear in the rendered HTML."""
    items = [
        _feed("must_read", "Lung Cancer Breakthrough", "Wang", "Nature",
              [{"name_zh": "肺癌"}]),
        _feed("recommended", "Breast Cancer Review", "Li", "Science",
              [{"name_zh": "乳腺癌"}]),
    ]
    html = build_daily_digest(items, "测试")
    assert "Lung Cancer Breakthrough" in html
    assert "Breast Cancer Review" in html
    assert "今日必读 1 篇" in html
    assert "推荐 1 篇" in html


def test_build_daily_digest_html_escaped():
    """User name containing HTML special characters is escaped."""
    html = build_daily_digest([], "<script>alert('xss')</script>")
    assert "&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;" in html
    assert "<script>" not in html


def test_build_daily_digest_must_read_limit():
    """Only the first 10 must-read items are rendered when there are more."""
    items = [_feed("must_read", f"Article {i}") for i in range(15)]
    html = build_daily_digest(items, "测试")
    for i in range(10):
        assert f"Article {i}" in html
    assert "Article 10" not in html


# ---------------------------------------------------------------------------
# build_invite_email
# ---------------------------------------------------------------------------


def test_build_invite_email():
    """Invite email contains inviter name, team name, and invite link."""
    html = build_invite_email("王医生", "肿瘤科", "/invite/abc123")
    assert "王医生" in html
    assert "肿瘤科" in html
    assert "/invite/abc123" in html
    assert "接受邀请" in html
    assert "scilit-oncology.com/invite/abc123" in html or "/invite/abc123" in html


def test_build_invite_email_html_escaped():
    """Names with HTML metacharacters are escaped in the invite email."""
    html = build_invite_email("<script>alert(1)</script>", "<b>Team</b>", "/link")
    assert "&lt;script&gt;alert(1)&lt;/script&gt;" in html
    assert "&lt;b&gt;Team&lt;/b&gt;" in html
    assert "<script>" not in html
    assert "<b>Team</b>" not in html
