"""The page on your own computer for saying yes or no."""
import http.cookiejar
import threading
import html
import json
import urllib.error
import urllib.parse
import urllib.request

import pytest

from scout.projects import Project
from scout.review import decide, make_server
from scout.store import Store

TOKEN = "a-pass-word-for-this-run"
PROJECT = Project(key="terminal", name="Test App", folder="/tmp/app")


@pytest.fixture
def store(tmp_path):
    """A real file, because the page opens its own connection for every request — which is the
    whole point: a running watcher and this page have to see each other's decisions."""
    return Store(tmp_path / "scout.sqlite")


@pytest.fixture
def site(store):
    server = make_server(projects=[PROJECT], token=TOKEN, port=0,
                         store_factory=lambda: Store(store.path),
                         reader_factory=lambda: Store(store.path, readonly=True))
    threading.Thread(target=server.serve_forever, daemon=True).start()
    yield f"http://127.0.0.1:{server.server_address[1]}"
    server.shutdown()


def an_idea(store, title="Whale feed", status="proposed"):
    iid = store.add_idea("terminal", title, "It shows a thing.", user_value="People asked.",
                         evidence=[{"url": "https://example.com/a", "note": "a thread"}])
    if status != "proposed":
        store.set_status(iid, status, build_branch=f"scout/{iid}-x")
    return iid


class NoRedirects(urllib.request.HTTPRedirectHandler):
    """Look at the redirect itself instead of following it."""

    def redirect_request(self, *args, **kwargs):
        return None


def browser():
    """A client that keeps cookies, the way a real browser does."""
    return urllib.request.build_opener(
        urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))


def get(url, headers=None, using=None):
    req = urllib.request.Request(url, headers=headers or {})
    try:
        with (using or browser()).open(req) as r:
            return r.status, r.read().decode()
    except urllib.error.HTTPError as e:          # a refusal is an answer, not a crash
        return e.code, e.read().decode()


def post(url, fields, headers=None, using=None):
    data = urllib.parse.urlencode(fields).encode()
    req = urllib.request.Request(url, data=data, headers=headers or {})
    try:
        with (using or browser()).open(req) as r:            # 303 is followed to the new page
            return r.status, r.read().decode()
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode()


def opened(site):
    """Follow the private link once; the cookie carries the secret from then on."""
    client = browser()
    get(f"{site}/?k={TOKEN}", using=client)
    return client


def test_the_ideas_are_listed_in_words_a_person_can_act_on(site, store):
    an_idea(store)
    status, body = get(f"{site}/", using=opened(site))
    assert status == 200
    assert "Waiting on you" in body and "Whale feed" in body
    assert "Yes, build it" in body and "No thanks" in body
    assert "https://example.com/a" in body
    for jargon in ("worktree", "branch", "commit", "merge", "repository"):
        assert jargon not in body.lower()


def test_without_the_pass_word_the_page_shows_nothing(site, store):
    an_idea(store, "A hidden idea")
    status, body = get(f"{site}/")
    assert status == 403 and "hidden" not in body.lower()
    status, body = get(f"{site}/?k=wrong")
    assert status == 403


def test_the_private_link_takes_itself_out_of_the_address_bar(site, store):
    """So the secret isn't left in history, or in a link shared by accident."""
    an_idea(store)
    client = browser()
    with client.open(urllib.request.Request(f"{site}/?k={TOKEN}")) as r:
        assert r.url.rstrip("/") == site          # redirected to the plain address
        assert "Waiting on you" in r.read().decode()
    jar = [c for h in client.handlers if hasattr(h, "cookiejar") for c in h.cookiejar]
    assert any(c.name == "feature_scout" for c in jar)


def test_the_link_still_works_when_it_arrives_from_somewhere_else(site, store):
    """The link gets pasted into notes and chats, so the click that opens it comes from another site."""
    an_idea(store)
    client = browser()
    status, body = get(f"{site}/?k={TOKEN}", headers={"Sec-Fetch-Site": "cross-site"}, using=client)
    assert status == 200 and "Waiting on you" in body


def test_saying_yes_is_recorded(site, store):
    iid = an_idea(store)
    status, body = post(f"{site}/decide", {"k": TOKEN, "verb": "approve", "id": iid, "was": "proposed"},
                        using=opened(site))
    assert status == 200
    assert store.idea(iid)["status"] == "approved"
    assert "built the next time" in body


