"""Tests für app.report — insbesondere XSS/XXE-Escape-Verhalten.

Abdeckung der Bug-Klasse aus Issue #57 (Security Audit, Befunde #2 und #6):
das LLM-Output landet direkt in der HTML-Vorlage, die von WeasyPrint
gerendert wird. Ohne Escape kann eine Prompt-Injection mit einem rohen
``<img src="file:///etc/passwd">`` Local File Read im Container auslösen
oder ``<script>``-Payloads im Browser des Lesers ausführen, der den
HTML-Report über ``/result/{job_id}`` öffnet.

Diese Tests bauen ein Assessment mit kontrollierten XSS-Payloads in jedem
LLM-getragenen Feld und verifizieren, dass die generierte HTML die rohen
Payloads nicht enthält und die escapeden Varianten dafür schon.
"""
from __future__ import annotations

import asyncio
from pathlib import Path

import pytest

from app.models import (
    Assessment,
    Empfehlung,
    FraktionScores,
    MatrixEntry,
    ProgrammScore,
    Verbesserung,
    Verbesserungspotenzial,
)
from app.report import build_matrix_html, format_redline_html, generate_html_report


# ─────────────────────────────────────────────────────────────────────────────
# format_redline_html unit tests
# ─────────────────────────────────────────────────────────────────────────────


class TestFormatRedlineHtml:
    """``format_redline_html`` muss escape-first-then-replace fahren, sonst
    überleben rohe HTML-Tags aus dem LLM die Konvertierung."""

    def test_plain_text_passes_through(self):
        assert format_redline_html("hallo welt") == "hallo welt"

    def test_inserted_marker_becomes_span(self):
        assert format_redline_html("**neu**") == '<span class="inserted">neu</span>'

    def test_deleted_marker_becomes_span(self):
        assert format_redline_html("~~weg~~") == '<span class="deleted">weg</span>'

    def test_script_tag_in_input_is_escaped(self):
        out = format_redline_html("vor <script>alert(1)</script> nach")
        assert "<script>" not in out
        assert "&lt;script&gt;" in out

    def test_local_file_image_in_input_is_escaped(self):
        out = format_redline_html('<img src="file:///etc/passwd">')
        assert '<img src="file:///etc/passwd">' not in out
        assert "&lt;img" in out

    def test_marker_inside_escaped_tag_still_renders(self):
        # Edge: ein Angreifer kann seine Payload in **markern** einbetten,
        # die Marker müssen weiterhin als spans rendern, der HTML-Inhalt
        # aber escaped sein.
        out = format_redline_html("**<script>x</script>**")
        assert "<span class=\"inserted\">" in out
        assert "<script>" not in out
        assert "&lt;script&gt;" in out


# ─────────────────────────────────────────────────────────────────────────────
# build_matrix_html title-attribute escape
# ─────────────────────────────────────────────────────────────────────────────


def _minimal_assessment(**overrides) -> Assessment:
    """Build a smallest-possible Assessment with all required fields,
    plus deep-link overrides for the field under test."""
    base = dict(
        drucksache="18/12345",
        title="Demo-Antrag",
        fraktionen=["CDU"],
        datum="2026-04-09",
        link=None,
        gwoe_score=5.0,
        gwoe_begruendung="Begründung.",
        gwoe_matrix=[
            MatrixEntry(field="A1", label="Lieferant:innen × 1", aspect="kein Aspekt", rating=0),
        ],
        gwoe_schwerpunkt=["Solidarität"],
        wahlprogramm_scores=[
            FraktionScores(
                fraktion="CDU",
                wahlprogramm=ProgrammScore(score=5.0, begruendung="ok", zitate=[]),
                parteiprogramm=ProgrammScore(score=5.0, begruendung="ok", zitate=[]),
            ),
        ],
        verbesserungen=[],
        staerken=["eine stärke"],
        schwaechen=["eine schwäche"],
        empfehlung=Empfehlung.UEBERARBEITEN,
        verbesserungspotenzial=Verbesserungspotenzial.MITTEL,
        themen=[],
        antrag_zusammenfassung="zusammenfassung",
        antrag_kernpunkte=["punkt eins"],
    )
    base.update(overrides)
    return Assessment(**base)


