"""Tests for report-renderer escaping/injection hardening (issue #215).

Covers three low-severity hardening gaps in the audit/assurance report
renderers:

1. audit_report._safe_url rejects javascript:/vbscript:/data:-html URLs
   before they can be interpolated into an href built from non-static data
   (affected_pages link, design-standard canvas_link, accessibility
   canvas_link).
2. audit_report._escape and assurance_batch._esc both escape single quotes
   (matching html.escape(s, quote=True) semantics), closing a latent
   attribute-breakout gap.
3. audit_report's post-generation `open`/`xdg-open` launch (--open) is
   handed an absolute path that cannot be misparsed as a CLI flag, even
   when an operator-supplied --output/--xlsx-output value starts with '-'.
"""
import os
import subprocess
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import audit_report
import assurance_batch


# ---------------------------------------------------------------------------
# 1. javascript: URLs blanked/rejected in href/src
# ---------------------------------------------------------------------------

class TestSafeUrlHelper:
    def test_javascript_scheme_rejected(self):
        assert audit_report._safe_url("javascript:alert(1)") == ""

    def test_javascript_scheme_rejected_case_insensitive(self):
        assert audit_report._safe_url("JavaScript:alert(1)") == ""

    def test_javascript_scheme_rejected_with_leading_whitespace(self):
        assert audit_report._safe_url("   javascript:alert(1)") == ""

    def test_vbscript_and_data_html_schemes_rejected(self):
        assert audit_report._safe_url("vbscript:msgbox(1)") == ""
        assert audit_report._safe_url("data:text/html,<script>alert(1)</script>") == ""

    def test_http_https_mailto_pass_through_unchanged(self):
        assert (audit_report._safe_url("https://canvas.asu.edu/courses/1/pages/foo")
                == "https://canvas.asu.edu/courses/1/pages/foo")
        assert (audit_report._safe_url("http://canvas.asu.edu/courses/1")
                == "http://canvas.asu.edu/courses/1")
        assert audit_report._safe_url("mailto:id@asu.edu") == "mailto:id@asu.edu"

    def test_relative_url_passes_through_unchanged(self):
        assert audit_report._safe_url("/courses/1/pages/foo") == "/courses/1/pages/foo"

    def test_empty_and_none_are_blank(self):
        assert audit_report._safe_url("") == ""
        assert audit_report._safe_url(None) == ""


def _malicious_report_data():
    """Minimal audit_report data dict with javascript: URLs at every
    non-static href site (affected_pages url, design-standard canvas_link,
    accessibility canvas_link) -- mirrors data that would originate from
    Canvas page content (e.g. issue.get("url"))."""
    return {
        "course": {"name": "Escaping Test Course", "id": "999999"},
        "audit_date": "2026-01-01T00:00:00",
        "auditor": "Test",
        "sections": {
            "design_standards": {
                "summary": {"Not Met": 1},
                "items": [
                    {
                        "id": "01",
                        "name": "Test Standard",
                        "status": "Not Met",
                        "evidence": "test evidence marker",
                        "canvas_link": "javascript:alert(document.cookie)",
                        "criteria_results": [
                            {
                                "criterion_id": "01.1",
                                "status": "Not Met",
                                "evidence": "test",
                                "affected_pages": [
                                    {
                                        "url": "javascript:alert(1)",
                                        "title": "Evil Page Title",
                                        "issue_summary": "bad",
                                    }
                                ],
                            }
                        ],
                    }
                ],
            },
            "accessibility": {
                "summary": {"Critical": 1},
                "items": [
                    {
                        "severity": "Critical",
                        "page": "evil-a11y-page",
                        "issue": "test issue",
                        "element": "<img>",
                        "fix": "test fix",
                        "canvas_link": "javascript:alert(2)",
                    }
                ],
            },
        },
    }


