"""W20A2: review HTML emitter tests.

LLM / image / VLM / DB / ImageAsset write 0. Pure rendering function.

Covers:
- HTML contains fp_id, grid size, marker dots, camera point candidates,
  view cone polygons, per-bg facts table, diagnostics.
- Output is deterministic (byte-identical for identical input).
- Runtime labels pass through unescaped of meaning but HTML-escaped of
  syntax (no scenario-specific token leakage in static text).
"""
from __future__ import annotations

from app.modules.pipeline.floor_plan_geometry_readback import (
    BASE_OPENING,
    BASE_PERSISTENT_FIXTURE,
    BASE_PERSISTENT_FURNITURE,
    BASE_STRUCTURAL_UNIT,
    compute_geometry_candidates,
    compute_synthetic_readback_fixture,
)
from app.modules.pipeline.floor_plan_review_html import render_review_html


def _dossier() -> dict:
    return {
        "fp_id": "fp_a",
        "fp_image_path": "/p/c/e/floor_plan_render/fp_a.png",
        "grid_size": [10, 10],
        "dwelling_identity": {
            "standard_of_living_band": "modest_residential",
        },
        "base_marker_inventory": [
            {"number": 1, "label": "primary unit", "category": "area",
             "position_hint": "center",
             "base_layer_decision": BASE_STRUCTURAL_UNIT},
            {"number": 2, "label": "secondary unit", "category": "area",
             "position_hint": "east",
             "base_layer_decision": BASE_STRUCTURAL_UNIT},
            {"number": 3, "label": "interior opening", "category": "opening",
             "position_hint": "between 1 and 2",
             "base_layer_decision": BASE_OPENING},
            {"number": 4, "label": "service counter", "category": "furniture",
             "position_hint": "north wall",
             "base_layer_decision": BASE_PERSISTENT_FIXTURE},
            {"number": 5, "label": "anchor seating", "category": "furniture",
             "position_hint": "south of unit 1",
             "base_layer_decision": BASE_PERSISTENT_FURNITURE},
        ],
        "per_bg_render_facts_by_bg_id": {
            "L01B01": {
                "bg_id": "L01B01",
                "target_unit_marker_numbers": [1],
                "dominant_target_unit_marker_number": 1,
                "use_numbered_elements": [1, 3, 4],
                "base_marker_numbers_to_reference": [1, 3, 4],
                "transient_marker_numbers_to_describe": [],
                "clean_background_expected": True,
                "applies_to_shots": ["S1_Shot1"],
                "depends_on_bg": [],
            },
        },
        "diagnostics": ["dossier-level diagnostic example"],
    }


def _g():
    rb = compute_synthetic_readback_fixture(dossier=_dossier())
    return compute_geometry_candidates(dossier=_dossier(), readback=rb)


def test_html_starts_with_doctype_and_is_string():
    html = render_review_html(dossier=_dossier(), geometry=_g())
    assert isinstance(html, str)
    assert html.startswith("<!doctype html>")
    assert html.rstrip().endswith("</html>")


def test_html_contains_fp_id_and_grid_label():
    html = render_review_html(dossier=_dossier(), geometry=_g())
    assert "fp_id=fp_a" in html
    assert "10×10" in html
    assert "readback_status: synthetic_fixture" in html


def test_html_contains_marker_dots_for_each_inventory_entry():
    html = render_review_html(dossier=_dossier(), geometry=_g())
    # Each of the 5 markers renders as `>N</text>` inside an <svg>.
    for n in range(1, 6):
        assert f">{n}</text>" in html


def test_html_contains_camera_point_rings_per_unit():
    html = render_review_html(dossier=_dossier(), geometry=_g())
    # Two units → two "cam · u…" labels.
    assert html.count("cam · u") == 2


def test_html_contains_view_cone_polygons_and_arrows():
    html = render_review_html(dossier=_dossier(), geometry=_g())
    # 2 units × 3 lens = 6 cones.
    assert html.count('class="fp-view-cone') == 6
    # 2 units × 1 other unit = 2 direction arrows.
    assert html.count('class="fp-direction-arrow"') == 2


def test_html_contains_per_bg_facts_row():
    html = render_review_html(dossier=_dossier(), geometry=_g())
    assert "<td>L01B01</td>" in html
    # Runtime label / shot id passes through verbatim (data, not contract).
    assert "S1_Shot1" in html


def test_html_contains_diagnostics_blocks_and_anchor_warning():
    html = render_review_html(dossier=_dossier(), geometry=_g())
    assert "geometry diagnostics" in html
    assert "wall/door invalidation diagnostics" in html
    assert "dossier diagnostics" in html
    # Anchor warning pill is mandatory.
    assert "NOT a final BG" in html


def test_html_is_deterministic_byte_identical_for_same_input():
    html_a = render_review_html(dossier=_dossier(), geometry=_g())
    html_b = render_review_html(dossier=_dossier(), geometry=_g())
    assert html_a == html_b


def test_html_escapes_runtime_label_html_syntax_safely():
    d = _dossier()
    # Inject angle brackets into a runtime label — must be escaped, not
    # rendered as raw HTML (read-only review surface).
    d["per_bg_render_facts_by_bg_id"]["L01B01"]["applies_to_shots"] = [
        "<script>danger</script>",
    ]
    html = render_review_html(dossier=d, geometry=_g())
    # Raw <script> tag should not appear (escaped) in the rendered HTML.
    assert "<script>danger</script>" not in html
    assert "&lt;script&gt;danger&lt;/script&gt;" in html