def test_a_decision_cannot_be_made_by_following_a_link(site, store):
    iid = an_idea(store)
    status, _ = get(f"{site}/decide?verb=approve&id={iid}", using=opened(site))
    assert status == 405
    assert store.idea(iid)["status"] == "proposed"


def test_a_page_from_another_website_cannot_press_the_buttons(site, store):
    iid = an_idea(store)
    status, _ = post(f"{site}/decide", {"k": TOKEN, "verb": "approve", "id": iid},
                     headers={"Sec-Fetch-Site": "cross-site"}, using=opened(site))
    assert status == 403
    assert store.idea(iid)["status"] == "proposed"


def test_having_visited_the_page_is_not_enough_to_press_a_button(site, store):
    """The real guard: the secret has to be in the form, not just in the cookie a browser
    would send along with anybody's request."""
    iid = an_idea(store)
    status, _ = post(f"{site}/decide", {"verb": "approve", "id": iid}, using=opened(site))
    assert status == 403
    assert store.idea(iid)["status"] == "proposed"


def test_a_request_addressed_to_somewhere_else_is_refused(site, store):
    an_idea(store)
    port = site.rsplit(":", 1)[1]
    status, _ = get(f"{site}/?k={TOKEN}", headers={"Host": f"evil.example.com:{port}"})
    assert status == 403


def test_pressing_the_same_button_twice_does_not_do_it_twice(site, store):
    iid = an_idea(store)
    client = opened(site)
    post(f"{site}/decide", {"k": TOKEN, "verb": "approve", "id": iid, "was": "proposed"}, using=client)
    status, body = post(f"{site}/decide", {"k": TOKEN, "verb": "approve", "id": iid, "was": "proposed"},
                        using=client)
    assert "has moved on since the page was drawn" in body
    assert store.idea(iid)["status"] == "approved"


def test_an_idea_title_cannot_smuggle_anything_into_the_page(site, store):
    an_idea(store, "<script>alert('x')</script>")
    _, body = get(f"{site}/", using=opened(site))
    assert "<script>alert" not in body
    assert "&lt;script&gt;" in body


def test_something_ready_to_try_says_whether_it_was_checked(site, store):
    import json
    iid = an_idea(store, "Checked thing", status="built")
    store.set_status(iid, "built", build_report=json.dumps(
        {"status": "done", "summary": "Built it.", "how_to_try": "/whales",
         "tests": {"passed": True}}))
    _, body = get(f"{site}/", using=opened(site))
    assert "Ready to try" in body and "Your tests pass with this change." in body
    assert "Put it into my project" in body

    other = an_idea(store, "Unchecked thing", status="built")
    store.set_status(other, "built", build_report=json.dumps({"status": "done", "summary": "x"}))
    _, body = get(f"{site}/", using=opened(site))
    assert "Not checked by tests" in body


def test_saying_put_it_in_visibly_moves_it(site, store):
    """The merge happens on the next pass, so the card has to stop looking like it needs a
    decision — otherwise the button appears to have done nothing."""
    import json
    iid = an_idea(store, "Print a week", status="built")
    store.set_status(iid, "built", build_report=json.dumps({"status": "done", "summary": "x"}))
    client = opened(site)

    status, body = post(f"{site}/decide", {"k": TOKEN, "verb": "ship", "id": iid, "was": "built"},
                        using=client)
    assert status == 200
    assert "going into your project on the next pass" in body
    assert "Going into your project" in body                 # it has moved section
    assert "Ready to try" not in body                        # and left the old one
    assert "Waiting to go in" in body
    assert "Put it into my project" not in body              # nothing left to press
    assert store.idea(iid)["ship_requested_at"]


def test_it_stays_put_until_the_merge_actually_happens(site, store):
    import json
    iid = an_idea(store, "Print a week", status="built")
    store.set_status(iid, "built", build_report=json.dumps({"status": "done", "summary": "x"}))
    store.request_ship(iid)
    _, body = get(f"{site}/", using=opened(site))
    assert "Going into your project" in body and "Put it into my project" not in body


