"""Tests for scripts/html_utils.py — shared HTML text-extraction helpers.

Characterises the behaviour of both public functions so future refactors
cannot silently change output.
"""

import pytest
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))

from html_utils import strip_html, strip_html_preserve_ws


# ---------------------------------------------------------------------------
# strip_html — standard path (entity decode + whitespace collapse)
# ---------------------------------------------------------------------------


class TestStripHtml:
    """Characterise strip_html output for common input patterns."""

    def test_removes_simple_tags(self):
        assert strip_html("<p>Hello <b>world</b></p>") == "Hello world"

    def test_none_returns_empty(self):
        assert strip_html(None) == ""

    def test_empty_string_returns_empty(self):
        assert strip_html("") == ""

    def test_zero_returns_empty(self):
        # str(0 or "") yields "" — same as None/empty
        assert strip_html(0) == ""

    def test_false_returns_empty(self):
        assert strip_html(False) == ""

    # --- entity decoding ---

    def test_decodes_amp(self):
        assert strip_html("Tom &amp; Jerry") == "Tom & Jerry"

    def test_decodes_nbsp(self):
        # &nbsp; is decoded to a non-breaking space then collapsed to a regular space
        result = strip_html("Hello&nbsp;World")
        assert result == "Hello\xa0World" or result == "Hello World"
        # Either form is acceptable; we just verify the entity was consumed
        assert "&nbsp;" not in result

    def test_decodes_lt_gt(self):
        assert strip_html("&lt;p&gt;test&lt;/p&gt;") == "<p>test</p>"

    def test_decodes_numeric_entity(self):
        assert strip_html("caf&#233;") == "café"

    def test_decodes_named_entity_copy(self):
        assert strip_html("&copy; 2024") == "© 2024"

    # --- whitespace collapse ---

    def test_collapses_internal_whitespace(self):
        assert strip_html("<p>  two   spaces  </p>") == "two spaces"

    def test_collapses_newlines(self):
        assert strip_html("<p>line1\nline2</p>") == "line1 line2"

    def test_strips_leading_trailing(self):
        assert strip_html("  hello  ") == "hello"

    # --- collapse_ws=False ---

    def test_preserve_ws_flag_keeps_whitespace(self):
        result = strip_html("<p>  two   spaces  </p>", collapse_ws=False)
        # Tags stripped, entity decode applied, but whitespace kept verbatim
        assert "  " in result or "\n" in result or result == "  two   spaces  "

    # --- special tag handling ---

    def test_script_tag_content_is_NOT_stripped(self):
        # strip_html (standard) does NOT strip script/style content — only tags
        # This is a deliberate design difference from course_content_cache._strip_html
        result = strip_html("<script>alert(1)</script>Text")
        assert "Text" in result
        # The script tag is stripped but its content is preserved
        assert "alert(1)" in result

    def test_mixed_case_tags(self):
        assert strip_html("<STRONG>bold</STRONG>") == "bold"

    def test_br_tag(self):
        result = strip_html("line1<br>line2")
        assert "line1" in result and "line2" in result

    def test_non_string_input_coerced(self):
        # Any truthy non-string is str()-coerced
        assert strip_html(42) == "42"
        assert strip_html([]) == ""  # [] is falsy via str([] or "") = ""


# ---------------------------------------------------------------------------
# strip_html_preserve_ws — HTMLParser path, whitespace NOT collapsed
# ---------------------------------------------------------------------------


class TestStripHtmlPreserveWs:
    """Characterise strip_html_preserve_ws output."""

    def test_removes_tags(self):
        result = strip_html_preserve_ws("<p>Hello world</p>")
        assert "Hello world" in result
        assert "<p>" not in result

    def test_none_returns_empty(self):
        assert strip_html_preserve_ws(None) == ""

    def test_empty_string_returns_empty(self):
        assert strip_html_preserve_ws("") == ""

    def test_entity_decode_handled_by_parser(self):
        # HTMLParser decodes &amp; etc. internally via handle_data
        result = strip_html_preserve_ws("Tom &amp; Jerry")
        assert result == "Tom & Jerry"

    def test_nbsp_decoded_as_nonbreaking_space(self):
        # HTMLParser decodes &nbsp; to \xa0
        result = strip_html_preserve_ws("Hello&nbsp;World")
        assert result == "Hello\xa0World"

    def test_whitespace_PRESERVED_inside_text_node(self):
        # This is the key difference from strip_html: internal WS is not collapsed
        result = strip_html_preserve_ws("<p>  two   spaces  </p>")
        assert "  " in result  # internal spaces preserved in text node

    def test_newlines_preserved(self):
        result = strip_html_preserve_ws("<p>line1\nline2</p>")
        assert "\n" in result

    def test_multiple_text_nodes_joined_with_space(self):
        # _TextExtractor joins nodes with " ".join(self._parts)
        result = strip_html_preserve_ws("<p>Hello</p><p>world</p>")
        # Two separate text nodes: "Hello" and "world"
        assert "Hello" in result and "world" in result

    def test_malformed_html_fallback(self):
        # Malformed input should fall back gracefully (regex strip)
        # We can't easily force the HTMLParser to raise, but we can check it
        # at least returns a string without crashing
        result = strip_html_preserve_ws("plain text no tags")
        assert result == "plain text no tags"

    def test_mixed_case_tags(self):
        result = strip_html_preserve_ws("<STRONG>bold</STRONG>")
        assert "bold" in result


# ---------------------------------------------------------------------------
# Cross-function consistency check: empty / None
# ---------------------------------------------------------------------------


class TestBothFunctionsNullHandling:
    @pytest.mark.parametrize("fn", [strip_html, strip_html_preserve_ws])
    @pytest.mark.parametrize("value", [None, "", 0, False])
    def test_falsy_returns_empty(self, fn, value):
        assert fn(value) == ""
