"""Tests for scripts/html_sanitize.py (S4 — sanitizer layer).

Covers the acceptance criteria from the PR2 spec:
- script/style/iframe stripped (including their text content)
- Nested <scr<script>ipt> obfuscation stripped
- onerror / onclick stripped
- href="javascript:..." neutralized
- <img src=x onerror=...> neutralized
- Entity-encoded &#106;avascript: neutralized
- Benign tags/attrs/inline-style preserved
"""

import pathlib
import sys

import pytest

SCRIPTS = pathlib.Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))

from html_sanitize import sanitize_html


# ---------------------------------------------------------------------------
# Dangerous tag stripping
# ---------------------------------------------------------------------------


def test_script_tag_stripped():
    out = sanitize_html('<p>hi</p><script>alert(1)</script>')
    assert '<script' not in out
    assert 'alert(1)' not in out


def test_script_text_content_stripped():
    """Text inside <script> must not leak through."""
    out = sanitize_html('<script>document.title="XSS"</script><p>ok</p>')
    assert 'document.title' not in out
    assert 'XSS' not in out
    assert '<p>' in out


def test_style_tag_stripped():
    out = sanitize_html('<style>body{display:none}</style><p>text</p>')
    assert '<style' not in out
    assert 'display:none' not in out


def test_style_text_content_stripped():
    out = sanitize_html('<style>*{color:red}</style><span>ok</span>')
    assert 'color:red' not in out


def test_iframe_tag_stripped():
    out = sanitize_html('<iframe src="http://evil.example"></iframe>')
    assert '<iframe' not in out


def test_object_tag_stripped():
    out = sanitize_html('<object data="x.swf"></object>')
    assert '<object' not in out


def test_embed_tag_stripped():
    out = sanitize_html('<embed src="x.swf">')
    assert '<embed' not in out


def test_link_tag_stripped():
    out = sanitize_html('<link rel="stylesheet" href="evil.css"><p>ok</p>')
    assert '<link' not in out


def test_meta_tag_stripped():
    out = sanitize_html('<meta http-equiv="refresh" content="0;url=evil"><p>ok</p>')
    assert '<meta' not in out


def test_base_tag_stripped():
    out = sanitize_html('<base href="https://evil.example/"><p>ok</p>')
    assert '<base' not in out


def test_form_tag_stripped():
    """<form> itself stripped but inner text preserved."""
    out = sanitize_html('<form action="evil"><input name="x"><p>content</p></form>')
    assert '<form' not in out
    assert 'content' in out


def test_noscript_tag_stripped():
    out = sanitize_html('<noscript><script>evil()</script></noscript>')
    assert 'evil' not in out
    assert '<noscript' not in out


def test_template_tag_stripped():
    out = sanitize_html('<template><script>evil()</script></template>')
    assert 'evil' not in out
    assert '<template' not in out


# ---------------------------------------------------------------------------
# Nested / obfuscated script tags
# ---------------------------------------------------------------------------


def test_nested_script_obfuscation():
    """<scr<script>ipt> obfuscation — sanitizer vs sandbox responsibility.

    Python's html.parser recognises and drops the inner <script> tag and its
    content.  The outer malformed `<scr...ipt>` context may leak residual text
    ('ipt>') but no executable script element remains.  The *sandbox* iframe
    is the primary barrier for this class of input (scripts inert under
    sandbox="" with no allow-scripts / no allow-same-origin).

    We assert here only what the sanitizer actually guarantees: no <script>
    element in the output.
    """
    inp = '<scr<script>ipt>alert(1)</scr<script>ipt>'
    out = sanitize_html(inp)
    assert '<script' not in out.lower()
    # Note: some text from inside the malformed outer tag may be present as
    # escaped text; that is benign because the sandbox iframe prevents execution.


def test_svg_script_stripped():
    out = sanitize_html('<svg><script>alert(1)</script></svg>')
    assert 'alert(1)' not in out
    assert '<script' not in out


# ---------------------------------------------------------------------------
# Event-handler attribute stripping
# ---------------------------------------------------------------------------


def test_onerror_stripped():
    out = sanitize_html('<img src="x" onerror="alert(1)">')
    assert 'onerror' not in out


def test_onclick_stripped():
    out = sanitize_html('<p onclick="evil()">click me</p>')
    assert 'onclick' not in out
    assert 'click me' in out


def test_onload_stripped():
    out = sanitize_html('<body onload="evil()"><p>text</p></body>')
    assert 'onload' not in out


def test_onmouseover_stripped():
    out = sanitize_html('<a href="x" onmouseover="steal()">link</a>')
    assert 'onmouseover' not in out
    assert 'link' in out


def test_onfocus_stripped():
    out = sanitize_html('<input onfocus="evil()" value="x">')
    assert 'onfocus' not in out


# ---------------------------------------------------------------------------
# Dangerous URL schemes in href/src
# ---------------------------------------------------------------------------


def test_href_javascript_stripped():
    out = sanitize_html('<a href="javascript:alert(1)">click</a>')
    # href must be gone
    assert 'javascript:' not in out
    # text content preserved
    assert 'click' in out


def test_href_javascript_mixed_case_stripped():
    out = sanitize_html('<a href="JavaScript:alert(1)">click</a>')
    assert 'javascript:' not in out.lower()


def test_href_vbscript_stripped():
    out = sanitize_html('<a href="vbscript:MsgBox(1)">click</a>')
    assert 'vbscript:' not in out


def test_href_data_html_stripped():
    out = sanitize_html('<a href="data:text/html,<script>alert(1)</script>">click</a>')
    assert 'data:text/html' not in out