def test_the_researchers_own_ranking_is_kept_within_a_day(store):
    """With no value scores (ideas from before scoring existed) the old order holds: newest
    day first, and within a day the order the researcher ranked them in."""
    from scout.review import in_reading_order
    day = 86400 * 20000
    ideas = [
        {"id": 1, "created_at": day},          # yesterday, best
        {"id": 2, "created_at": day},          # yesterday, weaker
        {"id": 3, "created_at": day + 86400},  # today, best
        {"id": 4, "created_at": day + 86400},  # today, weaker
    ]
    assert [i["id"] for i in in_reading_order(ideas)] == [3, 4, 1, 2]


def test_the_page_puts_the_most_valuable_idea_first_whatever_day_it_arrived(store):
    """Ken, 2026-09-06: "order by value". A 5 from last week beats a 2 from this morning; ideas
    without a score sit at the bottom until `scout score` gives them one."""
    from scout.review import in_reading_order
    day = 86400 * 20000
    ideas = [
        {"id": 1, "created_at": day, "value_score": 5},
        {"id": 2, "created_at": day, "value_score": None},
        {"id": 3, "created_at": day + 86400, "value_score": 2},
        {"id": 4, "created_at": day + 86400, "value_score": 5},
        {"id": 5, "created_at": day + 86400, "value_score": 4},
    ]
    assert [i["id"] for i in in_reading_order(ideas)] == [4, 1, 5, 3, 2]


def test_the_value_and_its_reason_are_on_the_card(site, store):
    store.add_idea("terminal", "Valuable thing", "s", evidence=[{"url": "https://e", "note": "n"}],
                   value_score=4, value_reason="most traders check this daily")
    _, body = get(f"{site}/", using=opened(site))
    assert "Value 4 of 5" in body and "most traders check this daily" in body


def test_how_big_a_job_it_is_is_on_the_card(site, store):
    iid = store.add_idea("terminal", "A small thing", "s", effort="S",
                         evidence=[{"url": "https://e", "note": "n"}])
    _, body = get(f"{site}/", using=opened(site))
    assert "Roughly a small job." in body
    store.set_status(iid, "built", build_branch="x")
    _, body = get(f"{site}/", using=opened(site))
    assert "Roughly a small job." not in body, "not useful once it is already built"


def test_a_very_long_list_says_what_it_is_not_showing(site, store):
    from scout import review
    for n in range(review.PER_SECTION + 3):
        store.add_idea("terminal", f"Idea {n}", "s", evidence=[{"url": "https://e", "note": "n"}])
    _, body = get(f"{site}/", using=opened(site))
    assert "and 3 more, not shown here" in body


def test_a_build_that_was_interrupted_can_be_picked_up_again(site, store):
    """A build killed with the machine leaves its idea saying "building" for ever, and the
    queue only takes approved ones — so without this it is stuck and invisible."""
    iid = an_idea(store, "Settlement sources card")
    run = store.start_run(f"build:{iid}")
    store.set_status(iid, "building", build_branch=f"scout/{iid}-x")
    store.finish_run(run, False, {"status": "failed"})      # the run ended, the idea did not

    _, body = get(f"{site}/", using=opened(site))
    assert "Stopped part-way" in body
    assert "Pick it up again" in body
    assert "Nothing reached your project" in body

    post(f"{site}/decide", {"k": TOKEN, "verb": "approve", "id": iid, "was": "building"},
         using=opened(site))
    assert store.idea(iid)["status"] == "approved"


def test_a_build_that_really_is_running_is_left_alone(site, store):
    iid = an_idea(store, "Still going")
    store.start_run(f"build:{iid}")                          # started, never finished
    store.set_status(iid, "building", build_branch=f"scout/{iid}-x")
    _, body = get(f"{site}/", using=opened(site))
    assert "Being built now" in body and "Stopped part-way" not in body
    assert "Pick it up again" not in body


def test_a_build_killed_with_the_machine_is_spotted(store):
    """The real case: the machine went away mid-build, so the run was never closed either.
    Asking "did the run finish?" would call this one healthy for ever."""
    import time as clock
    from scout.review import mark_if_stopped, stage
    iid = an_idea(store, "Interrupted")
    store.start_run(f"build:{iid}")
    store.set_status(iid, "building", build_branch=f"scout/{iid}-x")
    idea = store.idea(iid)
    began = float(store.last_run(f"build:{iid}")["started_at"])

    # the machine has been up since before the work began: it really is still going
    running = mark_if_stopped(store, idea, now=began + 120, booted=began - 3600)
    assert stage(running) == "building"

    # the same open run, but the machine restarted a minute after the work began
    killed = mark_if_stopped(store, idea, now=began + 120, booted=began + 60)
    assert stage(killed) == "stalled"


