"""Guards for ../widget.html: its data block, that its server gives the same numbers and words as settings_calc.py, and what a reader sees and can press."""
import json
import re
import subprocess

import settings_calc as sc
from embed_widget import CONFIG_BLOCK, WIDGET, config, embed

HTML = WIDGET.read_text(encoding="utf-8")
FEW = 40                                                     # Python's side of the parity run uses this many chats, to stay fast; the page itself uses all 600
GRID = [(share, longest, cap, reserved) for share in (30, 35, 43, 44, 50, 70, 90, 98) for longest in sc.LONGEST for cap in (8, 64, 512) for reserved in (False, True)]

# Reads the model out of the widget between its marker comments, runs every setting with Node and compares the memory parts, the three lines of words and
# the legend with the ones Python wrote into the fixture, character for character.
PARITY = r"""
import fs from 'node:fs';
const [fixture, widget] = process.argv.slice(2);
const fx = JSON.parse(fs.readFileSync(fixture, 'utf8'));
const html = fs.readFileSync(widget, 'utf8');
const cfg = JSON.parse(html.match(/<script type="application\/json" id="sw-config">([\s\S]*?)<\/script>/)[1]);
const js = html.slice(html.indexOf('/* ---------- the model ('), html.indexOf('/* ---------- display'));
const api = new Function('cfg', "'use strict';" + js + '; return {describe};')(cfg);
const reqs = cfg.prompts.slice(0, fx.n).map((p, i) => ({ prompt: p, reply: cfg.replies[i] }));
const bad = [];
fx.cases.forEach((c) => {
  const got = api.describe(c.share, c.longest, c.cap, c.reserved, reqs);
  if (JSON.stringify(got) !== JSON.stringify(c.want)) bad.push([c.share, c.longest, c.cap, c.reserved, JSON.stringify(got).slice(0, 300), JSON.stringify(c.want).slice(0, 300)]);
});
console.log(JSON.stringify({ checks: fx.cases.length, bad: bad.slice(0, 3) }));
process.exit(bad.length ? 1 : 0);
"""

# Runs the page script against a small stand-in for the DOM, moves the sliders, ticks the box and reads back what a reader sees.
PAGE = r"""
import fs from 'node:fs';
const [widget, fixture] = process.argv.slice(2);
const html = fs.readFileSync(widget, 'utf8');
const fx = JSON.parse(fs.readFileSync(fixture, 'utf8'));
const config = html.match(/<script type="application\/json" id="sw-config">([\s\S]*?)<\/script>/)[1];
const code = html.match(/<script>([\s\S]*?)<\/script>/)[1];
function el(tag) {
  const e = { tag, children: [], listeners: {}, value: '', checked: false, style: {}, attrs: {}, textContent: '' };
  e.setAttribute = (k, v) => { e.attrs[k] = v; };
  e.getAttribute = (k) => e.attrs[k];
  e.addEventListener = (type, f) => (e.listeners[type] = e.listeners[type] || []).push(f);
  return e;
}
const page = { root: el('html'), messages: [], parent: { other: true } };
[...html.matchAll(/id="(sw-[^"]+)"/g)].forEach((m) => { page[m[1]] = el('x'); });
page['sw-config'].textContent = config;
const doc = { getElementById: (id) => page[id], documentElement: page.root };
const win = { addEventListener: (type, f) => page.messages.push(f) };
new Function('document', 'window', 'parent', code)(doc, win, page.parent);
const fire = (e, type) => (e.listeners[type] || []).forEach((f) => f({ target: e }));
const bad = [];
const want = (what, got, expected) => { if (JSON.stringify(got) !== JSON.stringify(expected)) bad.push([what, JSON.stringify(got).slice(0, 200), JSON.stringify(expected).slice(0, 200)]); };
const shown = () => {
  const width = [0, 1, 2, 3].reduce((sum, k) => sum + parseFloat(page['sw-seg-' + k].style.width), 0);       // the four parts fill the bar, no more and no less
  if (Math.abs(width - 100) > 1e-6) bad.push(['bar widths add to 100', String(width), '100']);
  return { grow: [0, 1, 2, 3].map((k) => Number(page['sw-seg-' + k].attrs['data-bytes'])), fill: Number(page['sw-fill'].style.width.replace('%', '')), room: page['sw-room'].textContent,
    run: page['sw-run'].textContent, extra: page['sw-extra'].textContent, legend: [0, 1, 2, 3].map((k) => page['sw-leg-' + k].textContent) };
};
const cfg = JSON.parse(config);
want('opening controls', [page['sw-share'].value, page['sw-longest'].value, page['sw-cap'].value, page['sw-reserved'].checked, page['sw-longest'].max, page['sw-cap'].max],
  [String(cfg.opening.share), String(cfg.longest.indexOf(cfg.opening.longest)), String(cfg.caps.indexOf(cfg.opening.cap)), false, String(cfg.longest.length - 1), String(cfg.caps.length - 1)]);
want('opening state', shown(), fx.opening);
want('nothing spoken yet', page['sw-live'].textContent, '');
page['sw-cap'].value = String(cfg.caps.indexOf(256));
fire(page['sw-cap'], 'input');
want('cap 256, dragging', shown(), fx.cap256);
want('dragging stays silent', page['sw-live'].textContent, '');
fire(page['sw-cap'], 'change');
want('release speaks', page['sw-live'].textContent, [fx.cap256.room, fx.cap256.run, fx.cap256.extra].join(' '));
want('cap words', [page['sw-cap-out'].textContent, page['sw-cap'].attrs['aria-valuetext']], ['256', '256 chats']);
page['sw-share'].value = '40'; page['sw-longest'].value = String(cfg.longest.length - 1);
fire(page['sw-share'], 'input');
want('will not start', shown(), fx.nostart);
want('share and longest words', [page['sw-share-out'].textContent, page['sw-longest-out'].textContent, page['sw-share'].attrs['aria-valuetext'], page['sw-longest'].attrs['aria-valuetext']],
  ['40%', '128,000 tokens', '40% of the card', '128,000 tokens']);
fx.full.forEach((c) => {                                                                                       // pools that fill up, with all 600 chats
  page['sw-share'].value = String(c.share); page['sw-longest'].value = String(cfg.longest.indexOf(c.longest)); page['sw-cap'].value = String(cfg.caps.indexOf(c.cap));
  fire(page['sw-share'], 'input');
  want('pool full ' + [c.share, c.longest, c.cap].join('/'), shown(), c.want);
});
page['sw-share'].value = '90'; page['sw-longest'].value = String(cfg.longest.indexOf(8000)); page['sw-cap'].value = String(cfg.caps.indexOf(64)); page['sw-reserved'].checked = true;
fire(page['sw-reserved'], 'input');
want('reserved', shown(), fx.reserved);
const theme = (source, name) => page.messages.forEach((f) => f({ data: { type: 'spicyshark:theme', theme: name }, source }));
theme({ other: 'frame' }, 'dark');
want('theme from another sender is ignored', page.root.attrs['data-theme'], undefined);
theme(page.parent, 'dark');
want('theme from the host page', page.root.attrs['data-theme'], 'dark');
console.log(JSON.stringify({ bad: bad.slice(0, 3) }));
process.exit(bad.length ? 1 : 0);
"""


