#!/usr/bin/env python3
"""Smoke-test web/item_equipment_skill_static.html in 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 """
return {
  readyState: document.readyState,
  marker: window.HWANSE_LAST_ITEM_EQUIPMENT_SKILL_STATIC || null,
  readyMarker: window.HWANSE_ITEM_EQUIPMENT_SKILL_STATIC_READY === true,
  activeTab: document.querySelector('#tabs button.active')?.dataset.view || '',
  visibleCount: document.getElementById('visibleCount')?.textContent || '',
  staticJudgementCardCount: document.querySelectorAll('[data-static-judgement-panel] .status-card').length,
  rowCount: document.querySelectorAll('#tableBody tr').length,
  skillLevelCellCount: document.querySelectorAll('#tableBody .skill-level-cell').length,
  skillLevelConfirmedCellCount: document.querySelectorAll('#tableBody .skill-level-cell.confirmed').length,
  headers: [...document.querySelectorAll('#tableHead th')].map((node) => node.textContent.trim()),
  text: document.body.textContent || '',
};
"""


def click_view(port: int, session_id: str, view: str) -> dict:
    execute_js(
        port,
        session_id,
        f"""
const button = document.querySelector('#tabs button[data-view="{view}"]');
if (!button) throw new Error('missing tab {view}');
button.click();
return true;
""",
        timeout=3,
    )
    return execute_js(port, session_id, state_script(), timeout=3)


def set_search(port: int, session_id: str, value: str) -> dict:
    safe = json.dumps(value, ensure_ascii=False)
    execute_js(
        port,
        session_id,
        f"""
