#!/usr/bin/env python3
"""Browser-level guard for battle_skill_timeline_review.html playback rows.

The static canonical verifier proves that EXE-derived rows exist.  This smoke
test proves the review page itself selects those rows and renders the raw and
playback filmstrips without reintroducing old shifted-skill regressions.
"""
from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import sys
import time
from pathlib import Path
from urllib.error import URLError
from urllib.request import urlopen

from browser_smoke_helpers import (
    WebDriverError,
    execute_async_js,
    execute_js,
    free_port,
    request_json,
    wait_for_driver,
)


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"


CHECKS: list[dict] = [
    {
        "ownerKey": "rinshan",
        "skillIdHex": "0x05",
        "levelOrFixed": None,
        "name": "도발",
        "rawFrames": [51, 52, 53, 51, 52, 53, 51, 52, 53, 54, 4, 5, 4],
    },
    {
        "ownerKey": "rinshan",
        "skillIdHex": "0x06",
        "levelOrFixed": None,
        "name": "꼬챙이 꿰기",
        "rawFrames": [20, 21, 22, 23, 23, 3],
    },
    {
        "ownerKey": "rinshan",
        "skillIdHex": "0x09",
        "levelOrFixed": 1,
        "name": "안면백조권",
        "rawFrames": [6, 7, 8, 9, 9, 16, 17, 18, 19, 19, 6, 7, 8, 9, 9, 3],
    },
    {
        "ownerKey": "rinshan",
        "skillIdHex": "0x0c",
        "levelOrFixed": 4,
        "name": "안면백조권",
        "rawFrames": [6, 7, 8, 9, 9, 16, 17, 18, 19, 19, 33, 34, 38, 39, 39, 33, 34, 35, 36, 37, 35, 34, 42, 42, 41, 40, 3],
    },
    {
        "ownerKey": "ataho",
        "skillIdHex": "0x2f",
        "levelOrFixed": 1,
        "name": "맹호룬룬권",
        "rawFrames": [44, 45, 46, 47, 44, 48, 49, 50, 0],
    },
    {
        "ownerKey": "ataho",
        "skillIdHex": "0x32",
        "levelOrFixed": 4,
        "name": "맹호룬룬권",
        "rawFrames": [44, 45, 46, 47, 44, 48, 49, 50, 12, 13, 25, 26, 8, 9, 10, 11, 0],
    },
]


EFFECT_CHECKS: list[dict] = [
    {"ownerKey": "ataho", "skillIdHex": "0x0d", "levelOrFixed": 4, "name": "호격권", "minEffectFrames": 1},
    {"ownerKey": "ataho", "skillIdHex": "0x11", "levelOrFixed": 4, "name": "맹호비상각", "minEffectFrames": 1},
    {"ownerKey": "ataho", "skillIdHex": "0x15", "levelOrFixed": 4, "name": "폭전축", "minEffectFrames": 1},
    {"ownerKey": "ataho", "skillIdHex": "0x19", "levelOrFixed": 4, "name": "맹호스페셜", "minEffectFrames": 1},
    {"ownerKey": "rinshan", "skillIdHex": "0x10", "levelOrFixed": 4, "name": "선렬각", "minEffectFrames": 1},
    {"ownerKey": "rinshan", "skillIdHex": "0x18", "levelOrFixed": 4, "name": "암각·영상승룡파", "minEffectFrames": 1},
    {"ownerKey": "ataho", "skillIdHex": "0x37", "levelOrFixed": None, "name": "주구격", "minEffectFrames": 1},
    {"ownerKey": "ataho", "skillIdHex": "0x38", "levelOrFixed": None, "name": "취호염무", "minEffectFrames": 1},
    {"ownerKey": "rinshan", "skillIdHex": "0x2b", "levelOrFixed": 4, "name": "암각·영상뢰화", "minEffectFrames": 1},
    {"ownerKey": "smashu", "skillIdHex": "0x17", "levelOrFixed": 4, "name": "백인일섬", "minEffectFrames": 1},
    {"ownerKey": "smashu", "skillIdHex": "0x1f", "levelOrFixed": 4, "name": "비검·시공단", "minEffectFrames": 1},
    {"ownerKey": "smashu", "skillIdHex": "0x20", "levelOrFixed": None, "name": "열시섬", "minEffectFrames": 1},
    {"ownerKey": "smashu", "skillIdHex": "0x22", "levelOrFixed": None, "name": "투신의 춤", "minEffectFrames": 1},
]


