"""Guards for rent_vs_own.py: the tokens a month, both bills, the GPU count, the busy share and the break-even, each checked against a sum made by hand or an
independent route."""
import math
import re
import subprocess
import sys
import textwrap
from decimal import Decimal
from pathlib import Path

import pytest

import make_figs
import rent_vs_own as r
from rent_vs_own import cp

CODE = Path(__file__).resolve().parent
LESSON = CODE.parent
WEB_ADDRESS = re.compile("ht" + r"tps?://")             # written in two pieces so that this file holds none either


def test_a_month_of_tokens_becomes_replies_and_the_rent_is_the_per_token_bill():
    assert (r.TOKENS_IN, r.TOKENS_OUT, r.TOKENS_PER_REPLY) == (1882, 157, 2039)                         # lesson 29's usual chats, to the whole token
    assert r.replies_for(300 * 10**9) == 147_130_947 and r.replies_for(0) == 0 and r.replies_for(1019) == 0 and r.replies_for(1020) == 1
    for replies in (1, 1000, 147_130_947):
        for price_in, price_out in ((Decimal("0.10"), Decimal("0.50")), (Decimal(2), Decimal(10))):
            by_hand = (replies * 1882 * price_in + replies * 157 * price_out) / 1_000_000                 # tokens times price, per million
            assert r.monthly_rent(replies, price_in, price_out) == by_hand
    assert r.dollars(r.monthly_rent(147_130_947)) == 39_240 and r.monthly_rent(0) == 0


def test_the_gpu_count_is_lessons_29_plan_plus_the_spare():
    month = r.SECONDS_PER_MONTH
    assert r.gpus_to_pay_for(20 * month, 24) == cp.plan(1000, 60, 120, r.MIX, r.TARGET_MS)["n"] + 1 == 5          # lesson 29's 1,000 users at the busiest hour's pace all day
    for hours in (24, 12, 4):                                                    # one GPU carries 80% of what it finishes, and a month of traffic hours is 30 * hours * 3600 s
        carry = 0.8 * r.REPLIES_PER_GPU_SECOND * 30 * hours * 3600
        for n in (1, 2, 5, 9):
            assert r.gpus_to_pay_for(math.floor(n * carry), hours) == n + 1 and r.gpus_to_pay_for(math.floor(n * carry) + 2, hours) == n + 2
            assert r.gpus_to_pay_for(math.floor(n * carry), hours, spare=0) == n


def test_the_owning_bill_is_hours_times_price_plus_people_and_the_busy_share_counts_every_paid_gpu():
    assert r.monthly_own(12) == 12 * 720 * 3 + 10_000 == 35_920 and r.monthly_own(1, Decimal("2.5"), 0) == 1800
    for n in (1, 3, 12):                       # n planned GPUs at their planned load, plus the spare, are busy 80% * n / (n + spare) of the month
        carry = 0.8 * r.REPLIES_PER_GPU_SECOND * r.SECONDS_PER_MONTH
        assert r.busy_share(n * carry, n + 1) == pytest.approx(0.8 * n / (n + 1))
    assert r.busy_share(0, 5) == 0 and r.busy_share(r.REPLIES_PER_GPU_SECOND * r.SECONDS_PER_MONTH, 1) == pytest.approx(1)
    now = r.case(300 * 10**9, 24)
    assert (now["gpus"], r.dollars(now["rent"]), r.dollars(now["own"]), round(now["busy"], 3)) == (12, 39_240, 35_920, 0.677)
    assert now["own_per_million"] == now["own"] / 300_000 and now["rent_per_million"] == now["rent"] / 300_000


SETTINGS = [dict(hours=h, people=p, price_in=pi, price_out=po) for h in (24, 18) for p in (0, 10_000, 20_000) for pi, po in ((Decimal("0.10"), Decimal("0.50")), (Decimal(2), Decimal(10)))]


