#!/bin/bash
# Frontend design-system lint guards.
# See docs/design-system.md "Inline-style rules" and docs/ux-audit/remediation-plan.md Step 10.
# Exit 0 = clean. Exit non-zero = violations found (printed with file:line).

set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FRONTEND="$ROOT/src-tauri/frontend"
SRC_JS="$FRONTEND/js"
SRC_HTML="$FRONTEND/index.html $FRONTEND/compose.html"
SRC_CSS="$FRONTEND/css/styles.css"

violations=0

report() {
    local rule="$1"; shift
    local msg="$1"; shift
    if [ -n "$*" ]; then
        echo
        echo "[$rule] $msg"
        echo "$*"
        violations=$((violations + 1))
    fi
}

# 1. No raw hex literals in JS or source HTML (HTML entities &#NNNN; are OK;
#    <meta name="theme-color"> is a documented exception — value is set
#    programmatically by js/themes.js to track the active theme).
hits=$(grep -rnE '#[0-9a-fA-F]{3,8}\b' "$SRC_JS" $SRC_HTML 2>/dev/null \
    | grep -vE '&#[0-9]+;' \
    | grep -vE 'meta name="theme-color"' \
    || true)
report "no-raw-hex" "Raw hex literal in JS/HTML — use a CSS class or token instead." "$hits"

# 2. No style.cssText anywhere in JS.
hits=$(grep -rn 'cssText' "$SRC_JS" 2>/dev/null || true)
report "no-csstext" "style.cssText injection — move styles into a CSS class." "$hits"

# 3. No var(--token, #fallback). Fallback hex defeats theme switching.
hits=$(grep -rnE 'var\(--[a-z-]+,\s*#' "$FRONTEND" --include='*.js' --include='*.html' --include='styles.css' 2>/dev/null | grep -v styles.min.css || true)
report "no-var-fallback-hex" "var(--token, #fallback) — drop the fallback; it bypasses themes." "$hits"

# 4. No window.confirm / bare confirm() — route through GoingsOn.ui.showConfirmDialog.
#    Skip js/tests/: test fixtures carry attack payloads and mocks that
#    legitimately contain `confirm(`/`alert(` as data, not as app calls.
hits=$(grep -rnE '\b(window\.)?confirm\(' "$SRC_JS" 2>/dev/null | grep -v '/tests/' | grep -vE 'showConfirmDialog|confirmDelete|//\s*\*|\*\s' || true)
report "no-window-confirm" "window.confirm() — use GoingsOn.ui.showConfirmDialog instead." "$hits"

# 5. No inline style= that touches color / background / border / shadow / font / padding values.
hits=$(grep -rnE 'style="[^"]*(color|background|border|shadow|font-size|font-family|padding)' "$SRC_JS" $SRC_HTML 2>/dev/null || true)
report "no-styled-attrs" "Inline style= with color/background/border/shadow/font/padding — use a class." "$hits"

# 6. Deprecated empty-state classes have been removed.
hits=$(grep -rnE 'empty-dashboard-list|kanban-empty|virtual-scroller-empty' "$FRONTEND" --include='*.js' --include='*.html' --include='styles.css' 2>/dev/null | grep -v styles.min.css || true)
report "no-deprecated-empty-states" "Deprecated class — use .empty-state with --compact / --dashboard / --error." "$hits"

# 7. No native browser dialogs. Charter rule from Phase 7 roll-up.
#    Skip js/tests/ (attack payloads / mocks reference these as data, not calls).
hits=$(grep -rnE '\b(window\.)?(confirm|prompt|alert)\(' "$SRC_JS" 2>/dev/null \
    | grep -v '/tests/' \
    | grep -vE 'showConfirmDialog|showPromptDialog|confirmDelete|//\s|\*\s' || true)
report "no-native-dialogs" "window.confirm/prompt/alert are banned — use GoingsOn.ui.show{Confirm,Prompt}Dialog or showToast." "$hits"

# 8. Frontend JS test suite, including the CHRONIC-XSS escaping-enforcement gate
#    (js/tests/run.js). GO has no CI and running node inside `cargo build` is an
#    anti-pattern, so the gate lives here in the lint script that's run before a
#    commit — wiring it in makes the "build-failing gate" literal rather than a
#    test you have to remember to invoke. run.js exits non-zero on any failure.
if command -v node >/dev/null 2>&1; then
    if ! js_out=$(node "$SRC_JS/tests/run.js" 2>&1); then
        echo
        echo "[frontend-js-tests] JS test suite failed (includes the CHRONIC-XSS escaping gate):"
        echo "$js_out" | tail -40
        violations=$((violations + 1))
    fi
else
    echo "[frontend-js-tests] WARNING: node not found — skipping JS tests and the XSS gate"
fi

# 9. Inline event-handler ratchet (CSP `unsafe-inline` drawdown).
#    Dropping `script-src 'unsafe-inline'` from tauri.conf.json requires
#    converting every inline `on<event>="..."` handler to addEventListener — CSP
#    has no nonce for inline handlers. That's a phased migration; this gate stops
#    BACKSLIDING in the meantime: the occurrence count may only go DOWN. When you
#    remove handlers, lower INLINE_HANDLER_BUDGET to the new count (the script
#    prints the new number on progress). When it reaches 0 and the two inline
#    <script> blocks (index.html, compose.html) are externalized, drop
#    'unsafe-inline' from the CSP. Tests and minified bundles are excluded.
INLINE_HANDLER_BUDGET=389
inline_count=$(grep -rlE "\bon[a-z]+=[\"']" "$FRONTEND" --include='*.html' --include='*.js' 2>/dev/null \
    | grep -v '/tests/' | grep -v '\.min\.' \
    | tr '\n' '\0' | xargs -0 grep -oE "\bon[a-z]+=[\"']" 2>/dev/null | wc -l | tr -d ' ')
if [ "$inline_count" -gt "$INLINE_HANDLER_BUDGET" ]; then
    report "inline-handler-ratchet" \
        "Inline event handlers rose to $inline_count (budget $INLINE_HANDLER_BUDGET). Don't add inline on*= attributes; wire events with addEventListener (data-* + delegation)." \
        "count $inline_count > budget $INLINE_HANDLER_BUDGET"
elif [ "$inline_count" -lt "$INLINE_HANDLER_BUDGET" ]; then
    echo "[inline-handler-ratchet] progress: $inline_count inline handlers (budget $INLINE_HANDLER_BUDGET)."
    echo "  Lower INLINE_HANDLER_BUDGET in scripts/lint-frontend.sh to $inline_count to lock in the gain."
fi

if [ $violations -eq 0 ]; then
    echo "frontend lint: clean"
    exit 0
else
    echo
    echo "frontend lint: $violations rule(s) failed"
    exit 1
fi
