"""Guards for ../widget.html: its data block, what it may show, and that its bucket counts like keyproxy.py."""
import json
import re
import subprocess

import embed_widget
from embed_widget import WIDGET, embed
from scenario import DEMO_KEY, bill, stranger_outcomes

HTML = WIDGET.read_text(encoding="utf-8")
SNIPPETS = json.loads(re.search(r'<script type="application/json" id="akb-text">(.*?)</script>', HTML, re.S).group(1))

# Reads the bucket and bill code out of the widget between its marker comments, runs it, and compares with Python.
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="akb-config">([\s\S]*?)<\/script>/)[1]);
const js = html.slice(html.indexOf('/* ---------- the bucket and the stranger'), html.indexOf('/* ---------- state'));
const api = new Function('cfg', "'use strict';" + js + '; return {outcomes, dollars};')(cfg);
const bad = [];
fx.runs.forEach((r) => {
  if (JSON.stringify(api.outcomes(r.limited, r.addresses)) !== JSON.stringify(r.outcomes)) bad.push(['outcomes', r.limited, r.addresses]);
});
fx.money.forEach(([n, s]) => { if (api.dollars(n) !== s) bad.push(['dollars', n, api.dollars(n), s]); });
console.log(JSON.stringify({ runs: fx.runs.length, money: fx.money.length, bad: bad.slice(0, 5) }));
process.exit(bad.length ? 1 : 0);
"""


def money(accepted):
    text = f"{bill(accepted):.3f}"
    return "$" + (text[:-1] if text.endswith("0") else text)


def test_widget_data_block_matches_scenario():
    """Guards a scenario.py edit that was not copied into the widget (run embed_widget.py)."""
    assert embed(HTML) == HTML


def test_every_element_the_script_looks_up_exists_inside_the_root():
    """Guards a widget that never starts: a lookup of an id that is not in the page throws in the browser."""
    root = HTML[HTML.index('<div class="akb-root">'):HTML.index("<script")]
    lookups = set(re.findall(r"\$\('([\w-]+)'\)", HTML[HTML.index("<script>"):]))
    assert lookups and all(f'id="{i}"' in root for i in lookups), sorted(i for i in lookups if f'id="{i}"' not in root)


def test_widget_is_small_and_asks_for_nothing_online():
    """The shown code samples mention fetch() and a URL, so look at the page without its two data blocks."""
    assert len(HTML.encode("utf-8")) < 40 * 1024
    page = re.sub(r'<script type="application/json" id="akb-(config|text)">.*?</script>', "", HTML, flags=re.S)
    for banned in ("http://", "https://", "fetch(", "XMLHttpRequest", "WebSocket", "sendBeacon", "import("):
        assert banned not in page, banned
    assert " src=" not in page and "@import" not in page
    assert set(re.findall(r"https?://[a-z0-9./-]+", HTML)) == {"https://api.example.com/v1/chat"}


def test_widget_has_no_form_element():
    """Guards a dead Send button: the lesson page loads widget.html in a sandboxed frame, where a form cannot submit."""
    assert "<form" not in HTML and 'type="submit"' not in HTML


def test_the_key_is_shown_only_where_it_is_really_in_the_page():
    """Guards the lesson's central picture: with the key on the server, no panel text may contain it."""
    assert "EXAMPLE" in DEMO_KEY and "demo" in DEMO_KEY
    assert {name for name, text in SNIPPETS.items() if "{KEY}" in text} == {"sourcePage", "networkPage"}
    assert not [text for text in SNIPPETS.values() if DEMO_KEY in text]  # the key itself comes only from the config block


def test_widget_bucket_and_bill_match_the_python_code(tmp_path):
    """Guards the widget's JavaScript drifting from keyproxy.py: its counts must be the lesson's."""
    runs = [{"limited": limited, "addresses": n, "outcomes": stranger_outcomes(limited, n)}
            for limited, n in [(False, 1), (True, 1), (True, 2), (True, 3), (True, 7), (True, 20), (True, 100)]]
    fixture = {"runs": runs, "money": [[n, money(n)] for n in range(1001)]}
    (tmp_path / "fixture.json").write_text(json.dumps(fixture), encoding="utf-8")
    (tmp_path / "parity.mjs").write_text(PARITY, encoding="utf-8")
    run = subprocess.run(["node", str(tmp_path / "parity.mjs"), str(tmp_path / "fixture.json"), str(WIDGET)],
                         capture_output=True, text=True, timeout=60)
    assert run.returncode == 0, run.stdout + run.stderr
    assert json.loads(run.stdout) == {"runs": 7, "money": 1001, "bad": []}
    assert sum(runs[1]["outcomes"]) == 14 and sum(runs[0]["outcomes"]) == 1000  # the numbers the lesson quotes