def test_break_even_is_the_first_token_count_where_owning_costs_no_more_than_renting():
    def cheaper_to_own(replies, s):
        return r.monthly_own(r.gpus_to_pay_for(replies, s["hours"]), people=s["people"]) <= r.monthly_rent(replies, s["price_in"], s["price_out"])
    for s in SETTINGS:
        found = r.break_even(s["hours"], s["price_in"], s["price_out"], people=s["people"])
        top = math.ceil(found["replies"])
        assert cheaper_to_own(top, s) and found["gpus"] == r.gpus_to_pay_for(top, s["hours"])                        # at the crossing owning is no dearer, with these GPUs
        assert not cheaper_to_own(top - 3, s) or found["replies"] < 3                                                # and a hair below it, renting is cheaper
        assert not any(cheaper_to_own(math.ceil(top * k / 400), s) for k in range(1, 400)), s                        # nowhere below: no token count there favours owning
    assert r.break_even(4) is None and r.break_even(13) is None and r.break_even(14)["gpus"] == 132               # a load too peaky for its GPUs never crosses


def test_people_cost_moves_the_break_even_by_the_formula_the_lesson_gives():
    seen = [r.break_even(24, people=p)["tokens"] for p in (0, 10_000, 20_000)]
    assert seen[0] < seen[1] < seen[2] and r.dollars(r.break_even(24)["tokens"] / 1e9) == 225
    per_reply, gpu_month, carry = r.rates(24)
    for p, exact in zip((0, 10_000, 20_000), seen):                    # (people + spare * GPU month) / (rent per reply - GPU month / carry), in tokens: within one GPU's worth
        smooth = (p + r.SPARE * gpu_month) / (per_reply - gpu_month / carry) * r.TOKENS_PER_REPLY
        assert abs(smooth - exact) <= carry * r.TOKENS_PER_REPLY, (p, smooth, exact)


def lesson_text() -> str:
    return (LESSON / "lesson.md").read_text(encoding="utf-8")


def test_each_file_and_line_the_lesson_cites_holds_the_code_it_quotes():
    prose = lesson_text()
    cites = re.findall(r"`([\w]+\.py):(\d+)`<br>`(.+?)` \|", prose)
    assert [int(line) for _, line, _ in cites] == [68, 74, 78, 82, 86, 107]                                  # the six steps, one row each
    for name, line, quoted in cites:
        assert (CODE / name).read_text(encoding="utf-8").splitlines()[int(line) - 1].strip() == quoted, (name, line)
    file_lines = (CODE / "rent_vs_own.py").read_text(encoding="utf-8").splitlines()
    assert file_lines[104].strip() == "for n in range(1, MAX_GPUS + 1):" and "The `for` loop on line 105" in prose           # the row for step 6 sends the reader to line 105
    excerpt = textwrap.dedent("\n".join(file_lines[104:108]))
    assert f"```python\n{excerpt}\n```" in prose                                                           # and Go deeper shows those four lines as they are


def test_the_trace_prints_the_runs_block_and_the_one_line_edit_gives_the_hidden_answer():
    prose = lesson_text()
    blocks = re.findall(r"```text\n(.*?)```", prose, re.S)
    assert len(blocks) == 2
    source = (CODE / "trace.py").read_text(encoding="utf-8")
    assert source.splitlines()[9].startswith("HOURS = 24 ") and "change line 10 to `HOURS = 12`" in prose              # the Your turn line is the line it names
    def run(code):
        return subprocess.run([sys.executable, "-c", code], cwd=CODE, capture_output=True, text=True, check=True, env={"PYTHONDONTWRITEBYTECODE": "1"}).stdout
    assert run(source) == blocks[0]
    assert run(source.replace("HOURS = 24 ", "HOURS = 12 ", 1)).startswith(blocks[1])                         # the top of the output, as the hidden answer shows it