def load_execution_requirement_checks() -> list[dict]:
    """Return every skill row whose helper/effect behavior needs runner execution.

    These are not pixel-oracle checks.  They prove the browser can select every
    EXE-derived execution-requirement row and that the shared playback marker
    exposes the same requirement bucket without falling back to stale rows.
    """
    path = OUT / "battle_effect_animation_pattern_review.json"
    data = json.loads(path.read_text(encoding="utf-8"))
    checks: list[dict] = []
    seen: set[tuple[str, str, object]] = set()
    for row in data.get("skillRows") or []:
        requirements = row.get("executionRequirements") or []
        if not requirements:
            continue
        key = (
            str(row.get("ownerKey") or ""),
            str(row.get("skillIdHex") or "").lower(),
            row.get("levelOrFixed"),
        )
        if key in seen:
            continue
        seen.add(key)
        min_effect_frames = 0 if "palette transform effect; no CNS frame stream" in requirements else 1
        checks.append(
            {
                "ownerKey": key[0],
                "skillIdHex": key[1],
                "levelOrFixed": key[2],
                "name": row.get("skillName") or "",
                "requirements": requirements,
                "effectAnimationClass": row.get("effectAnimationClass") or "",
                "minEffectFrames": min_effect_frames,
            }
        )
    return checks


def wait_for_http(url: str, proc: subprocess.Popen[bytes], timeout: float = 8) -> None:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        if proc.poll() is not None:
            raise RuntimeError(f"static server exited early with status {proc.returncode}")
        try:
            with urlopen(url, timeout=0.5) as response:
                if response.status < 500:
                    return
        except (URLError, TimeoutError, OSError):
            time.sleep(0.1)
    raise RuntimeError(f"static server did not respond: {url}")


def wait_for_page_ready(port: int, session_id: str, timeout: float = 18) -> dict:
    script = """
return {
  readyState: document.readyState,
  marker: window.HWANSE_BATTLE_TIMELINE_REVIEW || null,
  hasSelector: typeof window.HWANSE_BATTLE_TIMELINE_SELECT === 'function',
  bootError: window.HWANSE_BATTLE_TIMELINE_BOOT_ERROR || null,
  title: document.title,
  rowCount: document.querySelectorAll('#skillList .skill-row').length,
  bodyText: document.body?.innerText || '',
};
"""
    deadline = time.monotonic() + timeout
    last_state: dict | None = None
    while time.monotonic() < deadline:
        last_state = execute_js(port, session_id, script, timeout=5)
        marker = last_state.get("marker") or {}
        if (
            last_state.get("readyState") == "complete"
            and last_state.get("hasSelector") is True
            and marker.get("browserBattleSkillTimelineReviewImplemented") is True
            and marker.get("canonicalRowsReady") is True
            and last_state.get("rowCount", 0) > 0
        ):
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"battle skill timeline review did not become ready: {last_state!r}")