def test_src_javascript_stripped():
    out = sanitize_html('<img src="javascript:alert(1)">')
    assert 'javascript:' not in out


def test_img_onerror_xss():
    """Canonical <img src=x onerror=...> XSS payload."""
    out = sanitize_html('<img src="x" onerror="fetch(\'/api/staging/1/pages/x\',{method:\'DELETE\'})">')
    assert 'onerror' not in out
    assert 'fetch' not in out
    assert 'DELETE' not in out


# ---------------------------------------------------------------------------
# Entity-encoded dangerous schemes
# ---------------------------------------------------------------------------


def test_entity_encoded_javascript_href():
    """&#106;avascript: should be decoded and rejected."""
    # &#106; = 'j', so this encodes "javascript:"
    out = sanitize_html('<a href="&#106;avascript:alert(1)">click</a>')
    assert 'javascript:' not in out
    # Ensure href is absent or safe
    assert 'alert(1)' not in out


def test_entity_encoded_full_javascript():
    # Full entity encoding: &#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;
    out = sanitize_html(
        '<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;alert(1)">x</a>'
    )
    assert 'javascript' not in out.lower()
    assert 'alert' not in out


def test_whitespace_obfuscated_javascript():
    """javascript: with embedded whitespace / tabs."""
    out = sanitize_html('<a href="java\tscript:alert(1)">x</a>')
    assert 'javascript' not in out.lower()
    assert 'alert(1)' not in out


# ---------------------------------------------------------------------------
# Benign content preserved
# ---------------------------------------------------------------------------


def test_benign_paragraph_preserved():
    out = sanitize_html('<p>Hello, <strong>world</strong>!</p>')
    assert '<p>' in out
    assert '<strong>' in out
    assert 'Hello' in out


def test_benign_anchor_preserved():
    out = sanitize_html('<a href="https://example.com" target="_blank" rel="noopener">link</a>')
    assert 'href="https://example.com"' in out
    assert 'link' in out


def test_benign_image_preserved():
    out = sanitize_html('<img src="https://example.com/image.png" alt="A photo">')
    assert 'src="https://example.com/image.png"' in out
    assert 'alt="A photo"' in out


def test_inline_style_preserved():
    out = sanitize_html('<p style="color:red;font-size:14px">text</p>')
    assert 'style=' in out
    assert 'color:red' in out


def test_class_and_id_preserved():
    out = sanitize_html('<div class="callout" id="block1">content</div>')
    assert 'class="callout"' in out
    assert 'id="block1"' in out


def test_headings_preserved():
    inp = '<h1>Title</h1><h2>Sub</h2><h3>Sub-sub</h3>'
    out = sanitize_html(inp)
    assert '<h1>' in out
    assert '<h2>' in out
    assert '<h3>' in out


def test_table_preserved():
    inp = '<table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td>1</td><td>2</td></tr></tbody></table>'
    out = sanitize_html(inp)
    assert '<table>' in out
    assert '<th>' in out
    assert '<td>' in out


def test_list_preserved():
    out = sanitize_html('<ul><li>Item 1</li><li>Item 2</li></ul>')
    assert '<ul>' in out
    assert '<li>' in out
    assert 'Item 1' in out


def test_aria_attrs_preserved():
    out = sanitize_html('<div role="alert" aria-live="polite">msg</div>')
    assert 'role="alert"' in out
    assert 'aria-live="polite"' in out


def test_data_attrs_preserved():
    out = sanitize_html('<div data-field="question_text">text</div>')
    assert 'data-field="question_text"' in out


def test_blockquote_preserved():
    out = sanitize_html('<blockquote><p>Quote</p></blockquote>')
    assert '<blockquote>' in out
    assert 'Quote' in out


def test_pre_code_preserved():
    out = sanitize_html('<pre><code>def foo(): pass</code></pre>')
    assert '<pre>' in out
    assert '<code>' in out
    assert 'def foo()' in out


# ---------------------------------------------------------------------------
# Unknown tags: inner text preserved, tag dropped
# ---------------------------------------------------------------------------


def test_unknown_tag_text_preserved():
    out = sanitize_html('<fancy-widget>some text</fancy-widget>')
    assert 'some text' in out
    assert '<fancy-widget' not in out


def test_unknown_tag_does_not_emit_closing():
    out = sanitize_html('<x-foo>text</x-foo>')
    assert '</x-foo>' not in out
    assert 'text' in out


# ---------------------------------------------------------------------------
# Empty / None input
# ---------------------------------------------------------------------------


def test_empty_string():
    assert sanitize_html('') == ''


def test_whitespace_only():
    out = sanitize_html('   ')
    # Should not crash; may return whitespace or empty.
    assert isinstance(out, str)


def test_plain_text_no_tags():
    out = sanitize_html('Hello world')
    assert 'Hello world' in out


# ---------------------------------------------------------------------------
# Malformed HTML resilience
# ---------------------------------------------------------------------------


def test_unclosed_tag():
    out = sanitize_html('<p>unclosed paragraph')
    assert 'unclosed paragraph' in out


def test_malformed_does_not_crash():
    """HTMLParser should handle broken HTML gracefully."""
    out = sanitize_html('<<script>alert(1)</script>>')
    assert 'alert(1)' not in out


def test_deeply_nested_scripts():
    inp = '<div>' + '<script>' * 10 + 'evil()' + '</script>' * 10 + '</div>'
    out = sanitize_html(inp)
    assert 'evil()' not in out
    assert '<script' not in out
