"""Caption discovery, normalization and Telegram-safe splitting tests."""

from __future__ import annotations

from bs4 import BeautifulSoup

from app.services.caption_parser import (
    CaptionParser,
    first_video_object,
    iter_jsonld_objects,
    normalize_text,
    split_caption,
    telegram_text_length,
    truncate_utf16,
)


class Adapter:
    title_selectors = ("h1.article-title",)
    caption_selectors = (".article-body",)


def test_telegram_length_counts_utf16_units() -> None:
    assert telegram_text_length("abc") == 3
    assert telegram_text_length("سلام") == 4
    assert telegram_text_length("🎬") == 2
    assert telegram_text_length("a🎬b") == 4


def test_normalize_text_plain() -> None:
    value = "  hello\r\nworld\u00a0test\u200f  "
    assert normalize_text(value) == "hello\nworld test\u200f"
    assert normalize_text(None) == ""
    assert normalize_text("") == ""


def test_normalize_text_removes_controls_and_scripts() -> None:
    html = (
        "<html><body><div>Hello<br>World</div>"
        "<script>alert('x')</script><style>.x{}</style>"
        "<p>Second &amp; last</p></body></html>"
    )
    assert normalize_text(html, from_html=True) == "Hello\nWorld\nSecond & last"


def test_normalize_text_preserves_zwnj() -> None:
    assert normalize_text("می\u200cروم") == "می\u200cروم"


def test_truncate_utf16_respects_limit() -> None:
    assert truncate_utf16("short", 10) == "short"
    assert truncate_utf16("anything", 0) == ""
    emoji = "🎬" * 10
    result = truncate_utf16(emoji, 5)
    assert result.endswith("…")
    assert telegram_text_length(result) <= 5
    long_text = "word " * 100
    result = truncate_utf16(long_text, 40)
    assert len(result) <= 40
    assert telegram_text_length(result) <= 40
    assert result.endswith("…")


def test_split_caption_short_text_is_untouched() -> None:
    first, extras = split_caption("سلام دنیا", 1024)
    assert first == "سلام دنیا"
    assert extras == []
    assert split_caption("", 1024) == ("", [])


def test_split_caption_long_text_is_bounded_and_preserved() -> None:
    text = ("This is a long caption paragraph. " * 300).strip()
    first, extras = split_caption(text, 120, extra_limit=200)
    assert first.endswith("…")
    assert telegram_text_length(first) <= 120
    assert extras
    for extra in extras:
        assert extra.endswith("…")
        assert telegram_text_length(extra) <= 200
    assert len(extras) <= 20
    # The split must not lose the beginning of the caption.
    assert first.startswith("This is a long caption")


def test_split_caption_emoji_budget() -> None:
    text = "🎬" * 100
    first, extras = split_caption(text, 20, extra_limit=20)
    assert telegram_text_length(first) <= 20
    assert all(telegram_text_length(item) <= 20 for item in extras)
    assert extras


def test_iter_jsonld_objects_handles_graph_and_invalid_json() -> None:
    soup = BeautifulSoup(
        """
        <script type="application/ld+json">
        {"@context":"https://schema.org","@graph":[
          {"@type":"VideoObject","name":"One"},
          {"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem"}]}
        ]}
        </script>
        <script type="application/ld+json">{not json}</script>
        """,
        "html.parser",
    )
    objects = list(iter_jsonld_objects(soup))
    assert any(item.get("name") == "One" for item in objects)


def test_first_video_object() -> None:
    soup = BeautifulSoup(
        '<script type="application/ld+json">'
        '{"@type":["VideoObject","NewsArticle"],"name":"Clip"}'
        "</script>",
        "html.parser",
    )
    obj = first_video_object(soup)
    assert obj is not None
    assert obj["name"] == "Clip"


def test_caption_parser_prefers_visible_heading_and_og_description() -> None:
    soup = BeautifulSoup(
        """
        <html><head>
          <title>Site title</title>
          <meta property="og:description" content="Open graph description">
          <meta name="twitter:title" content="Twitter title">
        </head><body><h1>Real heading</h1></body></html>
        """,
        "html.parser",
    )
    result = CaptionParser().parse(soup)
    assert result.title == "Real heading"
    assert result.caption == "Open graph description"
    assert result.source == "open_graph"


def test_caption_parser_uses_jsonld_videoobject() -> None:
    soup = BeautifulSoup(
        """
        <html><head>
          <script type="application/ld+json">
          {"@type":"VideoObject","name":"JSON title","description":"JSON caption"}
          </script>
        </head><body></body></html>
        """,
        "html.parser",
    )
    result = CaptionParser().parse(soup)
    assert result.title == "JSON title"
    assert result.caption == "JSON caption"
    assert result.source == "jsonld"


def test_caption_parser_adapter_selectors_win() -> None:
    soup = BeautifulSoup(
        """
        <html><body>
          <h1 class="article-title">Adapter title</h1>
          <div class="article-body"><p>Adapter caption text</p></div>
          <h1>Other heading</h1>
        </body></html>
        """,
        "html.parser",
    )
    result = CaptionParser().parse(soup, adapter=Adapter())
    assert result.title == "Adapter title"
    assert result.caption == "Adapter caption text"
    assert result.source == "adapter_caption"


def test_caption_parser_combines_short_description_with_title() -> None:
    soup = BeautifulSoup(
        '<html><head><h1>Long and useful title</h1>'
        '<meta name="description" content="Tiny tagline">'
        "</head></html>",
        "html.parser",
    )
    result = CaptionParser().parse(soup)
    assert result.title == "Long and useful title"
    assert result.caption == "Long and useful title\n\nTiny tagline"
    assert result.source == "title_plus_short_description"


def test_caption_parser_article_fallback() -> None:
    paragraph = "This is a sufficiently long article paragraph for the caption."
    soup = BeautifulSoup(
        f'<html><body><article><p>{paragraph}</p><p>short</p></article></body></html>',
        "html.parser",
    )
    result = CaptionParser().parse(soup, fallback_title="Fallback")
    assert result.title == "Fallback"
    assert result.caption == paragraph
    assert result.source == "article"


def test_caption_parser_itemprop_description() -> None:
    soup = BeautifulSoup(
        '<html><body><h1>Title</h1>'
        '<meta itemprop="description" content="Itemprop caption">'
        "</body></html>",
        "html.parser",
    )
    result = CaptionParser().parse(soup)
    assert result.caption == "Itemprop caption"
    assert result.source == "itemprop"


def test_caption_parser_fallback_title_and_html_entities() -> None:
    soup = BeautifulSoup("<html><head><title>  A &amp; B  </title></head></html>", "html.parser")
    result = CaptionParser().parse(soup, fallback_title="fallback")
    assert result.title == "A & B"
    empty = CaptionParser().parse(BeautifulSoup("<html></html>", "html.parser"))
    assert empty.title == "ویدئو"
    assert empty.caption == "ویدئو"
    assert empty.source == "title_fallback"


def test_caption_parser_rejects_broken_adapter_selector() -> None:
    class Broken:
        title_selectors = ("[[[",)
        caption_selectors = ("]]]",)

    soup = BeautifulSoup("<html><body><h1>Heading</h1></body></html>", "html.parser")
    result = CaptionParser().parse(soup, adapter=Broken())
    assert result.title == "Heading"
    assert result.caption == "Heading"