def select_action(port: int, session_id: str, check: dict) -> dict:
    payload = json.dumps({
        "ownerKey": check["ownerKey"],
        "skillIdHex": check["skillIdHex"],
        "levelOrFixed": check["levelOrFixed"],
    }, ensure_ascii=False)
    state = execute_async_js(
        port,
        session_id,
        f"""
const done = arguments[arguments.length - 1];
(async () => {{
  try {{
    const marker = await window.HWANSE_BATTLE_TIMELINE_SELECT({payload});
    done({{ ok: true, marker }});
  }} catch (error) {{
    done({{ ok: false, error: String(error && error.stack ? error.stack : error) }});
  }}
}})();
""",
        timeout=16,
    )
    if not state.get("ok"):
        raise WebDriverError(f"could not select action {check}: {state!r}")
    marker = state.get("marker") or {}
    expected_level = check["levelOrFixed"]
    selected = marker.get("selected") or {}
    if selected.get("ownerKey") != check["ownerKey"]:
        raise WebDriverError(f"selected owner mismatch for {check}: {marker!r}")
    if selected.get("skillIdHex") != check["skillIdHex"]:
        raise WebDriverError(f"selected skill id mismatch for {check}: {marker!r}")
    if selected.get("name") != check["name"]:
        raise WebDriverError(f"selected skill name mismatch for {check}: {marker!r}")
    if expected_level is None:
        if selected.get("levelOrFixed") is not None:
            raise WebDriverError(f"selected fixed row should not have level {check}: {marker!r}")
    elif int(selected.get("levelOrFixed") or -1) != int(expected_level):
        raise WebDriverError(f"selected level mismatch for {check}: {marker!r}")
    raw = marker.get("rawFrames") or []
    if "rawFrames" in check and raw != check["rawFrames"]:
        raise WebDriverError(f"raw frames mismatch for {check['name']} {check['skillIdHex']}: expected {check['rawFrames']}, got {raw}")
    playback = marker.get("playbackFrames") or []
    if "rawFrames" in check:
        if playback != check["rawFrames"]:
            raise WebDriverError(
                f"playback frame order mismatch for {check['name']} {check['skillIdHex']}: "
                f"expected {check['rawFrames']}, got {playback}"
            )
    if "rawFrames" in check and marker.get("rawFrameCardCount") != len(check["rawFrames"]):
        raise WebDriverError(f"raw filmstrip card count mismatch for {check}: {marker!r}")
    if marker.get("frameCardCount", 0) <= 0:
        raise WebDriverError(f"playback filmstrip did not render cards for {check}: {marker!r}")
    if "minEffectFrames" in check and int(marker.get("effectFrameCount") or 0) < int(check["minEffectFrames"]):
        raise WebDriverError(f"effect frame count too low for {check['name']} {check['skillIdHex']}: {marker!r}")
    if int(marker.get("effectFrameCount") or 0) > 0:
        stats = effect_strip_canvas_stats(port, session_id)
        marker["effectCanvasStats"] = stats
        if stats.get("canvasCount", 0) <= 0:
            raise WebDriverError(f"effect strip did not create canvases for {check}: {marker!r}")
        if stats.get("nonEmptyCanvasCount", 0) <= 0:
            raise WebDriverError(f"effect strip canvases are blank for {check}: {marker!r}")
        tick = first_effect_active_tick(marker)
        if tick is not None:
            preview = render_preview_tick(port, session_id, tick)
            marker["previewRender"] = preview
            marker["previewCanvasStats"] = preview_canvas_stats(port, session_id)
            if int(preview.get("activeEffectCount") or 0) <= 0:
                raise WebDriverError(f"preview render did not activate effects for {check}: {marker!r}")
            if int((marker.get("previewCanvasStats") or {}).get("nonEmptyCanvasCount") or 0) <= 0:
                raise WebDriverError(f"preview canvas is blank at active effect tick for {check}: {marker!r}")
            if int((marker.get("previewCanvasStats") or {}).get("coloredCanvasCount") or 0) <= 0:
                raise WebDriverError(f"preview canvas has no colored pixels at active effect tick for {check}: {marker!r}")
    if "requirements" in check:
        marker_requirements = set(marker.get("executionRequirements") or [])
        expected_requirements = set(check.get("requirements") or [])
        if not expected_requirements.issubset(marker_requirements):
            raise WebDriverError(
                f"execution requirements mismatch for {check['name']} {check['skillIdHex']}: "
                f"expected {sorted(expected_requirements)}, got {sorted(marker_requirements)} in {marker!r}"
            )
        validate_execution_requirement_marker(check, marker)
    return marker


def first_effect_active_tick(marker: dict) -> int | None:
    diagnostics = marker.get("effectDiagnostics") or {}
    samples = diagnostics.get("activeTickSamples") or []
    for value in samples:
        try:
            return int(value)
        except (TypeError, ValueError):
            continue
    value = diagnostics.get("tickMin")
    try:
        return int(value) if value is not None else None
    except (TypeError, ValueError):
        return None