def test_a_build_running_longer_than_is_possible_is_spotted(store):
    """Belt and braces for a machine that cannot say when it booted."""
    import time as clock
    from scout.review import mark_if_stopped, stage
    from scout.config import BUILD_TIMEOUT_S
    iid = an_idea(store, "Far too long")
    store.start_run(f"build:{iid}")
    store.set_status(iid, "building", build_branch=f"scout/{iid}-x")
    idea = store.idea(iid)
    later = clock.time() + BUILD_TIMEOUT_S + 3600
    assert stage(mark_if_stopped(store, idea, now=later, booted=None)) == "stalled"


def test_reading_the_page_never_waits_on_a_build(store):
    """A build holds the database while it writes. Opening the usual way runs a schema check —
    itself a write — so every page load would queue behind it. Reading is read-only."""
    an_idea(store)
    reader = Store(store.path, readonly=True)
    try:
        assert len(reader.ideas()) == 1
        with pytest.raises(Exception):          # proves nothing on this connection can write
            reader.conn.execute("CREATE TABLE scribble (x INTEGER)")
    finally:
        reader.conn.close()


def test_a_brand_new_machine_can_still_show_the_page(tmp_path):
    """Read-only cannot create the file, so the first ever run must not fall over."""
    store = Store(tmp_path / "not-there-yet.sqlite", readonly=True)
    assert store.ideas() == []


def test_what_has_been_spent_today_is_on_the_page(site, store):
    run = store.start_run("scan:terminal")
    store.finish_run(run, True, {"project": "terminal", "cost_usd": 2.5})
    _, body = get(f"{site}/", using=opened(site))
    assert "$2.50" in body


def test_an_empty_day_says_what_to_do_next(site, store):
    _, body = get(f"{site}/", using=opened(site))
    assert "feature-scout scan" in body


# ---- the decision rules themselves, without a server in the way ----

def test_a_decision_about_another_projects_idea_is_refused(store):
    iid = an_idea(store)
    other = Project(key="somethingelse", name="Other", folder="/tmp/other")
    assert "another project" in decide(store, [other], "approve", iid)
    assert store.idea(iid)["status"] == "proposed"


def test_the_rules_about_what_can_follow_what_are_the_same_as_everywhere_else(store):
    iid = an_idea(store)
    said = decide(store, [PROJECT], "ship", iid)      # can't put in what isn't built
    assert "can't ship" in said
    assert store.idea(iid)["status"] == "proposed"


def test_an_idea_that_has_vanished_is_handled(store):
    assert "can't find" in decide(store, [PROJECT], "approve", 9999)


# ---- the hosted page, which anyone on the internet can reach ----

def test_the_hosted_page_accepts_the_address_its_host_gives_it(store):
    """Behind a hosting proxy the Host header is the public name, not this machine."""
    server = make_server(projects=[PROJECT], token=TOKEN, port=0, public=True,
                         store_factory=lambda: Store(store.path),
                         reader_factory=lambda: Store(store.path, readonly=True))
    threading.Thread(target=server.serve_forever, daemon=True).start()
    try:
        site = f"http://127.0.0.1:{server.server_address[1]}"
        an_idea(store)
        hosted = {"Host": "feature-scout.fly.dev"}

        # The link hands over the secret and sends the browser to a plain address.
        req = urllib.request.Request(f"{site}/?k={TOKEN}", headers=hosted)
        opener = urllib.request.build_opener(NoRedirects())
        try:                                   # urllib treats an unfollowed redirect as an error
            with opener.open(req) as r:
                status, headers = r.status, r.headers
        except urllib.error.HTTPError as e:
            status, headers = e.code, e.headers
        assert status == 303 and headers["Location"] == "/"
        handover = headers["Set-Cookie"]
        assert "Secure" in handover, "the cookie must only travel over https"
        assert "HttpOnly" in handover and "SameSite=Lax" in handover

        status, body = get(f"{site}/", headers={**hosted, "Cookie": f"feature_scout={TOKEN}"})
        assert status == 200 and "Waiting on you" in body
    finally:
        server.shutdown()


