#!/usr/bin/env python3
"""Run a focused WebKit smoke for the map3_09a runtime surface."""
from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import time
from pathlib import Path
from urllib.parse import urlencode, urljoin

from verify_mobile_browser_controls import (
    WebDriverError,
    canvas_checksum_script,
    execute_js,
    free_port,
    request_json,
    wait_for_driver,
    wait_for_map_runtime,
    wait_for_page,
)


ROOT = Path(__file__).resolve().parents[1]
MAP_NAME = "map3_09a"
START_TILE = {"x": 76, "y": 18}
MOVE_TARGET_TILE = {"x": 77, "y": 18}


def load_map(base: str, port: int, session_id: str) -> str:
    query = urlencode({
        "map": MAP_NAME,
        "startTile": f"{START_TILE['x']},{START_TILE['y']}",
        "collision": "1",
        "overview": "1",
        "moveMs": "192",
        "_": str(time.time_ns()),
    })
    url = urljoin(base.rstrip("/") + "/", f"/web/game.html?{query}")
    request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
    wait_for_page(port, session_id)
    wait_for_map_runtime(port, session_id, MAP_NAME)
    return url


def map3_runtime_script() -> str:
    return f"""
const startTile = {json.dumps(START_TILE)};
const rightTarget = {json.dumps(MOVE_TARGET_TILE)};
    const movementDirections = [
      {{ name: "left", code: "ArrowLeft", dir: 1, dx: -1, dy: 0 }},
      {{ name: "up", code: "ArrowUp", dir: 3, dx: 0, dy: -1 }},
      {{ name: "down", code: "ArrowDown", dir: 0, dx: 0, dy: 1 }},
      {{ name: "right", code: "ArrowRight", dir: 2, dx: 1, dy: 0 }},
    ];
activeDialogue = null;
menuOpen = false;
runtimeState = createPrototypeRuntimeState();
fieldEncounterState = createFieldEncounterState({{ enabled: false }});
lastMovementAction = null;
pendingMovementAction = null;
virtualMovementAction = null;
player.dir = 2;
player.walkScriptPhase = 0;
player.frame = PLAYER_IDLE_FRAME;
player.step = null;
stepSoundAt = -1000;
window.HWANSE_SOUND_LOG = [];
window.HWANSE_SOUND_COUNTS = {{}};
window.HWANSE_LAST_SOUND = null;
window.HWANSE_FIELD_MOVEMENT_SOUND_LOG = [];
window.HWANSE_LAST_FIELD_MOVEMENT_SOUND = null;
window.HWANSE_MOVEMENT_BUMP_LOG = [];
window.HWANSE_MOVEMENT_BUMP_RENDER = [];
window.HWANSE_LAST_MOVEMENT_BUMP_EFFECT = null;
window.HWANSE_LAST_MOVEMENT_BUMP_RENDER = null;
window.HWANSE_MOVEMENT_BUMP_SOUND_LOG = [];
window.HWANSE_LAST_MOVEMENT_BUMP_SOUND = null;
activeMovementBumpEffect = null;

function compactTile(tile) {{
  return tile ? {{ x: tile.x, y: tile.y }} : null;
}}

    function tileInfo(tile) {{
      return {{
    x: tile.x,
    y: tile.y,
    layer0: layerTile(0, tile.x, tile.y),
    layer1: layerTile(1, tile.x, tile.y),
    standableOriginal: canStandOnTileWithMode(tile.x, tile.y, "originalLayer1Flags"),
    standableClass: canStandOnTileWithMode(tile.x, tile.y, "tileClassLayer0"),
      }};
    }}

    function targetTileForDirection(tile, direction) {{
      return {{
        x: clamp(tile.x + direction.dx, 0, map.width - 1),
        y: clamp(tile.y + direction.dy, 0, map.height - 1),
      }};
    }}

    function blockedAttemptFromTile(tile) {{
      for (const direction of movementDirections) {{
        const targetTile = targetTileForDirection(tile, direction);
        if (targetTile.x === tile.x && targetTile.y === tile.y) continue;
        if (!movementTargetFromTile(tile, direction.dx, direction.dy) && !chooseMovementStep(tile, direction.dx, direction.dy)) {{
          return {{ tile, ...direction, targetTile }};
        }}
      }}
      return null;
    }}

    function blockedAttemptInMap() {{
      for (let y = 0; y < map.height; y += 1) {{
        for (let x = 0; x < map.width; x += 1) {{
          const tile = {{ x, y }};
          if (!canStandOnTileWithMode(x, y, "originalLayer1Flags")) continue;
          const attempt = blockedAttemptFromTile(tile);
          if (attempt) return attempt;
        }}
      }}
      return null;
    }}

function sendKey(code) {{
  const down = new KeyboardEvent("keydown", {{
    code,
    key: code,
    bubbles: true,
    cancelable: true,
  }});
  window.dispatchEvent(down);
  update(0);
  const stepAfterInput = player.step ? {{
    fromX: player.step.fromX,
    fromY: player.step.fromY,
    toX: player.step.toX,
    toY: player.step.toY,
    elapsed: player.step.elapsed,
    duration: player.step.duration,
  }} : null;
  const up = new KeyboardEvent("keyup", {{
    code,
    key: code,
    bubbles: true,
    cancelable: true,
  }});
  window.dispatchEvent(up);
  return {{ defaultPrevented: down.defaultPrevented, stepAfterInput }};
}}

    function finishStep() {{
      for (let index = 0; index < 4; index += 1) update(ORIGINAL_FRAME_SECONDS);
      update(0);
      if (typeof render === "function") render();
    }}

    function clearMovementInputState() {{
      if (keyboardState?.clear) keyboardState.clear();
      lastMovementAction = null;
      pendingMovementAction = null;
      virtualMovementAction = null;
      scriptedMovementAction = null;
      scriptedMovementInputSource = "";
      activeMovementInputSource = "";
    }}

    setPlayerPosition(playerPositionForTile(startTile.x, startTile.y).x, playerPositionForTile(startTile.x, startTile.y).y);
    clearMovementInputState();
    updateCamera();
const beforeMoveFoot = compactTile(footTile());
const rightProbe = movementTargetFromTile(beforeMoveFoot, 1, 0);
const leftProbe = movementTargetFromTile(beforeMoveFoot, -1, 0);
const keyboardRight = sendKey("ArrowRight");
const movementSoundAfterInput = window.HWANSE_LAST_FIELD_MOVEMENT_SOUND || null;
finishStep();
const afterMoveFoot = compactTile(footTile());

    const blockedAttempt = blockedAttemptInMap();
    let beforeBumpFoot = null;
    let afterBumpFoot = null;
    let blockedProbe = null;
    let keyboardBlocked = null;
    let bumpEffect = null;
    let bumpRender = null;
    let bumpSound = null;
    if (blockedAttempt) {{
      const blockedStartPosition = playerPositionForTile(blockedAttempt.tile.x, blockedAttempt.tile.y);
      setPlayerPosition(blockedStartPosition.x, blockedStartPosition.y);
      updateCamera();
      clearMovementInputState();
      player.dir = blockedAttempt.dir;
      player.walkScriptPhase = 0;
      player.frame = PLAYER_IDLE_FRAME;
      player.step = null;
      activeMovementBumpEffect = null;
      beforeBumpFoot = compactTile(footTile());
      blockedProbe = movementTargetFromTile(beforeBumpFoot, blockedAttempt.dx, blockedAttempt.dy);
      keyboardBlocked = sendKey(blockedAttempt.code);
      if (typeof render === "function") render();
      afterBumpFoot = compactTile(footTile());
      bumpEffect = window.HWANSE_LAST_MOVEMENT_BUMP_EFFECT || null;
      bumpRender = window.HWANSE_LAST_MOVEMENT_BUMP_RENDER || null;
      bumpSound = window.HWANSE_LAST_MOVEMENT_BUMP_SOUND || null;
    }}

const renderReview = acceptedMapRenderReview(map);
const layoutReview = acceptedMapLayoutReview(map);
return {{
  status: "passed",
  scene,
  map: map?.name || "",
  width: map?.width || 0,
  height: map?.height || 0,
  tileSize: map?.tileSize || 0,
  layerTilesets: [...(map?.layerTilesets || [])],
  sceneTilesets: [...(map?.sceneTilesets || [])],
  observedSceneTilesets: [...(map?.observedSceneTilesets || [])],
  mapChunkLoaded: Boolean(window.HWANSE_MAPS && window.HWANSE_MAPS[{json.dumps(MAP_NAME)}]),
  mapIndexLoaded: Boolean(window.HWANSE_MAP_INDEX && window.HWANSE_MAP_INDEX[{json.dumps(MAP_NAME)}]),
  visualMode,
  tileIndexMode,
  layoutMode,
  collisionMode,
  renderReview: renderReview ? {{
    map: renderReview.map,
    visual: renderReview.visual,
    tileIndexMode: renderReview.tileIndexMode,
    state: renderReview.state,
    familyDefault: renderReview.familyDefault === true,
    tilesets: [...(renderReview.tilesets || [])],
  }} : null,
  renderReviewState: mapRenderReviewState(map),
  layoutReview: layoutReview ? {{
    map: layoutReview.map,
    layout: layoutReview.layout,
    state: layoutReview.state,
    tilesets: [...(layoutReview.tilesets || [])],
  }} : null,
  layoutReviewState: mapLayoutReviewState(map),
      startTile,
      moveTargetTile: rightTarget,
      bumpStatus: blockedAttempt ? "checked" : "no-blocked-attempt-in-map",
      bumpStartTile: blockedAttempt?.tile || null,
      blockedTargetTile: blockedAttempt?.targetTile || null,
      blockedDirection: blockedAttempt ? {{
        name: blockedAttempt.name,
        code: blockedAttempt.code,
        dx: blockedAttempt.dx,
        dy: blockedAttempt.dy,
      }} : null,
  tileInfo: {{
    start: tileInfo(startTile),
    moveTarget: tileInfo(rightTarget),
        blockedTarget: blockedAttempt ? tileInfo(blockedAttempt.targetTile) : null,
  }},
  beforeMoveFoot,
  afterMoveFoot,
  rightProbe: rightProbe ? {{ nextTile: compactTile(rightProbe.nextTile), target: rightProbe.target }} : null,
      leftProbe: leftProbe ? {{ nextTile: compactTile(leftProbe.nextTile), target: leftProbe.target }} : null,
      blockedProbe: blockedProbe ? {{ nextTile: compactTile(blockedProbe.nextTile), target: blockedProbe.target }} : null,
  keyboardRight,
  movementSoundAfterInput,
  fieldMovementSoundLog: window.HWANSE_FIELD_MOVEMENT_SOUND_LOG || [],
  beforeBumpFoot,
  afterBumpFoot,
      keyboardBlocked,
  bumpEffect,
  bumpRender,
  bumpSound,
  bumpLogLength: (window.HWANSE_MOVEMENT_BUMP_LOG || []).length,
  bumpRenderLength: (window.HWANSE_MOVEMENT_BUMP_RENDER || []).length,
  bumpSoundLogLength: (window.HWANSE_MOVEMENT_BUMP_SOUND_LOG || []).length,
  soundCounts: {{ ...(window.HWANSE_SOUND_COUNTS || {{}}) }},
  search: location.search,
  originalRoutePromotionImplemented: false,
  originalObjectScriptCollisionRuntimeImplemented: false,
  originalStoryFlagRuntimeImplemented: false,
}};
"""


