"""Verify that inline <script> blocks in admin templates parse as valid JS.

We had a regression where an Italian translation containing an apostrophe
(e.g. "Errore nell'eliminazione") broke a single-quoted JS string literal,
killing every Alpine binding on the page (the "Nuova Stazione" button no
longer triggered openCreateModal).

Strategy: render each admin page through the Flask test client, extract every
inline <script> body and feed it to `node --check` for a syntax verdict. We
force the IT locale so the worst-case translations (which contain apostrophes)
are the ones evaluated.

The test is skipped if `node` is not on PATH; CI images that exclude Node
get a soft skip rather than a false failure.
"""
from __future__ import annotations

import os
import re
import shutil
import subprocess
import tempfile
from unittest.mock import MagicMock, patch

import pytest


# Match every inline <script>…</script> (i.e. without `src=`) so we don't try
# to syntax-check Alpine.js / Plotly bundles served from a CDN.
_INLINE_SCRIPT_RX = re.compile(
    r"<script(?![^>]*\bsrc=)[^>]*>(.*?)</script>",
    re.DOTALL | re.IGNORECASE,
)

# Match every Alpine.js / event-binding attribute on a tag — the bug class we
# care about (apostrophe in IT translation closing a JS literal) hides equally
# well inside x-text="…" as inside <script>…</script>.
#
# Covers: x-data, x-init, x-text, x-html, x-show, x-if, x-for, x-bind:foo,
# x-on:foo, x-model, x-effect, x-transition:*, plus the @evt and :attr
# shortcuts.
_ALPINE_ATTR_RX = re.compile(
    r"""(?P<name>(?:x-[a-zA-Z][a-zA-Z0-9:-]*|@[a-zA-Z][a-zA-Z0-9.:-]*|:[a-zA-Z][a-zA-Z0-9.:-]*))="(?P<value>[^"]*)\"""",
    re.DOTALL,
)

# Skip these — they're plain identifiers / object shapes, not JS expressions
# that node --check should reject for things like a stray apostrophe.
_ALPINE_NON_EXPR_NAMES = {
    # transition modifiers like x-transition:enter-start get bare class names
    "x-transition:enter",
    "x-transition:enter-start",
    "x-transition:enter-end",
    "x-transition:leave",
    "x-transition:leave-start",
    "x-transition:leave-end",
    # x-cloak has no value, x-data is parsed below as an expression but Alpine
    # also accepts an object literal.
}


def _alpine_attribute_expressions(html: str):
    """Yield (attr_name, value) tuples for every Alpine expression attribute."""
    for m in _ALPINE_ATTR_RX.finditer(html):
        name = m.group("name")
        if name in _ALPINE_NON_EXPR_NAMES:
            continue
        value = m.group("value")
        if not value.strip():
            continue
        # HTML entities used to embed quotes inside the attribute value
        # (most common: &quot; for ").
        value = (
            value.replace("&quot;", '"')
                 .replace("&#34;", '"')
                 .replace("&#39;", "'")
                 .replace("&amp;", "&")
        )
        yield name, value


def _node_check(source: str, label: str) -> None:
    """Fail the test if `node --check` rejects `source`."""
    if not source.strip():
        return
    node = shutil.which("node")
    if node is None:
        pytest.skip("node binary not found on PATH; cannot validate JS syntax")

    fd, path = tempfile.mkstemp(suffix=".js")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(source)
        result = subprocess.run(
            [node, "--check", path],
            capture_output=True,
            text=True,
            timeout=10,
        )
    finally:
        os.unlink(path)

    if result.returncode != 0:
        snippet = source.strip()
        if len(snippet) > 600:
            snippet = snippet[:600] + "\n…(truncated)…"
        pytest.fail(
            f"{label}: node rejected this code as invalid JS.\n"
            f"--- node stderr ---\n{result.stderr.strip()}\n"
            f"--- source (first 600 chars) ---\n{snippet}"
        )


def _check_alpine_attributes(html: str, page_label: str) -> None:
    """Validate every Alpine expression attribute on the page.

    Wraps each value in `void (…)` so node parses it as an expression rather
    than a statement. Function-body forms like `async () => { … }` parse fine
    inside that wrapper too.
    """
    for name, value in _alpine_attribute_expressions(html):
        # Some Alpine attrs accept a function call shorthand (e.g.
        # x-data="myComponent(window.__x)"); those parse fine as expressions.
        wrapper = f"void ({value});\n"
        _node_check(
            wrapper,
            f"{page_label} attribute {name}=\"…\" did not parse as JS",
        )


def _force_italian(client) -> None:
    """Make the next request render with the IT locale (worst case for JS)."""
    with client.session_transaction() as sess:
        sess["language"] = "it"


@pytest.fixture
def mock_admin_api():
    """Patch api_client used inside the admin blueprint."""
    mock = MagicMock()
    with patch("blueprints.admin.api_client", mock):
        yield mock


def test_admin_stations_inline_js_is_valid(logged_in_client, mock_admin_api):
    """The /admin/stations page must emit syntactically valid inline JS in IT.

    Regression guard for the apostrophe-in-single-quote bug.
    """
    mock_admin_api.get.side_effect = [
        # GET /api/stations
        [{
            "id": 1, "code": "ST-DEFAULT", "name": "Default Station",
            "location": None, "notes": None, "active": True,
            "created_by": 1, "created_at": "2026-04-25T10:00:00",
        }],
        # GET /api/recipes
        [{"id": 1, "code": "DEMO-001", "name": "Demo Recipe", "active": True}],
    ]

    _force_italian(logged_in_client)
    resp = logged_in_client.get("/admin/stations")
    assert resp.status_code == 200, resp.data[:300]

    html = resp.data.decode("utf-8")
    scripts = _INLINE_SCRIPT_RX.findall(html)
    assert scripts, "expected at least one inline <script> on /admin/stations"

    for i, body in enumerate(scripts):
        _node_check(body, f"/admin/stations script[{i}]")

    _check_alpine_attributes(html, "/admin/stations")


def test_admin_users_inline_js_is_valid(logged_in_client, mock_admin_api):
    """Same guard for /admin/users (reuses the userManagement Alpine pattern)."""
    mock_admin_api.get.return_value = [{
        "id": 1, "username": "admin", "display_name": "Admin",
        "email": None, "roles": ["Maker"], "is_admin": True,
        "language_pref": "it", "theme_pref": "light", "active": True,
    }]

    _force_italian(logged_in_client)
    resp = logged_in_client.get("/admin/users")
    assert resp.status_code == 200

    html = resp.data.decode("utf-8")
    scripts = _INLINE_SCRIPT_RX.findall(html)
    assert scripts

    for i, body in enumerate(scripts):
        _node_check(body, f"/admin/users script[{i}]")

    _check_alpine_attributes(html, "/admin/users")
