#!/usr/bin/env python3
"""Smoke-test resource_reference_review.html resource links/previews through WebKit."""
from __future__ import annotations

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

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


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


def state_script() -> str:
    return """
const marker = window.HWANSE_LAST_RESOURCE_REFERENCE_REVIEW || null;
const cara = document.querySelector('[data-resource-link][data-resource-name="cara_01"]');
const mapB1 = document.querySelector('[data-resource-link][data-resource-name="map_b1"]');
const mapPreview = document.querySelector('[data-map-preview-link][data-map-name="map1_02b"]') || document.querySelector('[data-map-preview-link]');
const missingMapPreview = document.querySelector('[data-status="missing"] [data-map-preview-link]');
const battlePreview = document.querySelector('[data-battle-preview-link][data-battle-name="btl_b1"]') || document.querySelector('[data-battle-preview-link]');
const sceneScript = document.querySelector('[data-map-preview-link][data-map-name="map1_02b"]')?.closest('[data-resource-row]')?.querySelector('[data-scene-script-link]') || document.querySelector('[data-scene-script-link]');
const map102bRow = document.querySelector('[data-map-preview-link][data-map-name="map1_02b"]')?.closest('[data-resource-row]');
const map102bTileUsage = map102bRow?.querySelector('[data-tile-usage-summary]');
const map102bExtraRect = map102bRow?.querySelector('[data-scene-extra-resource-rect][data-tileset="map_b3"]');
const popover = document.getElementById('resourcePreviewPopover');
const scenePopover = document.getElementById('sceneScriptPopover');
const image = document.getElementById('resourcePreviewImage');
const canvas = document.getElementById('resourcePreviewCanvas');
return {
  readyState: document.readyState,
  href: location.href,
  marker,
  rowCount: document.querySelectorAll('[data-resource-row]').length,
  resourceLinkCount: document.querySelectorAll('[data-resource-link]').length,
  mapPreviewLinkCount: document.querySelectorAll('[data-map-preview-link]').length,
  battlePreviewLinkCount: document.querySelectorAll('[data-battle-preview-link]').length,
  sceneScriptLinkCount: document.querySelectorAll('[data-scene-script-link]').length,
  caraHref: cara?.getAttribute('href') || '',
  caraPreview: cara?.dataset.previewSrc || '',
  mapB1Href: mapB1?.getAttribute('href') || '',
  mapB1CnsHref: mapB1?.dataset.cnsHref || '',
  mapPreviewHref: mapPreview?.getAttribute('href') || '',
  mapPreviewName: mapPreview?.dataset.mapName || '',
  missingMapPreviewHref: missingMapPreview?.getAttribute('href') || '',
  missingMapPreviewName: missingMapPreview?.dataset.mapName || '',
  battlePreviewHref: battlePreview?.getAttribute('href') || '',
  battlePreviewName: battlePreview?.dataset.battleName || '',
  sceneScriptHref: sceneScript?.getAttribute('href') || '',
  sceneScriptTitle: sceneScript?.dataset.sceneScriptTitle || '',
  sceneScriptMeta: sceneScript?.dataset.sceneScriptMeta || '',
  sceneScriptSample: sceneScript?.dataset.sceneScriptSample || '',
  tileUsageSummaryCount: document.querySelectorAll('[data-tile-usage-summary]').length,
  tileIndexLinkCount: document.querySelectorAll('.tile-index-link').length,
  map102bTileUsageText: map102bTileUsage?.textContent || '',
  map102bExtraRectText: map102bExtraRect?.textContent || '',
  popoverHidden: popover?.getAttribute('aria-hidden') || '',
  previewImageSrc: image?.getAttribute('src') || '',
  previewImageHidden: image?.hidden ?? null,
  previewCanvasHidden: canvas?.hidden ?? null,
  previewCanvasWidth: canvas?.width || 0,
  previewCanvasHeight: canvas?.height || 0,
  previewTitle: document.getElementById('resourcePreviewTitle')?.textContent || '',
  scenePopoverHidden: scenePopover?.getAttribute('aria-hidden') || '',
  scenePopoverTitle: document.getElementById('sceneScriptPopoverTitle')?.textContent || '',
  scenePopoverMeta: document.getElementById('sceneScriptPopoverMeta')?.textContent || '',
  scenePopoverText: document.getElementById('sceneScriptPopoverText')?.textContent || '',
};
"""


def wait_for_review(port: int, session_id: str, timeout: float = 15, predicate=None) -> dict:
    deadline = time.monotonic() + timeout
    last_state = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, state_script(), timeout=3)
        last_state = state
        marker = state.get("marker") or {}
        if (
            state.get("readyState") == "complete"
            and marker.get("browserResourceReferenceReviewImplemented") is True
            and marker.get("resourceHoverPreviewImplemented") is True
            and (predicate is None or predicate(state))
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"resource reference review did not become ready: {last_state!r}")