class TestBuildMatrixHtml:
    def test_aspect_with_quote_does_not_break_title_attribute(self):
        """Ein nacktes ``"`` in ``aspect`` darf das ``title="..."``-Attribut
        nicht beenden — sonst kann ein folgender ``onmouseover=...``-Token
        zu einem echten Event-Handler werden. Entscheidender Check: das
        rohe ``"`` ist im Output durch ``&quot;`` ersetzt, sodass das
        Attribut geschlossen bleibt."""
        a = _minimal_assessment(
            gwoe_matrix=[
                MatrixEntry(
                    field="A1",
                    label="Lieferant:innen × 1",
                    aspect='" onmouseover="alert(1)',
                    rating=1,
                ),
            ],
        )
        html = build_matrix_html(a)
        # Das rohe " aus dem aspect darf nicht mehr im title-Wert stehen
        assert '"" onmouseover' not in html  # Attribut-Breakout
        assert '" onmouseover="alert' not in html
        # Stattdessen muss die escapete Form vorhanden sein
        assert "&quot;" in html


# ─────────────────────────────────────────────────────────────────────────────
# generate_html_report end-to-end XSS escape
# ─────────────────────────────────────────────────────────────────────────────


_XSS = "<script>alert('xss')</script>"
_XXE = '<img src="file:///etc/passwd">'


def _build_xss_assessment() -> Assessment:
    return _minimal_assessment(
        title=f"Antrag {_XSS}",
        gwoe_begruendung=f"Begründung mit {_XSS}",
        gwoe_schwerpunkt=[f"Schwerpunkt {_XXE}"],
        antrag_zusammenfassung=f"Zusammenfassung mit {_XXE}",
        antrag_kernpunkte=[f"Kernpunkt mit {_XSS}"],
        staerken=[f"Stärke mit {_XSS}"],
        schwaechen=[f"Schwäche mit {_XXE}"],
        gwoe_matrix=[
            MatrixEntry(field="A1", label="Lieferant:innen × 1",
                        aspect=f"Aspekt {_XSS}", rating=2),
        ],
        wahlprogramm_scores=[
            FraktionScores(
                fraktion=f"Fraktion {_XSS}",
                wahlprogramm=ProgrammScore(score=5.0, begruendung=f"WP {_XXE}", zitate=[]),
                parteiprogramm=ProgrammScore(score=5.0, begruendung=f"PP {_XSS}", zitate=[]),
            ),
        ],
        verbesserungen=[
            Verbesserung(
                original=f"Original {_XSS}",
                vorschlag=f"Vorschlag **mit {_XSS} markup**",
                begruendung=f"Begründung {_XXE}",
            ),
        ],
    )


def test_generate_html_report_escapes_all_llm_payloads(tmp_path: Path):
    a = _build_xss_assessment()
    out = tmp_path / "report.html"
    asyncio.run(generate_html_report(a, out, bundesland="NRW"))
    html = out.read_text()

    # Negative: keine rohen Angriffs-Strings
    assert "<script>" not in html, "rohes <script> aus LLM-Felder im Output"
    assert "alert('xss')" not in html
    assert 'src="file:///etc/passwd"' not in html, (
        "rohes file:// in HTML würde WeasyPrint zum Local-File-Read verleiten"
    )

    # Positive: escapete Form ist vorhanden (also wurde der Payload überhaupt
    # mit-gerendert, nur eben sicher)
    assert "&lt;script&gt;" in html
    assert "&lt;img" in html

    # Format-Redline-Marker müssen weiterhin funktionieren (Vorschlag mit **)
    assert '<span class="inserted">' in html