def render_preview_tick(port: int, session_id: str, tick: int) -> dict:
    state = execute_async_js(
        port,
        session_id,
        f"""
const done = arguments[arguments.length - 1];
(async () => {{
  try {{
    if (typeof window.HWANSE_BATTLE_TIMELINE_RENDER_TICK !== 'function') {{
      throw new Error('HWANSE_BATTLE_TIMELINE_RENDER_TICK is not available');
    }}
    const result = await window.HWANSE_BATTLE_TIMELINE_RENDER_TICK({int(tick)});
    done({{ ok: true, result }});
  }} catch (error) {{
    done({{ ok: false, error: String(error && error.stack ? error.stack : error) }});
  }}
}})();
""",
        timeout=10,
    )
    if not state.get("ok"):
        raise WebDriverError(f"could not render preview tick {tick}: {state!r}")
    return state.get("result") or {}


def effect_strip_canvas_stats(port: int, session_id: str) -> dict:
    return execute_js(
        port,
        session_id,
        """
const canvases = [...document.querySelectorAll('#effectStrip canvas')];
const rows = canvases.map((canvas, index) => {
  const ctx = canvas.getContext('2d');
  const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  let alpha = 0;
  let colored = 0;
  for (let i = 0; i < data.length; i += 4) {
    const a = data[i + 3];
    if (a > 0) alpha += 1;
    if (a > 0 && (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245)) colored += 1;
  }
  return { index, width: canvas.width, height: canvas.height, alphaPixels: alpha, coloredPixels: colored };
});
return {
  canvasCount: canvases.length,
  nonEmptyCanvasCount: rows.filter((row) => row.alphaPixels > 0).length,
  coloredCanvasCount: rows.filter((row) => row.coloredPixels > 0).length,
  maxAlphaPixels: rows.reduce((max, row) => Math.max(max, row.alphaPixels), 0),
  rows: rows.slice(0, 12),
};
""",
        timeout=8,
    )


def preview_canvas_stats(port: int, session_id: str) -> dict:
    return execute_js(
        port,
        session_id,
        """
const canvas = document.querySelector('#previewCanvas');
if (!canvas) return { canvasCount: 0, nonEmptyCanvasCount: 0, coloredCanvasCount: 0, maxAlphaPixels: 0, rows: [] };
const ctx = canvas.getContext('2d');
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
let alpha = 0;
let colored = 0;
for (let i = 0; i < data.length; i += 4) {
  const a = data[i + 3];
  if (a > 0) alpha += 1;
  if (a > 0 && (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245)) colored += 1;
}
return {
  canvasCount: 1,
  nonEmptyCanvasCount: alpha > 0 ? 1 : 0,
  coloredCanvasCount: colored > 0 ? 1 : 0,
  maxAlphaPixels: alpha,
  rows: [{ index: 0, width: canvas.width, height: canvas.height, alphaPixels: alpha, coloredPixels: colored }],
};
""",
        timeout=8,
    )


def validate_execution_requirement_marker(check: dict, marker: dict) -> None:
    requirements = set(check.get("requirements") or [])
    diag = marker.get("effectDiagnostics") or {}
    effect_count = int(diag.get("effectFrameCount") or marker.get("effectFrameCount") or 0)
    tick_span = int(diag.get("tickSpan") or 0)
    if "palette transform effect; no CNS frame stream" in requirements:
        if diag.get("hasPaletteRequirement") is not True:
            raise WebDriverError(f"palette requirement not reflected in marker for {check}: {marker!r}")
    if "random placement/range present" in requirements:
        if int(diag.get("framesWithTransformScope") or 0) <= 0 and int(diag.get("framesWithCoordinateSummary") or 0) <= 0:
            raise WebDriverError(f"random/range requirement has no transform diagnostics for {check}: {marker!r}")
    if "runner must instantiate child objects over time" in requirements:
        if effect_count <= 1 or tick_span <= 0:
            raise WebDriverError(f"child-spawn requirement has no multi-frame timed effect for {check}: {marker!r}")
    if "child motion loop present" in requirements:
        if (
            int(diag.get("framesWithMotion") or 0) <= 0
            and int(diag.get("framesWithNestedVisual") or 0) <= 0
            and int(diag.get("framesWithLoopPreview") or 0) <= 0
            and not (tick_span > 0 and int(diag.get("framesWithTransformScope") or 0) > 0)
        ):
            raise WebDriverError(f"child-motion-loop requirement has no motion/nested diagnostics for {check}: {marker!r}")