const input = document.getElementById('searchInput');
input.value = {safe};
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
return true;
""",
        timeout=3,
    )
    return execute_js(port, session_id, state_script(), timeout=3)


def wait_for_review(port: int, session_id: str, timeout: float = 8) -> 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 state.get("readyMarker") is True
            and marker.get("itemEquipmentStaticImplemented") is True
            and marker.get("equipmentRecordCount") == 36
            and marker.get("itemRecordCount") == 31
            and marker.get("skillReferenceCount") == 41
            and marker.get("skillReferenceExeLevelPayloadConfirmedCount") == 23
            and marker.get("equipmentSkillBindingCount") == 37
            and marker.get("battleSkillPayloadCount") == 130
            and marker.get("staticDataJudgementImplemented") is True
            and state.get("staticJudgementCardCount") == 4
            and state.get("activeTab") == "equipment"
            and state.get("rowCount") == 36
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"item/equipment/skill static review did not become ready: {last_state!r}")


def require(condition: bool, message: str) -> None:
    if not condition:
        raise AssertionError(message)


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")

    port = free_port()
    log_path = OUT / "item_equipment_skill_static_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)
            response = request_json(
                port,
                "POST",
                "/session",
                {"capabilities": {"alwaysMatch": {"browserName": "MiniBrowser"}}},
                timeout=60,
            )
            value = (response or {}).get("value") or {}
            session_id = value.get("sessionId") or ""
            if not session_id:
                raise WebDriverError(f"could not create WebDriver session: {response!r}")
            url = urljoin(base, "/web/item_equipment_skill_static.html")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            initial = wait_for_review(port, session_id)

            search = set_search(port, session_id, "화주")
            require(search["rowCount"] >= 1 and "화주" in search["text"], "equipment search should find 화주")
            set_search(port, session_id, "")

            items = click_view(port, session_id, "items")
            require(items["rowCount"] == 31 and "약초" in items["text"], "items tab should show 31 item rows")
            require("EXE 효과" in items["text"] and "HP +50" in items["text"] and "handler 0x004357e6" in items["text"], "items tab should expose EXE-bound consumable effect handlers")
            require("참고/가격" in items["text"] and "가격은 참고/상점 데이터" in items["text"], "items tab should separate reference prices from EXE effects")

            skills = click_view(port, session_id, "skills")
            require(skills["rowCount"] == 41 and "맹호스페셜" in skills["text"], "skills tab should show reference + EXE hit rows")
            require(skills["skillLevelCellCount"] == 164, "skills tab should render four level cells for each skill row")
            require(skills["skillLevelConfirmedCellCount"] == 92, "skills tab should render 23 confirmed four-level payload groups")
            require("숙련도별 EXE payload" in skills["text"], "skills tab should expose grouped EXE level payload columns")
            require("필살기" in skills["text"] and "장기" in skills["text"] and "달인기" in skills["text"] and "신기" in skills["text"], "skills tab should expose four level child columns")
            require("MP 20" in skills["text"] and "7타" in skills["text"], "skills tab should show confirmed level payload hit counts")
            require("인법·분신술" in skills["text"] and "5타" in skills["text"], "skills tab should treat clone count as visual and show EXE attack count")

            equipment_skills = click_view(port, session_id, "equipmentSkills")
            require(equipment_skills["rowCount"] == 37 and "마시기" in equipment_skills["text"], "equipment skills tab should show 37 bindings")
            require("MP" in equipment_skills["text"] and "타수" in equipment_skills["text"] and "기타 특성" in equipment_skills["text"], "equipment skills tab should split payload fields into columns")
            require("지옥 다리후리기" in equipment_skills["text"] and "5" in equipment_skills["text"], "equipment skills tab should show hit count for equipment attacks")
            require("술취함 상태 변경" in equipment_skills["text"] and "MP회복" in equipment_skills["text"], "equipment skills tab should show non-attack payload traits")

            payloads = click_view(port, session_id, "payloads")
            require(payloads["rowCount"] == 130 and "도주" in payloads["text"], "payload tab should show 130 payload rows")
            require("cost 후보" in payloads["text"] and "공격수" in payloads["text"], "payload tab should label sequence count as attack count")
            require("수치 후보" in payloads["text"] and "enum 후보" in payloads["text"], "payload tab should split numeric and enum candidates")
            require("개인공격" in payloads["text"] and "전체공격" in payloads["text"] and "넘어짐" in payloads["text"], "payload tab should show decoded target/action/status labels")
            require("출처 후보" in payloads["text"] and "플레이어 커맨드: 도주" in payloads["text"], "payload tab should show confirmed player command source rows")
            require("전투 액션 페이로드" in payloads["text"], "payload tab should label records by corrected battle action scope")
            marker = payloads.get("marker") or {}
            require("shared battle action payloads" in (marker.get("battleSkillPayloadScope") or ""), "browser marker should expose corrected shared battle action payload scope")
            require(marker.get("battleSkillPayloadTargetScopeSplitImplemented") is True, "browser marker should expose target scope split")
            require(marker.get("battleSkillPayloadSequenceCountMeaning") == "attack-count-for-offensive-actions-shared-effect-count-for-support-actions", "browser marker should expose sequence count meaning")
            require(marker.get("battleSkillPayloadCostBytePromotedAsMonsterMp") is False, "browser marker should keep cost byte unpromoted as monster MP")
            require(marker.get("battleSkillPayloadPlayerCommandRowsConfirmed") is True, "browser marker should expose confirmed player command rows")
            require(marker.get("staticDataJudgementImplemented") is True, "browser marker should expose static data judgement panel")
            require("남은 정적 연결" in payloads["text"] and "battle_monster_action_catalog" in payloads["text"], "static judgement panel should point monster skill binding to the catalog review")
            state = {
                "initial": initial,
                "items": items,
                "skills": skills,
                "equipmentSkills": equipment_skills,
                "payloads": payloads,
                "search": search,
            }
        finally:
            if session_id:
                try:
                    request_json(port, "DELETE", f"/session/{session_id}", timeout=2)
                except Exception:
                    pass
            proc.terminate()
            try:
                proc.wait(timeout=4)
            except subprocess.TimeoutExpired:
                proc.kill()
            if not keep_log:
                log_path.unlink(missing_ok=True)
    return state


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()
    state = verify_browser(args.base.rstrip("/") + "/", keep_log=args.keep_log)
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "item_equipment_skill_static_browser_smoke.json").write_text(
        json.dumps(state, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    print(json.dumps({
        "status": "ok",
        "equipmentRows": state["initial"]["rowCount"],
        "itemRows": state["items"]["rowCount"],
        "skillRows": state["skills"]["rowCount"],
        "equipmentSkillRows": state["equipmentSkills"]["rowCount"],
        "payloadRows": state["payloads"]["rowCount"],
    }, ensure_ascii=False))


if __name__ == "__main__":
    main()