def verify_state(state: dict) -> None:
    render_review = state.get("renderReview") or {}
    layout_review = state.get("layoutReview") or {}
    right_probe = state.get("rightProbe") or {}
    keyboard_right = state.get("keyboardRight") or {}
    keyboard_blocked = state.get("keyboardBlocked") or {}
    movement_sound = state.get("movementSoundAfterInput") or {}
    bump_effect = state.get("bumpEffect") or {}
    bump_render = state.get("bumpRender") or {}
    bump_sound = state.get("bumpSound") or {}
    tile_info = state.get("tileInfo") or {}
    start_info = tile_info.get("start") or {}
    target_info = tile_info.get("moveTarget") or {}
    blocked_info = tile_info.get("blockedTarget") or {}
    bump_start = state.get("bumpStartTile") or {}
    blocked_target = state.get("blockedTargetTile") or {}
    bump_status = state.get("bumpStatus")

    if (
        state.get("status") != "passed"
        or state.get("scene") != "map"
        or state.get("map") != MAP_NAME
        or state.get("width") != 96
        or state.get("height") != 64
        or state.get("tileSize") != 16
        or state.get("layerTilesets") != ["map_a1", "map_a2"]
        or state.get("sceneTilesets") != ["map_a1", "map_a2"]
        or state.get("mapChunkLoaded") is not True
        or state.get("mapIndexLoaded") is not True
        or state.get("visualMode") != "layer1UnderSceneTilesets"
        or state.get("tileIndexMode") != "row40_zero"
        or state.get("layoutMode") != "planar_layers"
        or state.get("collisionMode") != "originalLayer1Flags"
        or state.get("renderReviewState") != "accepted"
        or render_review.get("map") != MAP_NAME
        or render_review.get("visual") != "layer1UnderSceneTilesets"
        or render_review.get("tileIndexMode") != "row40_zero"
        or render_review.get("state") != "accepted"
        or render_review.get("tilesets") != ["map_a1", "map_a2"]
        or state.get("layoutReviewState") != "layout accepted"
        or layout_review.get("map") != MAP_NAME
        or layout_review.get("layout") != "planar_layers"
        or layout_review.get("state") != "accepted"
        or state.get("beforeMoveFoot") != START_TILE
        or state.get("afterMoveFoot") != MOVE_TARGET_TILE
        or (right_probe.get("nextTile") or {}) != MOVE_TARGET_TILE
        or bump_status not in {"checked", "no-blocked-attempt-in-map"}
        or keyboard_right.get("defaultPrevented") is not True
        or not keyboard_right.get("stepAfterInput")
        or movement_sound.get("source") != "field-movement-sound"
        or movement_sound.get("soundKey") != "step"
        or movement_sound.get("soundSrc") != "../extract_wlk/00.wav"
        or movement_sound.get("soundPlayed") is not True
        or movement_sound.get("browserFieldMovementSoundImplemented") is not True
        or (state.get("soundCounts") or {}).get("step") != 1
        or start_info.get("layer0") != 251
        or start_info.get("layer1") != 32
        or target_info.get("x") != 77
        or target_info.get("y") != 18
        or "map=map3_09a" not in str(state.get("search") or "")
        or state.get("originalRoutePromotionImplemented") is not False
        or state.get("originalObjectScriptCollisionRuntimeImplemented") is not False
        or state.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"map3 runtime smoke is incomplete: {state!r}")

    if bump_status == "checked":
        if (
            state.get("blockedProbe") is not None
            or not bump_start
            or not blocked_target
            or blocked_target == bump_start
            or blocked_target == MOVE_TARGET_TILE
            or state.get("beforeBumpFoot") != bump_start
            or state.get("afterBumpFoot") != bump_start
            or keyboard_blocked.get("defaultPrevented") is not True
            or keyboard_blocked.get("stepAfterInput") is not None
            or bump_effect.get("source") != "movement-collision"
            or bump_effect.get("reason") != "collision"
            or bump_effect.get("map") != MAP_NAME
            or bump_effect.get("fromTile") != bump_start
            or bump_effect.get("targetTile") != blocked_target
            or bump_effect.get("browserMovementBumpFeedbackImplemented") is not True
            or bump_render.get("source") != "movement-collision"
            or bump_render.get("map") != MAP_NAME
            or bump_render.get("targetTile") != blocked_target
            or bump_render.get("browserMovementBumpFeedbackImplemented") is not True
            or bump_sound.get("source") != "movement-bump-sound"
            or bump_sound.get("soundKey") != "menuMove"
            or bump_sound.get("soundSrc") != "../extract_wlk/03.wav"
            or bump_sound.get("soundPlayed") is not True
            or bump_sound.get("browserMovementBumpSoundImplemented") is not True
            or state.get("bumpLogLength", 0) < 1
            or state.get("bumpRenderLength", 0) < 1
            or state.get("bumpSoundLogLength", 0) < 1
            or (state.get("soundCounts") or {}).get("menuMove") != 1
            or blocked_info.get("x") != blocked_target.get("x")
            or blocked_info.get("y") != blocked_target.get("y")
        ):
            raise WebDriverError(f"map3 runtime bump smoke is incomplete: {state!r}")
    else:
        if (
            state.get("blockedProbe") is not None
            or state.get("keyboardBlocked") is not None
            or state.get("bumpEffect") is not None
            or state.get("bumpLogLength", 0) != 0
            or (state.get("soundCounts") or {}).get("menuMove", 0) != 0
        ):
            raise WebDriverError(f"map3 runtime bump skip is inconsistent: {state!r}")