def write_report(payload: dict) -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "resource_reference_review_browser_smoke.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def verify_browser(base: str, keep_log: bool = False) -> 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 = OUT / "resource_reference_review_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 or {}).get("value", {}).get("sessionId") or "")
            if not session_id:
                raise WebDriverError(f"could not create WebKit session: {session!r}")
            request_json(
                port,
                "POST",
                f"/session/{session_id}/window/rect",
                {"x": 0, "y": 0, "width": 1280, "height": 900},
                timeout=8,
            )

            url = urljoin(base.rstrip("/") + "/", "/web/resource_reference_review.html")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            initial = wait_for_review(
                port,
                session_id,
                predicate=lambda state: state.get("resourceLinkCount", 0) > 100,
            )
            marker = initial.get("marker") or {}
            if (
                initial.get("rowCount") != marker.get("resourceReferenceTableRowCount")
                or initial.get("resourceLinkCount") != marker.get("resourceLinkCount")
                or initial.get("mapPreviewLinkCount") != marker.get("mapPreviewLinkCount")
                or initial.get("battlePreviewLinkCount") != marker.get("battleBackgroundPreviewLinkCount")
                or initial.get("sceneScriptLinkCount") != marker.get("sceneScriptLinkCount")
                or initial.get("sceneScriptLinkCount", 0) <= 0
                or "field_character_review.html?asset=cara_01" not in initial.get("caraHref", "")
                or not initial.get("caraPreview", "").endswith("/extract_fld/cara_01.cns")
                or "map_review.html?map=" not in initial.get("mapB1Href", "")
                or "tileset=map_b1" not in initial.get("mapB1Href", "")
                or not initial.get("mapB1CnsHref", "").endswith("/extract_fld/map_b1.cns")
                or "map_review.html?map=" not in initial.get("mapPreviewHref", "")
                or "battle_background_review.html?background=" not in initial.get("battlePreviewHref", "")
                or "../out/scene_script_player.html?" not in initial.get("sceneScriptHref", "")
                or not initial.get("sceneScriptSample", "")
                or marker.get("resourceLabelReviewLinksImplemented") is not True
                or marker.get("mapHoverPreviewImplemented") is not True
                or marker.get("battleBackgroundHoverPreviewImplemented") is not True
                or marker.get("sceneScriptHoverPreviewImplemented") is not True
                or marker.get("sceneScriptPlayerHref") != "../out/scene_script_player.html"
                or marker.get("tileUsageSummaryImplemented") is not True
                or marker.get("compactTileUsageSummaryImplemented") is not True
                or marker.get("sceneExtraResourceRectHintsImplemented") is not True
                or marker.get("sceneExtraSourceRectTablePatternImplemented") is not True
                or marker.get("sceneExtraMapPreviewPlacementRendererImplemented") is not True
                or marker.get("tilesetLayerGridUsageStatus") != "layer-tile-indices-confirmed-scene-extra-rect-hints-partial"
                or marker.get("sceneExtraTilesetGridStatus") != "source-rect-exe-pointer-table-active-object-bounds-partial"
                or marker.get("confirmedSceneExtraResourceRectCount") != 25
                or marker.get("confirmedSceneExtraPlacementCount") != 25
                or marker.get("inferredSceneExtraPlacementCount") != 0
                or marker.get("missingManifestMapPreviewLinkCount") != marker.get("missingManifestMapCount")
                or not initial.get("missingMapPreviewHref", "").startswith("map_review.html?map=")
                or marker.get("sourceOnlySceneExtraResourceRectCount", 0) < 20
                or marker.get("battleBackgroundCandidateStatus") != "family-derived-candidate-no-original-encounter-proof"
                or marker.get("battleBackgroundDirectBindingCount", marker.get("directBattleBackgroundBindingCount")) != 0
                or marker.get("resourcePreviewSourcePattern") != "../extract_fld/<stem>.cns rendered by web/engine/cns/renderer.js"
                or marker.get("resourceReviewLinkPattern") != "kind-specific review page; CNS kept in data-cns-href for hover rendering"
                or marker.get("mapPreviewSource") != "out/maps.js"
                or marker.get("battleBackgroundPreviewSource") != "out/battle_backgrounds.js"
                or initial.get("tileUsageSummaryCount", 0) < 170
                or initial.get("tileIndexLinkCount") != 0
                or "map_b3" not in initial.get("map102bTileUsageText", "")
                or "door R01-01" not in initial.get("map102bExtraRectText", "")
                or "source rect 0,0,48,96" not in initial.get("map102bExtraRectText", "")
                or "dest 240,128" not in initial.get("map102bExtraRectText", "")
                or "tile 15,8" not in initial.get("map102bExtraRectText", "")
                or "layer0 #119" not in initial.get("map102bExtraRectText", "")
                or "user-confirmed-prior" not in initial.get("map102bExtraRectText", "")
                or "0x00500ce8" not in initial.get("map102bExtraRectText", "")
            ):
                raise WebDriverError(f"resource link state mismatch: {initial!r}")

            execute_js(
                port,
                session_id,
                """
const link = document.querySelector('[data-resource-link][data-resource-name="cara_01"]');
link.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 120, clientY: 160 }));
return true;
""",
                timeout=3,
            )
            hover = wait_for_review(
                port,
                session_id,
                predicate=lambda state: (
                    state.get("popoverHidden") == "false"
                    and state.get("previewTitle", "").startswith("cara_01.cns")
                    and state.get("previewCanvasHidden") is False
                    and state.get("previewCanvasWidth", 0) > 1
                ),
            )
            if hover.get("previewCanvasHidden") is not False or hover.get("previewCanvasWidth", 0) <= 1:
                raise WebDriverError(f"resource hover preview mismatch: {hover!r}")

            execute_js(
                port,
                session_id,
                """
const link = document.querySelector('[data-map-preview-link][data-map-name="map1_02b"]') || document.querySelector('[data-map-preview-link]');
link.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 180, clientY: 220 }));
return link.dataset.mapName;
""",
                timeout=3,
            )
            map_hover = wait_for_review(
                port,
                session_id,
                predicate=lambda state: (
                    state.get("popoverHidden") == "false"
                    and state.get("previewTitle", "").startswith("map1_02b · map")
                    and "scene-extra 1개 합성" in state.get("previewTitle", "")
                    and state.get("previewCanvasHidden") is False
                    and state.get("previewCanvasWidth", 0) > 0
                    and state.get("previewCanvasHeight", 0) > 0
                ),
            )

            execute_js(
                port,
                session_id,
                """
const link = document.querySelector('[data-status="missing"] [data-map-preview-link]');
link.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 200, clientY: 240 }));
return link.dataset.mapName;
""",
                timeout=3,
            )
            missing_map_hover = wait_for_review(
                port,
                session_id,
                predicate=lambda state: (
                    state.get("popoverHidden") == "false"
                    and state.get("previewTitle", "").startswith((state.get("missingMapPreviewName") or "") + " · map")
                    and state.get("previewCanvasHidden") is False
                    and state.get("previewCanvasWidth", 0) > 0
                    and state.get("previewCanvasHeight", 0) > 0
                ),
            )

            execute_js(
                port,
                session_id,
                """
const link = document.querySelector('[data-battle-preview-link][data-battle-name="btl_b1"]') || document.querySelector('[data-battle-preview-link]');
link.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 220, clientY: 260 }));
return link.dataset.battleName;
""",
                timeout=3,
            )
            battle_hover = wait_for_review(
                port,
                session_id,
                predicate=lambda state: (
                    state.get("popoverHidden") == "false"
                    and state.get("previewTitle", "").endswith("· family 후보")
                    and state.get("previewCanvasHidden") is False
                    and state.get("previewCanvasWidth", 0) > 0
                    and state.get("previewCanvasHeight", 0) > 0
                ),
            )

            execute_js(
                port,
                session_id,
                """
const link = document.querySelector('[data-map-preview-link][data-map-name="map1_02b"]')?.closest('[data-resource-row]')?.querySelector('[data-scene-script-link]') || document.querySelector('[data-scene-script-link]');
link.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 260, clientY: 300 }));
return link.dataset.sceneSequenceId;
""",
                timeout=3,
            )
            scene_hover = wait_for_review(
                port,
                session_id,
                predicate=lambda state: (
                    state.get("scenePopoverHidden") == "false"
                    and bool(state.get("scenePopoverTitle"))
                    and bool(state.get("scenePopoverText"))
                    and "prompts" in state.get("scenePopoverMeta", "")
                ),
            )

            payload = {
                "status": "passed",
                "initial": initial,
                "hover": hover,
                "mapHover": map_hover,
                "missingMapHover": missing_map_hover,
                "battleHover": battle_hover,
                "sceneHover": scene_hover,
            }
            write_report(payload)
            print(
                "ok resource_reference_review "
                f"rows={initial.get('rowCount')} "
                f"links={initial.get('resourceLinkCount')} "
                f"resource={hover.get('previewTitle')} "
                f"map={map_hover.get('previewTitle')} "
                f"missingMap={missing_map_hover.get('previewTitle')} "
                f"battle={battle_hover.get('previewTitle')} "
                f"scene={scene_hover.get('scenePopoverTitle')}"
            )
        finally:
            if session_id:
                try:
                    request_json(port, "DELETE", f"/session/{session_id}", None, timeout=5)
                except Exception:
                    pass
            proc.terminate()
            try:
                proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                proc.kill()
            if not keep_log and log_path.exists():
                log_path.unlink()


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


if __name__ == "__main__":
    main()