def test_every_number_in_the_lesson_and_figures_comes_from_the_code():
    """The prose, the trace and the four figures quote numbers by hand; a change to a constant or to lesson 29's planner must show up here, not in a reader's check."""
    prose, figures = lesson_text(), {path.name: path.read_text(encoding="utf-8") for path in sorted((LESSON / "figs").glob("*.svg"))}
    assert set(figures) == set(re.findall(r"\]\(figs/([\w-]+\.svg)\)", prose)) == set(make_figs.FIGURES) and len(figures) == 4
    for name, draw in make_figs.FIGURES.items():                                                    # the files on disk are what make_figs.py draws now
        assert figures[name] == draw(), name
    D = lambda x: "$" + r.commas(r.dollars(x))
    B, P = r.tokens_words, r.percent
    W = lambda tokens: B(tokens).removesuffix(" billion")                                          # "225" from "225 billion"
    T = 300 * 10**9
    per_reply, gpu_month, carry = r.rates(24)
    now, x24, c12, c4 = r.case(T, 24), r.break_even(24), r.case(T, 12), r.case(T, 4)
    rent_pm, own_pm = float(now["rent_per_million"]), float(now["own_per_million"])
    by = {h: (r.case(T, h), r.break_even(h)) for h in (24, 18, 16, 12, 8, 4)}
    people = {p: r.break_even(24, people=p) for p in (0, 5_000, 10_000, 20_000, 30_000)}
    spare = {s: r.break_even(24, spare=s) for s in (0, 1, 2)}
    gpu_hour = {g: r.break_even(24, gpu_hour=Decimal(g)) for g in ("2.0", "4.0")}
    dear = r.break_even(24, Decimal(2), Decimal(10))
    flat_out = r.REPLIES_PER_GPU_SECOND * r.SECONDS_PER_MONTH
    smooth = (r.PEOPLE + r.SPARE * gpu_month) / (per_reply - gpu_month / carry)
    floor = gpu_month / (flat_out * per_reply)                                                     # busy share below which a GPU costs more than the rent for its replies
    busy_cross = now["busy"] * own_pm / rent_pm                                                    # the 12 GPUs' busy share where owning per million tokens equals renting
    shifts = [(people[b]["tokens"] - people[a]["tokens"]) / 1e9 for a, b in ((0, 10_000), (10_000, 20_000), (20_000, 30_000))]     # tokens moved per $10,000 of people, in billions
    nearest5 = lambda x: round(x / 5) * 5
    assert [nearest5(x) for x in shifts[:2]] == [175, 175] and nearest5(min(shifts)) == 160 and nearest5(max(shifts)) == 175           # "about 175" to $20,000, "about 160 to 175" over the table
    assert r.break_even(13) is None and r.break_even(24, Decimal("0.05"), Decimal("0.25")) is None                                   # "13 hours or fewer", "half the price"
    assert 0.69 <= float(r.GPU_HOUR) <= 6.99 and 0.10 <= float(r.PRICE_IN) <= 15 and 0.50 <= float(r.PRICE_OUT) <= 75 and r.TOKENS_IN <= 100_000      # the toy sits inside the pages' ranges (read 2026-10-20)
    dearer_again = [t for t in (x24["tokens"] + k * 5e7 for k in range(1, 10_001)) if (c := r.case(round(t), 24))["own"] > c["rent"]]     # after the first crossing, in steps of 0.05 billion up to 500 billion past it
    ceiling = r.busy_share((now["gpus"] - r.SPARE) * carry, now["gpus"])                            # the 11 planned GPUs at their planned load, over the 12 paid for
    q5, b14, rate12 = 20 * r.SECONDS_PER_MONTH, r.break_even(14), c12["replies"] / (r.DAYS * 12 * 3600)
    g5, carry12 = r.gpus_to_pay_for(q5, 24), r.rates(12)[2]
    millionths = lambda: f"{r.TOKENS_IN * r.PRICE_IN:.1f} + {r.TOKENS_OUT * r.PRICE_OUT:.1f}"
    wanted = [
        f"{B(T)} tokens a month cost {D(now['rent'])} to rent and {D(now['own'])} to run on {now['gpus']} GPUs, because the GPUs are busy {P(now['busy'])} of the time",
        f"owning costs {D(c12['own'])} against the same {D(now['rent'])}", f"{B(x24['tokens'])} in the first case",
        f"Renting costs {D(now['rent'])}. Owning costs {D(now['own'])}, with {now['gpus']} GPUs busy {P(now['busy'])} of the time. The lines cross at {B(x24['tokens'])} tokens a month.",
        f"Owning needs {c4['gpus']} GPUs, costs {D(c4['own'])} and is busy {P(c4['busy'])} of the time",
        f"With $0 a month for people the lines cross at {B(people[0]['tokens'])} tokens a month. With $20,000 they cross at {W(people[20_000]['tokens'])} billion.",
        f"average {r.TOKENS_IN:,} tokens in and {r.TOKENS_OUT} out. At {B(T)} tokens a month that is {now['replies']:,} replies", f"one reply costs {per_reply * 1e6:.1f} millionths of a dollar",
        f"times {r.HOURS_PER_MONTH} hours ({r.DAYS} days of 24)", f"{r.TOKENS_PER_REPLY:,} tokens of one reply ({r.TOKENS_IN:,} in, {r.TOKENS_OUT} out)", f"over {r.DAYS} days of `hours` hours",
        f"{B(T)} tokens at {r.TOKENS_PER_REPLY:,} per reply are {now['replies']:,} replies",
        f"{r.TOKENS_IN:,} x {r.PRICE_IN} + {r.TOKENS_OUT} x {r.PRICE_OUT} = {millionths()} = {per_reply * 1e6:.1f} millionths of a dollar, which makes {D(now['rent'])} for the month",
        f"are {now['replies'] / (r.DAYS * 24 * 3600):.1f} a second. The plan needs {now['gpus'] - r.SPARE} GPUs for that, and the spare makes {now['gpus']}",
        f"{now['gpus']} x {r.HOURS_PER_MONTH} x ${r.GPU_HOUR} is {D(now['gpus'] * r.HOURS_PER_MONTH * r.GPU_HOUR)}, and the ${r.PEOPLE:,} for people makes {D(now['own'])}",
        f"The {now['gpus']} GPUs serve {now['busy']:.1%} of the replies they could finish. Per million tokens, owning costs ${own_pm:.4f} and renting ${rent_pm:.4f}",
        f"Owning first costs no more than renting at {B(x24['tokens'])} tokens a month, with {x24['gpus']} GPUs paid for there. At {B(T)}, owning is {D(now['rent'] - now['own'])} cheaper.",
        f"need {c12['gpus']} GPUs, busy {c12['busy']:.1%} of the time", "Each $10,000 more for people, up to $20,000, pushes the crossing out by about 175 billion tokens", "Each $10,000 moves the crossing about 160 to 175 billion tokens.",
        f"{rate12:.1f} replies a second", f"so the plan needs {c12['gpus'] - r.SPARE} GPUs and a spare: {c12['gpus']}, against {now['gpus']}. The bill is {D(c12['own'])} against {D(c12['rent'])} for renting, "
        f"and the GPUs are busy {c12['busy']:.1%} of the time", f"carries at most the rent of {D(per_reply * carry12)} a month in 12-hour days, and it costs {D(gpu_month)}",
        f"busy more than about {math.floor(floor * 100 + 0.5)}% of the month", f"Each ${10_000:,} a month for people moves the crossing out by about 160 to 175 billion tokens. A second spare moves it from {W(x24['tokens'])} to {W(spare[2]['tokens'])} billion.",
        f"At $4.00 for a GPU hour the crossing moves from {W(x24['tokens'])} to {W(gpu_hour['4.0']['tokens'])} billion tokens, and at $2.00 to {W(gpu_hour['2.0']['tokens'])} billion.",
        f"it is ${round(now['rent_per_million'] * 1000)} for each billion tokens", f"`K` = {round(carry):,} replies a month", f"which is {carry * r.TOKENS_PER_REPLY / 1e9:.1f} billion tokens",
        f"`c` = {D(gpu_month)} a month", f"(the ${r.PEOPLE:,} for people plus the spare's {D(gpu_month)})", f"`r` = ${per_reply:.7f} and `c / K` = ${gpu_month / carry:.7f}",
        f"`{D(r.PEOPLE + r.SPARE * gpu_month)[1:]} / {per_reply - gpu_month / carry:.7f}` = {smooth / 1e6:.1f} million replies, or {smooth * r.TOKENS_PER_REPLY / 1e9:.0f} billion tokens",
        f"The staircase gives {W(x24['tokens'])} billion, within one GPU's carry of {carry * r.TOKENS_PER_REPLY / 1e9:.1f} billion",
        f"Here owning costs more than renting again between {W(min(dearer_again))} and {W(max(dearer_again))} billion tokens a month. The code returns the first crossing.",
        f"Two lines over 0 to {B(1.25 * T)} tokens a month", f"starts at {D(r.monthly_own(1 + r.SPARE))} and steps up {D(gpu_month)} for each GPU added",
        f"They cross at {B(x24['tokens'])} tokens a month, with {x24['gpus']} GPUs paid for there. At {B(T)}, renting costs {D(now['rent'])} and owning {D(now['own'])}.",
        f"Rent costs ${rent_pm:.4f} per million tokens at every volume", f"owning is cheaper per million tokens above {P(busy_cross)} busy. At {P(now['busy'])} it is ${own_pm:.4f}.",
        f"a planned GPU runs at {P(r.HEADROOM)} of its limit, the spare lowers the top for these {now['gpus']} GPUs to {P(ceiling)}, and a GPU that gets traffic 12 hours of the day is busy at most {P(r.HEADROOM * 12 / 24)}", f"{now['gpus']} GPUs that costs {D(now['own'])} a month",
        f"passes under the rent price of ${rent_pm:.4f} at {P(busy_cross)} busy. At {P(now['busy'])} busy, the case of the lesson, owning is ${own_pm:.4f}. The curve ends at {P(ceiling)} busy, the most that {now['gpus'] - r.SPARE} planned GPUs and a spare can reach.",
        f"it finishes {flat_out / 1e6:.1f} million replies, worth {D(flat_out * per_reply)} at the rent price", f"{D(gpu_month)} / {D(flat_out * per_reply)} = {floor:.1%} of the month",
        f"must exceed {24 * gpu_month / (per_reply * carry):.1f}", f"At 14 hours the crossing is {B(b14['tokens'])} tokens a month, with {b14['gpus']} GPUs. At 13 hours or fewer it does not exist.",
        f"is {D(now['rent'])} in every row",
        *(f"| {h} | {c['gpus']} | {D(c['own'])} | {P(c['busy'])} | {W(x['tokens']) if x else 'none'} |" for h, (c, x) in by.items()),
        *(f"| ${p:,} | {B(x['tokens'])} | {x['gpus']} |" for p, x in people.items()),
        "Bars of what owning costs a month for the same " + f"{B(T)} tokens with all the traffic in 24, 18, 12, 8 and 4 hours a day: "
        + ", ".join(f"{by[h][0]['gpus']} GPUs and {D(by[h][0]['own'])}" for h in (24, 18, 12, 8, 4)) + f". A dashed line marks the rent, {D(now['rent'])}.",
        "by what the people cost a month: " + "; ".join(f"${p:,}, {B(x['tokens'])}" for p, x in people.items()),
        f"none gives {W(spare[0]['tokens'])} billion, one {W(x24['tokens'])} and two {W(spare[2]['tokens'])}.",
        f"A GPU hour at $2.00 gives {W(gpu_hour['2.0']['tokens'])} billion tokens and at $4.00 gives {W(gpu_hour['4.0']['tokens'])}.",
        f"Rent at $2 and $10 per million tokens gives {B(dear['tokens'])}, so at {B(T)} tokens renting costs {D(r.case(T, 24, price_in=Decimal(2), price_out=Decimal(10))['rent'])} and owning {D(now['own'])}.",
        f"at {B(T)} tokens renting costs {D(r.case(T, 24, price_in=Decimal('0.05'), price_out=Decimal('0.25'))['rent'])} and owning {D(now['own'])}.",
        f"The toy's ${r.PRICE_IN} and ${r.PRICE_OUT} equal the cheapest row", f"The toy's ${r.GPU_HOUR} sits inside that range",
        "from $0.10 to $15 for input and from $0.50 to $75 for output across its listed model rows, retired rows included", "which applies to prompts up to 100,000 tokens", f"the longest prompt in lesson 29's mix is about {round(max(r.cp.chats_of(r.MIX)['prompts']), -3):,} tokens", "Most rows cost far more",
        f"at $2 and $10 per million tokens the crossing falls to {B(dear['tokens'])} tokens a month", "a batch discount of 50% on both input and output tokens",
        "from $0.69 to $6.99 per GPU per hour, plus sales tax, VAT or GST where it applies", "at $2.79", "its newer models use a tokenizer that produces approximately 30% more tokens for the same text",
        "the page lists a batch price of $0.05 and $0.25 for prompts up to 100,000 tokens, and at those prices there is no crossing",                                           # the two price pages, read 2026-10-20
        f"{r.TOKENS_IN:,} x {r.PRICE_IN} = {r.TOKENS_IN * r.PRICE_IN:.1f} and {r.TOKENS_OUT} x {r.PRICE_OUT} = {r.TOKENS_OUT * r.PRICE_OUT:.1f} millionths of a dollar, which is "
        f"{per_reply * 1e6:.1f} millionths per reply. A million replies cost ${per_reply * 1e6:.2f}.",
        f"${r.GPU_HOUR} an hour, so {D(gpu_month)} for a {r.HOURS_PER_MONTH}-hour month", f"It finishes {r.REPLIES_PER_GPU_SECOND:.2f} replies a second at its limit ({flat_out / 1e6:.1f} million in a month)",
        f"{D(gpu_month)} / {per_reply:.7f} = {gpu_month / per_reply / 1e6:.1f} million replies. {gpu_month / per_reply / 1e6:.1f} / {flat_out / 1e6:.1f} = {gpu_month / per_reply / flat_out:.1%} of the month",
        f"owning cost {D(c12['own'])} and not {D(now['own'])}", f"to {rate12:.1f} replies a second, so the plan needs {c12['gpus'] - r.SPARE} GPUs and a spare: {c12['gpus']}, not {now['gpus']}",
        f"{c12['gpus']} x {r.HOURS_PER_MONTH} x ${r.GPU_HOUR} = {D(c12['gpus'] * r.HOURS_PER_MONTH * r.GPU_HOUR)}, plus ${r.PEOPLE:,} for people", f"the rent stays {D(c12['rent'])}",
        f"From {W(x24['tokens'])} to {W(people[20_000]['tokens'])} billion tokens a month, with {people[20_000]['gpus']} GPUs paid for there", "about 175 billion tokens for each $10,000",
        f"{q5 / r.SECONDS_PER_MONTH:.0f} x {r.SECONDS_PER_MONTH:,} seconds = {q5:,} replies, or {q5 * r.TOKENS_PER_REPLY / 1e9:.1f} billion tokens, below the {W(x24['tokens'])} billion crossing",
        f"Renting costs {D(r.monthly_rent(q5))}. Owning takes {g5} GPUs: {g5} x {r.HOURS_PER_MONTH} x ${r.GPU_HOUR} = {D(g5 * r.HOURS_PER_MONTH * r.GPU_HOUR)}, plus ${r.PEOPLE:,} for people = {D(r.monthly_own(g5))}",
    ]
    visible = {
        "cost-per-million.svg": [f"rent ${rent_pm:.4f}", f"own, {now['gpus']} GPUs", f"{P(busy_cross)} busy", f"${own_pm:.4f}", f"for a fixed set of {now['gpus']} GPUs that costs {D(now['own'])} a month", f"at most {P(ceiling)}",
                                f"The curve ends at {P(ceiling)} busy, the most that {now['gpus'] - r.SPARE} planned GPUs and a spare can reach."],
        "people-cost.svg": [f"people {D(p)} a month: {x['gpus']} GPUs" for p, x in people.items()] + [f">{B(x['tokens'])}<" for x in people.values()],
        "rent-or-own.svg": [f">{B(x24['tokens'])}<", f"rises ${round(now['rent_per_million'] * 1000)} for each billion tokens", f"Owning starts at {D(r.monthly_own(1 + r.SPARE))}", f"steps up {D(gpu_month)} for each GPU added",
                            f"with {x24['gpus']} GPUs paid for there. At {B(T)}, renting costs {D(now['rent'])} and owning {D(now['own'])}."],
        "same-tokens-fewer-hours.svg": [f"{h} hours a day: {by[h][0]['gpus']} GPUs, {P(by[h][0]['busy'])} busy" for h in (24, 18, 12, 8, 4)]
                                       + [f"{D(by[h][0]['own'])}, {'cheaper' if by[h][0]['own'] < by[h][0]['rent'] else 'dearer'}" for h in (24, 18, 12, 8, 4)] + [f"renting, {D(now['rent'])} a month"],
    }
    missing = [f"lesson.md: {fragment!r}" for fragment in wanted if fragment not in prose]
    missing += [f"{name}: {fragment!r}" for name, fragments in visible.items() for fragment in fragments if fragment not in figures[name]]
    assert not missing, "not found: " + "; ".join(missing)
    for name, text in figures.items():                                                              # one palette, a white page, a title and a description, no script and no address
        assert not re.search(r"0072B2|56B4E9|D55E00", text, re.I) and "<title>" in text and "<desc>" in text and 'fill="#ffffff"' in text, name
        assert "<script" not in text and not WEB_ADDRESS.search(text.replace("ht" + "tp://www.w3.org/2000/svg", "")), name
    for path in CODE.glob("*.py"):
        assert not WEB_ADDRESS.search(path.read_text(encoding="utf-8")), path.name