def write_report(report: dict) -> None:
    out_dir = ROOT / "out"
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "map3_runtime_browser_smoke.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    lines = [
        "# map3_09a Runtime Browser Smoke",
        "",
        f"- status: `{report.get('status')}`",
        f"- runtime: `{report.get('runtime')}`",
        f"- movement: `{report.get('movement')}`",
        f"- bump: `{report.get('bump')}`",
        f"- checksum: `{report.get('checksum')}`",
        "",
    ]
    (out_dir / "map3_runtime_browser_smoke.md").write_text("\n".join(lines), encoding="utf-8")


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

    port = free_port()
    log_path = ROOT / "out" / "map3_runtime_webkitdriver.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    with log_path.open("wb") as log:
        proc = subprocess.Popen(
            [
                driver_path,
                "--host=127.0.0.1",
                f"--port={port}",
                "--replace-on-new-session",
            ],
            stdout=log,
            stderr=subprocess.STDOUT,
        )
        session_id = ""
        try:
            wait_for_driver(port, proc)
            session = request_json(
                port,
                "POST",
                "/session",
                {"capabilities": {"alwaysMatch": {"browserName": "MiniBrowser"}}},
                timeout=30,
            )
            session_id = str(session["value"]["sessionId"])
            request_json(
                port,
                "POST",
                f"/session/{session_id}/window/rect",
                {"x": 0, "y": 0, "width": 390, "height": 844},
                timeout=8,
            )
            url = load_map(base, port, session_id)
            state = execute_js(port, session_id, map3_runtime_script(), timeout=8)
            verify_state(state)
            checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
            if checksum == 0:
                raise WebDriverError("map3 runtime smoke rendered a blank canvas")
            before_bump = state.get("beforeBumpFoot") or {}
            bump_effect = state.get("bumpEffect") or {}
            bump_target = bump_effect.get("targetTile") or {}
            bump_sound = state.get("bumpSound") or {}
            report = {
                "status": "passed",
                "base": base,
                "url": url,
                "runtime": (
                    f"map={state.get('map')} "
                    f"size={state.get('width')}x{state.get('height')} "
                    f"tilesets={','.join(state.get('layerTilesets') or [])} "
                    f"render={((state.get('renderReview') or {}).get('visual'))}/"
                    f"{((state.get('renderReview') or {}).get('tileIndexMode'))}/"
                    f"{((state.get('renderReview') or {}).get('state'))} "
                    f"layout={((state.get('layoutReview') or {}).get('layout'))}/"
                    f"{((state.get('layoutReview') or {}).get('state'))} "
                    f"collision={state.get('collisionMode')} "
                    f"chunk={state.get('mapChunkLoaded')}"
                ),
                "movement": (
                    f"start={state.get('beforeMoveFoot', {}).get('x')},{state.get('beforeMoveFoot', {}).get('y')} "
                    f"right={state.get('afterMoveFoot', {}).get('x')},{state.get('afterMoveFoot', {}).get('y')} "
                    f"stepSound={(state.get('movementSoundAfterInput') or {}).get('soundKey')}:"
                    f"{(state.get('movementSoundAfterInput') or {}).get('soundSrc')}:"
                    f"{(state.get('movementSoundAfterInput') or {}).get('soundPlayed')}"
                ),
                "bump": (
                    f"status={state.get('bumpStatus')} "
                    f"from={before_bump.get('x')},{before_bump.get('y')} "
                    f"target={bump_target.get('x')},"
                    f"{bump_target.get('y')} "
                    f"reason={bump_effect.get('reason')} "
                    f"render={bool(state.get('bumpRender'))} "
                    f"bumpSound={bump_sound.get('soundKey')}:"
                    f"{bump_sound.get('soundSrc')}:"
                    f"{bump_sound.get('soundPlayed')} "
                    f"originalRoutePromotionImplemented={state.get('originalRoutePromotionImplemented')}"
                ),
                "checksum": checksum,
                "snapshot": state,
            }
            write_report(report)
            print(
                "ok map3 runtime browser "
                f"runtime={report['runtime']} movement={report['movement']} "
                f"bump={report['bump']} checksum={checksum}"
            )
        finally:
            if session_id:
                try:
                    request_json(port, "DELETE", f"/session/{session_id}", timeout=5)
                except Exception:
                    pass
            proc.terminate()
            try:
                proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                proc.kill()
                proc.wait(timeout=5)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base", default="http://127.0.0.1:8013")
    args = parser.parse_args()
    verify_browser(args.base)


if __name__ == "__main__":
    main()
