#!/usr/bin/env python3
"""
Generate hs-behavioral.spec.js from upstream _hyperscript test data.

Reads spec/tests/hyperscript-upstream-tests.json and produces a data-driven
Playwright test file that runs each test in the WASM sandbox.

Usage: python3 tests/playwright/generate-hs-tests.py
"""
import json
import re
import os

PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
INPUT = os.path.join(PROJECT_ROOT, 'spec/tests/hyperscript-upstream-tests.json')
OUTPUT = os.path.join(PROJECT_ROOT, 'tests/playwright/hs-behavioral-data.js')

with open(INPUT) as f:
    raw_tests = json.load(f)

def normalize_html(html):
    """Clean up HTML for our harness — ensure IDs exist for element targeting."""
    # Remove | separators (upstream convention for multi-element make())
    html = html.replace(' | ', '')
    # If no id in the HTML, add id="el" to first element
    if ' id=' not in html and ' id =' not in html:
        html = re.sub(r'^<(\w+)', r'<\1 id="el"', html, count=1)
    return html

def normalize_action(action, html):
    """Convert upstream action to work with our byId/qs helpers."""
    if not action or action == '(see body)':
        return ''

    # Replace element variable references with DOM lookups
    # Common pattern: div.click(), form.click(), d1.click(), etc.

    # First handle ID-based: d1.click() -> byId("d1").click()
    action = re.sub(r'\b([a-z]\d+)\.', lambda m: f'byId("{m.group(1)}").', action)

    # div1.something -> byId("div1") if there's an id, else qs("div")
    action = re.sub(r'\bdiv1\.', 'byId("div1") && byId("div1").', action)
    action = re.sub(r'\bdiv2\.', 'byId("div2") && byId("div2").', action)

    # Generic tag.action: div.click() -> qs("div").click()
    for tag in ['div', 'form', 'button', 'input', 'span', 'p', 'a', 'section']:
        action = re.sub(rf'\b{tag}\.', f'qs("{tag}").', action)

    # Handle document.getElementById patterns
    action = action.replace('document.getElementById', 'byId')

    return action

def parse_checks(check, html):
    """Convert Chai-style assertions to {expr, op, expected} tuples.

    Upstream tests often have pre-action AND post-action assertions joined by &&.
    Since we run checks only AFTER the action, we keep only the LAST assertion
    for each expression (which represents the post-action expected state).
    """
    if not check or check == '(no explicit assertion)':
        return []

    all_checks = []
    # Split on ' && ' to handle multiple assertions
    parts = check.split(' && ')

    for part in parts:
        part = part.strip()
        if not part:
            continue

        # Pattern: something.should.equal(value)
        m = re.match(r'(.+?)\.should\.equal\((.+?)\)$', part)
        if m:
            expr, expected = m.group(1).strip(), m.group(2).strip()
            expr = normalize_expr(expr)
            all_checks.append({'expr': expr, 'op': '==', 'expected': expected})
            continue

        # Pattern: should.equal(null, something)
        m = re.match(r'should\.equal\(null,\s*(.+?)\)', part)
        if m:
            expr = normalize_expr(m.group(1).strip())
            all_checks.append({'expr': expr, 'op': '==', 'expected': 'null'})
            continue

        # Pattern: assert.isNull(expr)
        m = re.match(r'assert\.isNull\((.+?)\)', part)
        if m:
            expr = normalize_expr(m.group(1).strip())
            all_checks.append({'expr': expr, 'op': '==', 'expected': 'null'})
            continue

        # Pattern: assert.isNotNull(expr)
        m = re.match(r'assert\.isNotNull\((.+?)\)', part)
        if m:
            expr = normalize_expr(m.group(1).strip())
            all_checks.append({'expr': expr, 'op': '!=', 'expected': 'null'})
            continue

        # Pattern: something.should.deep.equal(value)
        m = re.match(r'(.+?)\.should\.deep\.equal\((.+?)\)$', part)
        if m:
            expr, expected = m.group(1).strip(), m.group(2).strip()
            expr = normalize_expr(expr)
            all_checks.append({'expr': expr, 'op': 'deep==', 'expected': expected})
            continue

    # Deduplicate: keep only the LAST check for each expression
    # (upstream pattern: first check = pre-action state, last = post-action state)
    seen = {}
    for c in all_checks:
        seen[c['expr']] = c
    return list(seen.values())