def test_a_stranger_gets_nothing_from_the_hosted_page(store):
    server = make_server(projects=[PROJECT], token=TOKEN, port=0, public=True,
                         store_factory=lambda: Store(store.path),
                         reader_factory=lambda: Store(store.path, readonly=True))
    threading.Thread(target=server.serve_forever, daemon=True).start()
    try:
        site = f"http://127.0.0.1:{server.server_address[1]}"
        an_idea(store, "Not for strangers")
        for url in (f"{site}/", f"{site}/?k=guessing", f"{site}/decide"):
            status, body = get(url, headers={"Host": "feature-scout.fly.dev"})
            assert status == 403, url
            assert "strangers" not in body.lower()
    finally:
        server.shutdown()


def test_the_page_refuses_to_be_hosted_without_a_secret(monkeypatch):
    from scout.review import hosted_settings
    monkeypatch.delenv("SCOUT_REVIEW_TOKEN", raising=False)
    assert hosted_settings() is None
    monkeypatch.setenv("SCOUT_REVIEW_TOKEN", "too-short")
    assert hosted_settings() is None, "a guessable secret must not be accepted"
    monkeypatch.setenv("SCOUT_REVIEW_TOKEN", "x" * 24)
    monkeypatch.setenv("SCOUT_REVIEW_URL", "https://feature-scout.fly.dev")
    assert hosted_settings() == ("x" * 24, "https://feature-scout.fly.dev")


def test_what_happened_lately_shows_on_the_page(site, store):
    """Ken (2026-09-11): "I don't want [emails]. I much prefer the web interface." What the
    scout used to email — a build finished, something put in, a problem — is a line on the page."""
    an_idea(store)
    status, body = get(f"{site}/?k={TOKEN}")
    assert "Lately" not in body, "nothing to say yet, so no empty box"
    store.notice("done", "Built #1 Whale alerts — it is ready to try.")
    store.notice("problem", "poll crashed: <boom>")
    status, body = get(f"{site}/?k={TOKEN}")
    assert status == 200 and "Lately (2)" in body and "<details class='lately' open>" in body
    assert "Built #1 Whale alerts" in body and "poll crashed: &lt;boom&gt;" in body
    assert "<li class='problem'>" in body


# ---- the board ----

def test_the_board_has_three_columns_and_a_put_aside_pile(site, store):
    """Ken (2026-09-09): "more like a Jira board ... backlog vs build vs done"."""
    a = an_idea(store, "New one")
    b = an_idea(store, "Queued one", status="approved")
    c = an_idea(store, "Finished one", status="shipped")
    d = an_idea(store, "No thanks one", status="rejected")
    _, body = get(f"{site}/", using=opened(site))
    for col in ("backlog", "build", "progress", "done", "rejected"):
        assert f"data-col='{col}'" in body or f'data-col="{col}"' in body
    def column_of(iid):
        pos = body.index(f'data-id="{iid}"')
        return body.rfind("data-col=", 0, pos)
    cols = [body[column_of(i):column_of(i) + 24] for i in (a, b, c, d)]
    assert "backlog" in cols[0] and "build" in cols[1] and "done" in cols[2] and "rejected" in cols[3]
    assert "Sortable.create" in body and "verb: 'order'" in body and "cdnjs.cloudflare.com/ajax/libs/Sortable" in body


def test_dragging_within_build_sets_the_build_order(site, store):
    ids = [an_idea(store, f"Idea {n}", status="approved") for n in range(3)]
    client = opened(site)
    status, body = post(f"{site}/decide", {"k": TOKEN, "verb": "order", "ids": f"{ids[2]},{ids[0]},{ids[1]}"}, using=client)
    assert status == 200 and "Build order updated" in body
    rows = store.conn.execute("select id from ideas where status='approved' order by decided_at, id").fetchall()
    assert [r[0] for r in rows] == [ids[2], ids[0], ids[1]]
    # The page shows Build in that order.
    positions = [body.index(f'data-id="{i}"') for i in (ids[2], ids[0], ids[1])]
    assert positions == sorted(positions)