def verify_browser(base: str, keep_log: bool = False) -> dict:
    driver_path = shutil.which("WebKitWebDriver")
    if not driver_path:
        raise WebDriverError("WebKitWebDriver is not installed; install webkit2gtk-driver and run under Xvfb")

    OUT.mkdir(parents=True, exist_ok=True)
    driver_port = free_port()
    log_path = OUT / "battle_skill_timeline_browser_webkitdriver.log"
    with log_path.open("wb") as log:
        proc = subprocess.Popen(
            [
                driver_path,
                "--host=127.0.0.1",
                f"--port={driver_port}",
                "--replace-on-new-session",
            ],
            stdout=log,
            stderr=subprocess.STDOUT,
        )
        session_id = ""
        try:
            wait_for_driver(driver_port, proc)
            session = request_json(
                driver_port,
                "POST",
                "/session",
                {"capabilities": {"alwaysMatch": {"browserName": "MiniBrowser"}}},
                timeout=30,
            )
            session_id = str((session or {}).get("value", {}).get("sessionId") or "")
            if not session_id:
                raise WebDriverError(f"could not create WebKit session: {session!r}")
            request_json(
                driver_port,
                "POST",
                f"/session/{session_id}/window/rect",
                {"x": 0, "y": 0, "width": 1420, "height": 960},
                timeout=8,
            )
            request_json(
                driver_port,
                "POST",
                f"/session/{session_id}/url",
                {"url": base.rstrip("/") + "/web/battle_skill_timeline_review.html"},
                timeout=30,
            )
            initial = wait_for_page_ready(driver_port, session_id)
            markers = [select_action(driver_port, session_id, check) for check in CHECKS]
            effect_markers = [select_action(driver_port, session_id, check) for check in EFFECT_CHECKS]
            requirement_checks = load_execution_requirement_checks()
            requirement_markers = [select_action(driver_port, session_id, check) for check in requirement_checks]
            requirement_buckets = sorted({
                requirement
                for check in requirement_checks
                for requirement in (check.get("requirements") or [])
            })
            report = {
                "status": "ok",
                "kind": "hwanse-battle-skill-timeline-browser-smoke",
                "base": base,
                "checkedRows": len(CHECKS),
                "checkedEffectRows": len(EFFECT_CHECKS),
                "checkedExecutionRequirementRows": len(requirement_checks),
                "checkedExecutionRequirementBuckets": requirement_buckets,
                "checks": [
                    {
                        "ownerKey": check["ownerKey"],
                        "skillIdHex": check["skillIdHex"],
                        "levelOrFixed": check["levelOrFixed"],
                        "name": check["name"],
                        "rawFrames": marker.get("rawFrames"),
                        "playbackFrameCount": len(marker.get("playbackFrames") or []),
                        "effectFrameCount": marker.get("effectFrameCount"),
                    }
                    for check, marker in zip(CHECKS, markers)
                ],
                "effectChecks": [
                    {
                        "ownerKey": check["ownerKey"],
                        "skillIdHex": check["skillIdHex"],
                        "levelOrFixed": check["levelOrFixed"],
                        "name": check["name"],
                        "effectFrameCount": marker.get("effectFrameCount"),
                        "effectCanvasStats": marker.get("effectCanvasStats"),
                        "previewRender": marker.get("previewRender"),
                        "previewCanvasStats": marker.get("previewCanvasStats"),
                        "playbackFrameCount": len(marker.get("playbackFrames") or []),
                    }
                    for check, marker in zip(EFFECT_CHECKS, effect_markers)
                ],
                "executionRequirementChecks": [
                    {
                        "ownerKey": check["ownerKey"],
                        "skillIdHex": check["skillIdHex"],
                        "levelOrFixed": check["levelOrFixed"],
                        "name": check["name"],
                        "requirements": check["requirements"],
                        "effectAnimationClass": check["effectAnimationClass"],
                        "effectFrameCount": marker.get("effectFrameCount"),
                        "effectDiagnostics": marker.get("effectDiagnostics"),
                        "effectCanvasStats": marker.get("effectCanvasStats"),
                        "previewRender": marker.get("previewRender"),
                        "previewCanvasStats": marker.get("previewCanvasStats"),
                        "playbackFrameCount": len(marker.get("playbackFrames") or []),
                    }
                    for check, marker in zip(requirement_checks, requirement_markers)
                ],
                "initial": initial,
            }
            (OUT / "battle_skill_timeline_browser_smoke.json").write_text(
                json.dumps(report, ensure_ascii=False, separators=(",", ":")) + "\n",
                encoding="utf-8",
            )
            lines = [
                "# Battle Skill Timeline Browser Smoke",
                "",
                f"- status: `{report['status']}`",
                f"- checked rows: {report['checkedRows']}",
                f"- checked effect rows: {report['checkedEffectRows']}",
                f"- checked execution-requirement rows: {report['checkedExecutionRequirementRows']}",
                f"- checked execution-requirement buckets: {', '.join(report['checkedExecutionRequirementBuckets'])}",
                "- coverage: raw actor filmstrip, playback filmstrip, canonical-ready marker, known regression skills",
            ]
            for row in report["checks"]:
                suffix = f" Lv{row['levelOrFixed']}" if row["levelOrFixed"] is not None else ""
                lines.append(f"- {row['ownerKey']} {row['skillIdHex']} {row['name']}{suffix}: raw {row['rawFrames']} / playback cards {row['playbackFrameCount']}")
            lines.append("")
            lines.append("## Effect Smoke")
            for row in report["effectChecks"]:
                suffix = f" Lv{row['levelOrFixed']}" if row["levelOrFixed"] is not None else ""
                lines.append(f"- {row['ownerKey']} {row['skillIdHex']} {row['name']}{suffix}: effect frames {row['effectFrameCount']} / playback cards {row['playbackFrameCount']}")
            lines.append("")
            lines.append("## Execution Requirement Smoke")
            lines.append("")
            lines.append(
                "Every row with an EXE-derived helper execution requirement is selectable in the browser "
                "and exposes its requirement bucket through the page marker."
            )
            for row in report["executionRequirementChecks"][:20]:
                suffix = f" Lv{row['levelOrFixed']}" if row["levelOrFixed"] is not None else ""
                reqs = ", ".join(row["requirements"])
                lines.append(f"- {row['ownerKey']} {row['skillIdHex']} {row['name']}{suffix}: {reqs}; effect frames {row['effectFrameCount']}")
            if len(report["executionRequirementChecks"]) > 20:
                lines.append(f"- ... {len(report['executionRequirementChecks']) - 20} more rows in JSON")
            (OUT / "battle_skill_timeline_browser_smoke.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
            print(json.dumps({k: report[k] for k in ("status", "checkedRows", "checkedEffectRows", "checkedExecutionRequirementRows")}, ensure_ascii=False))
            return report
        finally:
            if session_id:
                try:
                    request_json(driver_port, "DELETE", f"/session/{session_id}", timeout=5)
                except Exception:
                    pass
            proc.terminate()
            try:
                proc.wait(timeout=3)
            except subprocess.TimeoutExpired:
                proc.kill()
            if not keep_log:
                try:
                    log_path.unlink()
                except FileNotFoundError:
                    pass


def verify_with_managed_server(keep_log: bool = False) -> dict:
    server_port = free_port()
    server_log_path = OUT / "battle_skill_timeline_browser_server.log"
    OUT.mkdir(parents=True, exist_ok=True)
    with server_log_path.open("wb") as log:
        proc = subprocess.Popen(
            [
                sys.executable,
                str(ROOT / "tools" / "serve_utf8.py"),
                str(server_port),
                "--bind",
                "127.0.0.1",
                "--directory",
                str(ROOT),
            ],
            cwd=ROOT,
            stdout=log,
            stderr=subprocess.STDOUT,
        )
        try:
            base = f"http://127.0.0.1:{server_port}"
            wait_for_http(base + "/web/battle_skill_timeline_review.html", proc)
            return verify_browser(base, keep_log=keep_log)
        finally:
            proc.terminate()
            try:
                proc.wait(timeout=3)
            except subprocess.TimeoutExpired:
                proc.kill()
            if not keep_log:
                try:
                    server_log_path.unlink()
                except FileNotFoundError:
                    pass


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--base", default="", help="Existing static server base URL. If omitted, a temporary serve_utf8 server is started.")
    parser.add_argument("--keep-log", action="store_true")
    args = parser.parse_args()
    if args.base:
        verify_browser(args.base, keep_log=args.keep_log)
    else:
        verify_with_managed_server(keep_log=args.keep_log)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
