"""app.text_content.parse_rich_text -- pure parsing logic, no HTTP, no
DB. This is the sanitization boundary for the text widget's dialog
(widget_dialog_text.js posts contenteditable innerHTML here, see
routers/api_widgets.py's api_widget_config_save "text" branch); the
main thing under test is that only recognized style flags survive and
everything else (unknown tags/attributes, unparseable colors, excess
length) is silently dropped rather than round-tripped."""

from __future__ import annotations

from app.text_content import MAX_TOTAL_CHARS, has_text, parse_rich_text


def test_plain_text_is_one_paragraph_one_run():
    paragraphs = parse_rich_text("Hello world")
    assert paragraphs == [[{"text": "Hello world", "bold": False, "italic": False,
                            "underline": False, "color": None, "bg": None}]]


def test_bold_italic_underline_tags():
    paragraphs = parse_rich_text("<b>bold</b> <i>italic</i> <u>under</u>")
    runs = paragraphs[0]
    assert runs[0]["bold"] is True and runs[0]["text"] == "bold"
    assert runs[2]["italic"] is True and runs[2]["text"] == "italic"
    assert runs[4]["underline"] is True and runs[4]["text"] == "under"


def test_strong_and_em_are_treated_like_b_and_i():
    paragraphs = parse_rich_text("<strong>bold</strong><em>italic</em>")
    runs = paragraphs[0]
    assert runs[0]["bold"] is True
    assert runs[1]["italic"] is True


def test_span_style_color_and_background():
    html = '<span style="color: rgb(207, 0, 15); background-color: #ffdb00;">hi</span>'
    run = parse_rich_text(html)[0][0]
    assert run["color"] == "#cf000f"
    assert run["bg"] == "#ffdb00"


def test_span_style_font_weight_and_style_and_decoration():
    html = '<span style="font-weight: bold; font-style: italic; text-decoration: underline;">x</span>'
    run = parse_rich_text(html)[0][0]
    assert run["bold"] is True
    assert run["italic"] is True
    assert run["underline"] is True


def test_font_tag_color_attribute():
    run = parse_rich_text('<font color="#00ff00">green</font>')[0][0]
    assert run["color"] == "#00ff00"


def test_short_hex_color_expands():
    run = parse_rich_text('<span style="color: #f00;">red</span>')[0][0]
    assert run["color"] == "#ff0000"


def test_unparseable_color_is_dropped():
    run = parse_rich_text('<span style="color: papayawhip;">x</span>')[0][0]
    assert run["color"] is None


def test_nested_styles_combine():
    run = parse_rich_text("<b><i>both</i></b>")[0][0]
    assert run["bold"] is True
    assert run["italic"] is True


def test_style_does_not_leak_past_closing_tag():
    paragraphs = parse_rich_text("<b>bold</b>plain")
    runs = paragraphs[0]
    assert runs[0]["bold"] is True
    assert runs[1]["bold"] is False


def test_div_per_line_becomes_separate_paragraphs():
    paragraphs = parse_rich_text("<div>line one</div><div>line two</div>")
    assert [p[0]["text"] for p in paragraphs] == ["line one", "line two"]


def test_shift_enter_br_within_a_div_also_breaks_paragraphs():
    paragraphs = parse_rich_text("<div>line one<br>line two</div>")
    assert [p[0]["text"] for p in paragraphs] == ["line one", "line two"]


def test_blank_line_idiom_produces_one_empty_paragraph():
    html = "<div>A</div><div><br></div><div>B</div>"
    paragraphs = parse_rich_text(html)
    assert [p[0]["text"] if p else None for p in paragraphs] == ["A", None, "B"]


def test_double_blank_line_produces_two_empty_paragraphs():
    html = "<div>A</div><div><br></div><div><br></div><div>B</div>"
    paragraphs = parse_rich_text(html)
    assert [p[0]["text"] if p else None for p in paragraphs] == ["A", None, None, "B"]


def test_mid_word_style_change_does_not_insert_a_space():
    """"wor" bolded, "ld" not -- must still read as one word "world" when
    rendered (see app/widgets/text.py's word-grouping), not "wor ld"."""
    paragraphs = parse_rich_text("<b>wor</b>ld")
    runs = paragraphs[0]
    assert [r["text"] for r in runs] == ["wor", "ld"]


def test_unrecognized_tags_are_dropped_but_their_text_survives_as_plain():
    """A <script> (or any tag outside the recognized set) never executes
    or persists as a tag -- its text content just becomes an ordinary
    unstyled run, exactly like any other stray text."""
    paragraphs = parse_rich_text('<script>alert(1)</script>hello')
    runs = paragraphs[0]
    assert any(r["text"] == "alert(1)" and not r["bold"] for r in runs)
    assert any(r["text"] == "hello" for r in runs)


def test_style_attribute_cannot_smuggle_unrecognized_css():
    """Only color/background-color/font-weight/font-style/text-decoration
    are ever read from a style attribute -- anything else (a CSS
    injection attempt via e.g. a bogus property) is just ignored."""
    html = '<span style="position: fixed; top: 0; color: #123456;">x</span>'
    run = parse_rich_text(html)[0][0]
    assert run["color"] == "#123456"
    # No other keys were introduced by the extra property.
    assert set(run.keys()) == {"text", "bold", "italic", "underline", "color", "bg"}


def test_content_is_truncated_to_max_total_chars():
    html = "a" * (MAX_TOTAL_CHARS + 500)
    paragraphs = parse_rich_text(html)
    total = sum(len(r["text"]) for p in paragraphs for r in p)
    assert total == MAX_TOTAL_CHARS


def test_malformed_html_does_not_raise():
    parse_rich_text("<b><i>unclosed tags <div>and a stray </b>")


def test_has_text_false_for_none_and_blank():
    assert has_text(None) is False
    assert has_text([]) is False
    assert has_text([[]]) is False
    assert has_text([[{"text": "   ", "bold": False, "italic": False,
                       "underline": False, "color": None, "bg": None}]]) is False


def test_has_text_true_for_real_content():
    assert has_text(parse_rich_text("hi")) is True