def normalize_expr(expr):
    """Normalize element references in assertion expressions."""
    # ID-based: d1.innerHTML -> byId("d1").innerHTML
    expr = re.sub(r'\b([a-z]\d+)\.', lambda m: f'byId("{m.group(1)}").', expr)
    expr = re.sub(r'\bdiv1\.', 'byId("div1").', expr)
    expr = re.sub(r'\bdiv2\.', 'byId("div2").', expr)
    expr = re.sub(r'\bdiv3\.', 'byId("div3").', expr)

    # Bare variable names that are IDs: bar.classList -> byId("bar").classList
    # Match word.property where word is not a known tag or JS global
    known_tags = {'div', 'form', 'button', 'input', 'span', 'p', 'a', 'section'}
    known_globals = {'document', 'window', 'Math', 'JSON', 'console', 'byId', 'qs', 'qsa'}

    def replace_bare_var(m):
        name = m.group(1)
        prop = m.group(2)
        if name in known_tags or name in known_globals:
            return m.group(0)
        return f'byId("{name}").{prop}'

    expr = re.sub(r'\b([a-z][a-zA-Z]*)\.(classList|innerHTML|textContent|style|parentElement|getAttribute|hasAttribute|children|firstChild|value|dataset|className|outerHTML)', replace_bare_var, expr)

    # Tag-based: div.classList -> qs("div").classList
    for tag in known_tags:
        expr = re.sub(rf'\b{tag}\.', f'qs("{tag}").', expr)

    # getComputedStyle(div) -> getComputedStyle(qs("div"))
    for tag in known_tags:
        expr = expr.replace(f'getComputedStyle({tag})', f'getComputedStyle(qs("{tag}"))')

    # window.results -> window.results (OK as-is)
    # Remove any double dots from prior replacements
    expr = expr.replace('..', '.')

    return expr

# Process tests
output_tests = []
skipped = 0

for t in raw_tests:
    if t.get('complexity') != 'simple':
        skipped += 1
        continue

    html = normalize_html(t['html'])
    action = normalize_action(t['action'], html)
    checks = parse_checks(t['check'], html)

    # Skip tests with no usable checks (log tests etc)
    if not checks and not action:
        skipped += 1
        continue

    # Skip tests with syntax that causes parser hangs
    hang_patterns = ['[@', '{color', '{font', '{display', '{opacity',
                     '${', 'transition ', 'as ', 'js(', 'make a', 'measure',
                     'fetch ', '\\\\']
    if any(p in html for p in hang_patterns):
        skipped += 1
        continue

    output_tests.append({
        'category': t['category'],
        'name': t['name'],
        'html': html,
        'action': action,
        'checks': checks,
        'async': t.get('async', False),
    })

# Write JS module
with open(OUTPUT, 'w') as f:
    f.write('// Auto-generated from _hyperscript upstream test suite\n')
    f.write('// Source: spec/tests/hyperscript-upstream-tests.json\n')
    f.write(f'// {len(output_tests)} tests ({skipped} skipped)\n')
    f.write('//\n')
    f.write('// DO NOT EDIT — regenerate with: python3 tests/playwright/generate-hs-tests.py\n\n')
    f.write('module.exports = ')
    f.write(json.dumps(output_tests, indent=2))
    f.write(';\n')

print(f'Generated {len(output_tests)} tests ({skipped} skipped) -> {OUTPUT}')

# Category breakdown
from collections import Counter
cats = Counter(t['category'] for t in output_tests)
for cat, n in cats.most_common():
    print(f'  {cat}: {n}')