def run_node(tmp_path, script, *args):
    path = tmp_path / "check.mjs"
    path.write_text(script, encoding="utf-8")
    done = subprocess.run(["node", str(path), *args], capture_output=True, text=True, timeout=120)
    assert done.returncode == 0, done.stdout + done.stderr
    return json.loads(done.stdout)


def test_widget_data_block_matches_the_python_module():
    assert embed(HTML) == HTML
    block = json.loads(CONFIG_BLOCK.search(HTML).group(2))
    assert block == json.loads(json.dumps(config()))
    assert block["opening"] == {"share": 90, "longest": 8000, "cap": 64, "reserved": False}
    assert re.search(r'id="sw-share" min="%d" max="%d"' % (min(sc.SHARES), max(sc.SHARES)), HTML)           # the slider covers the shares the tests cover


def test_widget_gives_the_same_numbers_and_words_as_python(tmp_path):
    few = sc.backlog()[:FEW]
    fixture = {"n": FEW, "cases": [{"share": a, "longest": b, "cap": c, "reserved": d, "want": sc.describe(a, b, c, d, few)} for a, b, c, d in GRID]}
    assert any("paused" in case["want"]["extra"] for case in fixture["cases"]) and any("turned away" in case["want"]["extra"] for case in fixture["cases"])
    assert any("will not start" in case["want"]["room"] for case in fixture["cases"])                          # the grid reaches pauses, cut-off chats and a server that will not start
    (tmp_path / "fx.json").write_text(json.dumps(fixture), encoding="utf-8")
    assert run_node(tmp_path, PARITY, str(tmp_path / "fx.json"), str(WIDGET))["checks"] == len(GRID)


def test_widget_page_shows_the_opening_state_and_follows_the_controls(tmp_path):
    state = lambda *a: sc.describe(*a)
    fixture = {"opening": state(90, 8000, 64), "cap256": state(90, 8000, 256), "nostart": state(40, 128_000, 256), "reserved": state(90, 8000, 64, True),
               "full": [{"share": a, "longest": b, "cap": c, "want": state(a, b, c)} for a, b, c in ((35, 8000, 64), (50, 8000, 128), (30, 8000, 64))]}
    assert all("paused" in case["want"]["extra"] for case in fixture["full"])                                    # states where the pool fills and chats pause
    assert fixture["opening"]["room"] == "Room for 205 chats of 2,000 tokens at once. 51 if every chat ran to 8,000."
    (tmp_path / "fx.json").write_text(json.dumps(fixture), encoding="utf-8")
    run_node(tmp_path, PAGE, str(WIDGET), str(tmp_path / "fx.json"))


def test_widget_is_small_themed_and_uses_only_the_site_palette():
    body = HTML[HTML.index('<div class="sw-root">'):HTML.index("<script")]
    visible = re.sub(r"<details.*?</details>", "", body, flags=re.S)
    assert len(re.findall(r"<input|<select|<button", visible)) <= 5 and "More settings" in body       # at most five controls on show; the rest sit under More settings
    assert len(HTML.splitlines()) < 300 and "light-dark(#ffffff, #0a0f17)" in HTML and "spicyshark:theme" in HTML and "prefers-reduced-motion" in HTML
    assert not re.search(r"0072B2|56B4E9|D55E00|https?://|<img|@import", HTML, re.I)