class TestReportRendererBlocksJavascriptHref:
    """End-to-end: generate_report() must never emit a javascript: href,
    even when fed audit data whose canvas_link / affected_pages url fields
    are attacker-controlled (e.g. sourced from Canvas page content)."""

    def test_no_javascript_href_anywhere_in_output(self):
        """The javascript: payload must not land inside an href/src
        attribute. (It legitimately still appears elsewhere as escaped,
        inert display text -- e.g. the "Object:" debug annotation renders
        the raw canvas_link/url as `_escape()`-d text for troubleshooting,
        which is safe and out of scope here; only the executable-attribute
        context is the vulnerability.)"""
        html = audit_report.generate_report(_malicious_report_data())
        lowered = html.lower()
        assert 'href="javascript:' not in lowered
        assert "href='javascript:" not in lowered

    def test_no_vbscript_or_data_html_href_in_output(self):
        data = _malicious_report_data()
        data["sections"]["design_standards"]["items"][0]["canvas_link"] = "vbscript:msgbox(1)"
        data["sections"]["accessibility"]["items"][0]["canvas_link"] = "data:text/html,<script>1</script>"
        html = audit_report.generate_report(data)
        lowered = html.lower()
        assert 'href="vbscript:' not in lowered
        assert "href='vbscript:" not in lowered
        assert 'href="data:text/html' not in lowered
        assert "href='data:text/html" not in lowered

    def test_surrounding_content_still_renders(self):
        """The fix should blank the dangerous URL in place, not silently
        drop the whole finding card."""
        html = audit_report.generate_report(_malicious_report_data())
        assert "test evidence marker" in html
        assert "Evil Page Title" in html

    def test_safe_url_still_renders_as_a_real_href(self):
        """Regression guard: a legitimate https:// canvas_link must still
        come through as a clickable href -- the fix should reject only
        dangerous schemes, not links in general."""
        data = _malicious_report_data()
        safe_link = "https://canvas.asu.edu/courses/999999/pages/safe-page"
        data["sections"]["accessibility"]["items"][0]["canvas_link"] = safe_link
        html = audit_report.generate_report(data)
        assert f'href="{safe_link}"' in html


# ---------------------------------------------------------------------------
# 2. Single-quote escaping in _escape / _esc
# ---------------------------------------------------------------------------

class TestSingleQuoteEscaping:
    def test_audit_report_escape_encodes_single_quote(self):
        assert audit_report._escape("it's a test") == "it&#x27;s a test"

    def test_audit_report_escape_still_encodes_existing_chars(self):
        # Guards against regressing the pre-existing escape behavior this
        # fix builds on top of.
        assert audit_report._escape("<script>") == "&lt;script&gt;"
        assert audit_report._escape('"hello"') == "&quot;hello&quot;"

    def test_assurance_batch_esc_encodes_single_quote(self):
        assert assurance_batch._esc("it's a test") == "it&#x27;s a test"

    def test_assurance_batch_esc_still_encodes_existing_chars(self):
        assert assurance_batch._esc("<script>") == "&lt;script&gt;"
        assert assurance_batch._esc('"hello"') == "&quot;hello&quot;"

    def test_single_quote_cannot_break_out_of_attribute(self):
        """A value containing a single quote must not be able to close a
        single-quoted HTML attribute early."""
        payload = "x' onmouseover='alert(1)"
        assert "'" not in audit_report._escape(payload)
        assert "'" not in assurance_batch._esc(payload)


# ---------------------------------------------------------------------------
# 3. subprocess.run(['open'/'xdg-open', path]) argv safety
# ---------------------------------------------------------------------------

class TestSafeOpenArg:
    def test_dash_leading_relative_path_becomes_absolute(self):
        result = audit_report._safe_open_arg("-rf-report.html")
        assert not result.startswith("-")
        assert os.path.isabs(result)

    def test_normal_path_resolves_to_its_absolute_form(self, tmp_path):
        p = tmp_path / "report.html"
        assert audit_report._safe_open_arg(p) == os.path.abspath(str(p))

    def test_faculty_mode_open_argv_cannot_be_parsed_as_flag(self, monkeypatch, tmp_path):
        """--format faculty --output=-rf-report.txt --open must not hand
        subprocess.run(['open', '-rf-report.txt']) -- a value `open` could
        parse as the flags -r -f instead of a filename."""
        monkeypatch.chdir(tmp_path)
        monkeypatch.setattr(audit_report, "PLUGIN_ROOT", tmp_path)
        monkeypatch.setattr(audit_report, "REPORTS_DIR", tmp_path / "reports")

        captured = {}

        def fake_run(cmd, *a, **k):
            captured["cmd"] = cmd

        monkeypatch.setattr(subprocess, "run", fake_run)
        monkeypatch.setattr(sys, "platform", "darwin")
        monkeypatch.setattr(
            sys,
            "argv",
            ["audit_report.py", "--demo", "--format", "faculty",
             "--output=-rf-report.txt", "--open"],
        )

        audit_report.main()

        assert "cmd" in captured, "subprocess.run was never called"
        cmd = captured["cmd"]
        assert cmd[0] == "open"
        opened_path = cmd[1]
        assert not opened_path.startswith("-"), (
            f"argv {opened_path!r} could be misparsed as a CLI flag by `open`"
        )
        assert os.path.isabs(opened_path)
