#!/bin/bash
# Does Wonder Ink open the worksheets it should and refuse the files it
# should not?
#
# The download-to-tab workflow is driven by hand against a running
# session; this is the part of it that can be asserted rather than looked
# at — which local files the shell will load, and which it will not.
#
# It runs inside the lab, because the classroom's own PyQt6 is there and
# the development host has no Qt at all. It imports the *installed*
# command, so it is testing what the package put on the machine.
set -uo pipefail

NAME="${LAB_NAME:-classroom-v2-lab}"

docker exec -i \
    -e QT_QPA_PLATFORM=offscreen \
    -e QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
    -e XDG_RUNTIME_DIR=/tmp/pdfcheck-rt \
    -e HOME=/root \
    "$NAME" bash -c 'mkdir -p /tmp/pdfcheck-rt && chmod 700 /tmp/pdfcheck-rt && python3 -' <<'PY'
import importlib.util, sys, tempfile
from importlib.machinery import SourceFileLoader
from pathlib import Path

loader = SourceFileLoader("wonderink", "/usr/local/bin/classroom-wonderink")
spec = importlib.util.spec_from_loader("wonderink", loader)
ink = importlib.util.module_from_spec(spec)
loader.exec_module(ink)

from PyQt6.QtCore import QUrl
from PyQt6.QtWidgets import QApplication
from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEnginePage

app = QApplication(sys.argv[:1])
profile = QWebEngineProfile("pdfcheck", app)
window = ink.WonderInk(profile)

passed = failed = 0

def check(label, condition, detail=""):
    global passed, failed
    if condition:
        print(f"  \033[32mPASS\033[0m  {label}")
        passed += 1
    else:
        print(f"  \033[31mFAIL\033[0m  {label}" + (f"  [{detail}]" if detail else ""))
        failed += 1

work = Path(tempfile.mkdtemp())
real = work / "worksheet.pdf"
real.write_bytes(b"%PDF-1.4\n% a worksheet\n")
spaces = work / "Leader Guide, Week 1 (Advent).pdf"
spaces.write_bytes(b"%PDF-1.4\n% another\n")
htmlish = work / "broken.pdf"
htmlish.write_bytes(b"<html><body>404</body></html>")
empty = work / "empty.pdf"
empty.write_bytes(b"")
notes = work / "notes.txt"
notes.write_text("bring scissors")

print("1. which local files the window will open")
check("nothing at all, before anything is downloaded",
      not window.may_open(Path("/etc/passwd")))
check("not a file that merely exists", not window.may_open(real))

page = ink.LessonPage(profile, window)
check("and a page may not navigate to one",
      page.acceptNavigationRequest(
          QUrl.fromLocalFile("/etc/passwd"),
          QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is False)
check("nor to a worksheet that was never opened",
      page.acceptNavigationRequest(
          QUrl.fromLocalFile(str(real)),
          QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is False)

print()
print("2. a worksheet the window itself opened")
window.open_pdf(real)
check("is openable afterwards", window.may_open(real))
check("and only it", not window.may_open(Path("/etc/passwd")))
check("a page may navigate to that one",
      page.acceptNavigationRequest(
          QUrl.fromLocalFile(str(real)),
          QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is True)
check("a symlink to somewhere else is not laundered into permission",
      not window.may_open(Path("/etc/passwd")))

print()
print("3. which schemes are allowed at all")
for scheme, want in (("https://app.wonderink.org/", True),
                     ("http://example.org/", True),
                     ("about:blank", True),
                     ("mailto:someone@example.org", False),
                     ("ftp://example.org/x", False),
                     ("javascript:alert(1)", False)):
    check(f"{scheme} -> {want}",
          page.acceptNavigationRequest(
              QUrl(scheme),
              QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is want)

print()
print("4. which downloads become a worksheet tab")
before = window._tabs.count()
check("a real PDF does", window.open_worksheet(spaces, "application/pdf") is True)
check("and it made a tab", window._tabs.count() == before + 1)
check("a PDF by extension alone does", window.open_worksheet(spaces) is True)

before = window._tabs.count()
check("HTML called .pdf does not open a blank tab",
      window.open_worksheet(htmlish, "application/pdf") is True
      and window._tabs.count() == before)
check("an empty file does not either",
      window.open_worksheet(empty, "application/pdf") is True
      and window._tabs.count() == before)
check("a file that is not there at all is handled",
      window.open_worksheet(work / "missing.pdf", "application/pdf") is True
      and window._tabs.count() == before)
check("plain text is not a worksheet and is left to the notification",
      window.open_worksheet(notes, "text/plain") is False
      and window._tabs.count() == before)
check("and a text file named .pdf is refused on its contents",
      window.open_worksheet(htmlish) is True and window._tabs.count() == before)

print()
print("5. names with spaces and punctuation survive the round trip")
window.open_pdf(spaces)
url = QUrl.fromLocalFile(str(spaces.resolve()))
# toString() is the display form and shows the spaces as spaces; the
# encoded form is what actually goes on the wire.
check("the URL percent-encodes rather than breaks",
      "%20" in bytes(url.toEncoded()).decode()
      and url.toLocalFile() == str(spaces.resolve()))
check("and the window will open exactly that path", window.may_open(spaces))

print()
print(f"{passed} passed, {failed} failed")

# Pages go before their profile. Left to Python's own teardown the order
# is arbitrary, and a profile released while a page still uses it is a
# segfault on the way out — measured, intermittently, AFTER every check
# had passed, which made a passing run report failure. The real Wonder
# Ink was closed six times in the same lab and exited 0 each time.
from PyQt6.QtCore import QCoreApplication, QEvent

for index in range(window._tabs.count()):
    view = window._tabs.widget(index)
    view.page().deleteLater()
    view.deleteLater()
window.close()
window.deleteLater()
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete.value)
profile.deleteLater()
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete.value)
sys.stdout.flush()
sys.exit(1 if failed else 0)
PY