def test_dragging_back_to_the_backlog_is_a_change_of_mind(site, store):
    queued = an_idea(store, "Queued", status="approved")
    aside = an_idea(store, "Put aside", status="rejected")
    running = an_idea(store, "Running", status="building")
    client = opened(site)
    _, body = post(f"{site}/decide", {"k": TOKEN, "verb": "backlog", "id": queued, "was": "approved"}, using=client)
    assert "back in the backlog" in body and store.idea(queued)["status"] == "proposed"
    post(f"{site}/decide", {"k": TOKEN, "verb": "backlog", "id": aside, "was": "rejected"}, using=client)
    assert store.idea(aside)["status"] == "proposed"
    _, body = post(f"{site}/decide", {"k": TOKEN, "verb": "backlog", "id": running, "was": "building"}, using=client)
    assert "go back to the backlog while" in body and store.idea(running)["status"] == "building"


def test_a_stuck_idea_sits_in_the_backlog_with_its_reason(site, store):
    iid = an_idea(store, "Broke", status="failed")
    store.set_status(iid, "failed", error="the test suite fails: x")
    _, body = get(f"{site}/", using=opened(site))
    pos = body.index(f'data-id="{iid}"')
    assert "backlog" in body[body.rfind("data-col=", 0, pos):pos]
    import html as _html
    body = _html.unescape(body)
    assert "Didn't work" in body and "the test suite fails" in body and "Try again" in body


def test_a_build_under_way_is_in_progress_and_a_finished_one_is_done(site, store):
    """Ken (2026-09-09): "Might add an in process column"."""
    running = an_idea(store, "Running", status="building")
    store.start_run(f"build:{running}")                      # a live run, so it is not 'stalled'
    built = an_idea(store, "Built", status="built")
    _, body = get(f"{site}/", using=opened(site))
    def column_of(iid):
        pos = body.index(f'data-id="{iid}"')
        return body[body.rfind("data-col=", 0, pos):pos][:24]
    assert "progress" in column_of(running) and "progress" in column_of(built)
    rules = json.loads(html.unescape(body.split("data-rules='")[1].split("'")[0]))
    assert rules["progress"] == [], "nothing can be dragged into In progress"
    assert rules["done"] == ["built"] and "proposed" in rules["build"]


def test_saying_yes_starts_a_build_right_away(site, store, monkeypatch):
    """Ken (2026-09-09): "When there is an item in build, lets build it. No reason to wait."."""
    from scout import review
    kicked = []
    monkeypatch.setattr(review, "kick_build", lambda: kicked.append(1) or True)
    iid = an_idea(store)
    client = opened(site)
    post(f"{site}/decide", {"k": TOKEN, "verb": "approve", "id": iid, "was": "proposed"}, using=client)
    assert kicked == [1]
    post(f"{site}/decide", {"k": TOKEN, "verb": "reject", "id": iid, "was": "approved"}, using=client)
    assert kicked == [1], "saying no starts nothing"


def test_the_kick_runs_the_configured_command_detached(monkeypatch, tmp_path):
    from scout import review
    marker = tmp_path / "kicked"
    monkeypatch.setenv("SCOUT_KICK_CMD", f"sh -c 'echo hi > {marker}'")
    assert review.kick_build() is True
    import time
    for _ in range(50):
        if marker.exists(): break
        time.sleep(0.05)
    assert marker.read_text().strip() == "hi"
    monkeypatch.delenv("SCOUT_KICK_CMD")
    assert review.kick_build() is False


def test_the_pages_scripts_are_allowed_to_run_and_nothing_else_is(site, store):
    """2026-09-09: the security header shut out every script, so dragging did nothing."""
    import re
    an_idea(store)
    client = opened(site)
    req = urllib.request.Request(f"{site}/")
    with client.open(req) as r:
        csp = r.headers.get("Content-Security-Policy"); body = r.read().decode()
    nonce = re.search(r"'nonce-([A-Za-z0-9_-]+)'", csp).group(1)
    assert "default-src 'none'" in csp and "https://cdnjs.cloudflare.com" in csp
    assert body.count(f"nonce='{nonce}'") == 2, "both script tags carry this response's nonce"
    with client.open(urllib.request.Request(f"{site}/")) as r2:
        assert re.search(r"'nonce-([A-Za-z0-9_-]+)'", r2.headers.get("Content-Security-Policy")).group(1) != nonce