def test_html_svg_defs_sits_inside_svg_in_order():
    """Codex W20A2 review #1: <defs> must be INSIDE <svg> so
    marker-end="url(#fp-arrow-head)" resolves. The expected ordering is
    <svg ...><defs>…</defs>…marker-end=…</svg>."""
    html = render_review_html(dossier=_dossier(), geometry=_g())
    svg_open_idx = html.find("<svg ")
    defs_idx = html.find("<defs>")
    marker_ref_idx = html.find('marker-end="url(#fp-arrow-head)"')
    svg_close_idx = html.find("</svg>")
    # All four anchors must exist.
    assert svg_open_idx != -1
    assert defs_idx != -1
    assert marker_ref_idx != -1
    assert svg_close_idx != -1
    # Ordering: svg open < defs < marker-end < svg close.
    assert svg_open_idx < defs_idx < marker_ref_idx < svg_close_idx, (
        f"SVG/defs/marker-end/close ordering wrong: "
        f"svg={svg_open_idx} defs={defs_idx} "
        f"marker-end={marker_ref_idx} </svg>={svg_close_idx}"
    )
    # And the arrow-head definition is between <defs> and </defs>.
    defs_close_idx = html.find("</defs>")
    assert defs_idx < html.find("id=\"fp-arrow-head\"") < defs_close_idx
    assert defs_close_idx < svg_close_idx


def test_html_renders_cones_rotated_for_vertical_direction_fixture():
    """Codex W20A2 review #2: with a vertical direction fixture
    (camera and look-at on the same column), at least one cone
    polygon edge must have ``y`` coordinates that differ noticeably
    in the vertical axis. The previous (always-east) implementation
    placed every cone edge to the right of the apex — the same Y row
    twice — so this test would fail under that version."""
    # Force a vertical-direction fixture.
    import math
    from app.modules.pipeline.floor_plan_geometry_readback import (
        compute_geometry_candidates as _cgc,
    )
    dossier = _dossier()
    readback = {
        "status": "ok",
        "fp_id": "fp_a",
        "grid_size": [10, 10],
        "observed_markers": [
            {"number": 1, "row": 1, "col": 4, "kind": BASE_STRUCTURAL_UNIT},
            {"number": 2, "row": 7, "col": 4, "kind": BASE_STRUCTURAL_UNIT},
            {"number": 3, "row": 4, "col": 4, "kind": BASE_OPENING},
            {"number": 4, "row": 0, "col": 0,
             "kind": BASE_PERSISTENT_FIXTURE},
            {"number": 5, "row": 9, "col": 9,
             "kind": BASE_PERSISTENT_FURNITURE},
        ],
        "missing_markers": [], "extra_markers": [],
        "confidence": None, "diagnostics": [],
    }
    g = _cgc(dossier=dossier, readback=readback)
    html = render_review_html(dossier=dossier, geometry=g)
    # Extract polygon "points" attributes for every cone.
    # Each cone is `<polygon points="ax,ay e1x,e1y e2x,e2y" ...`.
    import re
    polygons = re.findall(
        r'<polygon points="([^"]+)" fill="[^"]+" opacity="0\.08"',
        html,
    )
    assert polygons, "expected ≥1 view-cone polygon"
    # Verify at least one cone has edges that are NOT both on the
    # right of the apex (i.e. the polygon is rotated off the +x axis).
    rotated_off_east = 0
    for pts in polygons:
        coords = [tuple(map(float, p.split(","))) for p in pts.split()]
        assert len(coords) == 3
        apex, e1, e2 = coords
        # Vertical (north or south) direction → both edges share apex.x
        # ± symmetric, and y differs significantly from apex.y.
        if abs(e1[1] - apex[1]) > 5 and abs(e2[1] - apex[1]) > 5:
            rotated_off_east += 1
    assert rotated_off_east > 0, (
        "no cone polygon rotated off the +x axis — vertical-direction "
        "fixture should have produced at least one cone whose edges "
        "extend above or below the apex y-coordinate."
    )


def test_html_static_text_carries_no_scenario_specific_tokens():
    """Render HTML using only generic enum / shape-kind labels and
    confirm the static text does not contain forbidden scenario-specific
    literal tokens.

    F-11 applies to STATIC HTML emitter / template / code only;
    runtime labels (data) may carry scenario-specific strings. So this
    test renders against a fixture with **only generic labels** and
    inspects the resulting HTML.
    """
    d = _dossier()
    # Strip any scenario-flavored runtime labels — leave only generic ones.
    for entry in d["base_marker_inventory"]:
        entry["label"] = "generic_label"
        entry["position_hint"] = "generic_position"
    d["per_bg_render_facts_by_bg_id"] = {}
    d["diagnostics"] = []
    html = render_review_html(dossier=d, geometry=_g())
    # These tokens are example scenario-specific literals that have
    # appeared in earlier prompts and must NEVER show up in static
    # emitter text. The test asserts none of them is present.
    forbidden = [
        "rooftop", "옥탑방", "police", "L05", "L14",
        "민숙", "수리영", "kitchen counter", "bath fixtures",
    ]
    for tok in forbidden:
        assert tok not in html, f"scenario-specific token {tok!r} leaked into static HTML"
