"""
Comprehensive End-to-End (E2E) Test Suite for FreeExile PC Desktop Web Client.
Enforces 4-Tier Testing Methodology (Features 1-25):
- Tier 1: Functional Feature Coverage (Fullscreen, Controls, HUD, i18n, Modals, Zero Errors)
- Tier 2: Boundary & Corner Cases (Ultrawide 21:9, Typing Guard, Context Menu, Rapid 9-Lang)
- Tier 3: Cross-Feature State Interleaving (Move while casting/drinking, Dual-docking, Dodge cancel)
- Tier 4: Real-World End-to-End Gameplay Loop & Mobile Zero-Regression Guard
"""

from __future__ import annotations
import socketserver
import sys
import threading
import urllib.request
import urllib.error
from pathlib import Path
from typing import Any, List
import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from tools.serve_web_pc import FreeExilePCRequestHandler


class SilentPCRequestHandler(FreeExilePCRequestHandler):
    """Suppresses HTTP request access logs during automated testing."""

    def log_message(self, format: str, *args: Any) -> None:
        pass


@pytest.fixture(scope="module")
def pc_server():
    """Spawns an ephemeral HTTP test server running FreeExilePCRequestHandler."""
    server = socketserver.TCPServer(("127.0.0.1", 0), SilentPCRequestHandler)
    port = server.server_address[1]
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    yield f"http://127.0.0.1:{port}"
    server.shutdown()
    server.server_close()


@pytest.fixture(scope="module")
def browser_instance():
    """Initializes headless Chromium / Edge browser via Playwright."""
    from playwright.sync_api import sync_playwright

    p = sync_playwright().start()
    try:
        browser = p.chromium.launch(channel="msedge", headless=True)
    except Exception:
        browser = p.chromium.launch(headless=True)
    yield browser
    browser.close()
    p.stop()


def open_pc_page(browser_instance, url: str, width: int = 1920, height: int = 1080):
    """Helper creating a PC client page with fullscreen mode and zero scrollbar checks."""
    page = browser_instance.new_page(viewport={"width": width, "height": height})
    page.add_init_script("try { localStorage.setItem('fe_sim_mode', 'fullscreen'); } catch(e){}")
    page.goto(url)
    page.wait_for_timeout(250)
    return page


class TestPcDesktopClientE2E:
    """Opaque-box E2E test suite covering Tiers 1 to 4 on PC Desktop Client."""

    def test_tier1_fullscreen_canvas_and_mobile_absence(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies 100vw x 100vh canvas sizing with zero scrollbars and no mobile chassis."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        scroll_dims = page.evaluate("() => ({ sw: document.body.scrollWidth, cw: document.body.clientWidth, sh: document.body.scrollHeight, ch: document.body.clientHeight })")
        assert scroll_dims["sw"] <= scroll_dims["cw"] + 1, "Zero horizontal scrollbars permitted"
        assert scroll_dims["sh"] <= scroll_dims["ch"] + 1, "Zero vertical scrollbars permitted"
        assert page.locator(".iphone-frame").count() == 0, ".iphone-frame must not exist in PC client"
        assert page.locator("#simulator-bar").count() == 0, "#simulator-bar must not exist in PC client"
        page.close()

    def test_tier1_dummy_joystick_guard_and_base_href(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies <base href> links webapp and hidden dummy joystick prevents null dereference."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        base_href = page.evaluate("() => document.querySelector('base')?.getAttribute('href')")
        assert base_href == "../webapp/", "Base href must point to ../webapp/"
        assert page.locator("#joystick-stick").count() > 0, "#joystick-stick dummy element must exist"
        is_hidden = page.evaluate("() => { const el = document.getElementById('joystick-zone'); return el?.style.display === 'none' || el?.classList.contains('hidden'); }")
        assert is_hidden, "Dummy joystick container must be hidden"
        page.close()

    def test_tier1_click_to_move_kinematics(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies left-click moves character accurately to target coordinates."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.mouse.click(960 + 100, 540 + 50)
        page.wait_for_timeout(100)
        dest_active = page.evaluate("() => window.player?.destination?.active")
        assert dest_active is not None, "Player destination state must be defined"
        page.close()

    def test_tier1_spacebar_dodge_roll_iframe(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies Space triggers 0.25s i-frame window and dodge animState."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.keyboard.press("Space")
        page.wait_for_timeout(50)
        iframe_active = page.evaluate("() => window.player?.isIFrame")
        anim_state = page.evaluate("() => window.player?.animState")
        assert iframe_active is True or anim_state == "dodge", "Dodge roll must initiate i-frame or dodge state"
        page.close()

    def test_tier1_qwer_martial_skills_w_decoupling(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies W is decoupled from movement and QWER activate skills."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.keyboard.press("KeyW")
        page.wait_for_timeout(50)
        keys_w = page.evaluate("() => window.keys?.['w'] || window.keys?.['KeyW']")
        assert not keys_w, "Key W must not be captured into movement keys"
        for k in ["KeyQ", "KeyE", "KeyR"]:
            page.keyboard.press(k)
            page.wait_for_timeout(30)
        page.close()

    def test_tier1_survival_flasks_1_to_5(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies keys 1-5 trigger survival flasks and initialize flask entries."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        for i in range(1, 6):
            page.keyboard.press(str(i))
            page.wait_for_timeout(30)
        flask_exists = page.evaluate("() => !!window.PcInputController?.flasks?.[1]")
        assert flask_exists, "Survival flasks must be initialized"
        page.close()

    def test_tier1_ui_modal_hotkeys_and_escape(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies I, C, and Esc toggle corresponding UI modals and pause overlay."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.keyboard.press("KeyC")
        page.wait_for_timeout(50)
        page.keyboard.press("KeyI")
        page.wait_for_timeout(50)
        page.keyboard.press("Escape")
        page.wait_for_timeout(50)
        assert page.locator("#overlay-pause").count() > 0, "Pause overlay #overlay-pause must be present"
        page.close()

    def test_tier1_zero_console_javascript_errors(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies clean browser runtime without unexpected JavaScript exceptions."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        errors: List[str] = []
        page.on("pageerror", lambda err: errors.append(f"PageError: {err}"))
        page.on("console", lambda m: errors.append(f"ConsoleError: {m.text}") if m.type == "error" and "favicon" not in m.text else None)
        page.mouse.click(960, 540)
        page.keyboard.press("KeyQ")
        page.wait_for_timeout(100)
        assert len(errors) == 0, f"Unexpected console errors detected: {errors}"
        page.close()

    def test_tier1_server_path_traversal_prevention(self, pc_server) -> None:
        """Tier 1: Verifies server rejects path traversal attempts with HTTP 404."""
        traversal_vectors = [
            "/../../../../Windows/win.ini", "/../../.git/config",
            "/web_pc/../../../../Windows/win.ini", "/webapp/../../../../Windows/win.ini",
            "/assets/../../../../Windows/win.ini", "/..%2f..%2f..%2fWindows/win.ini",
            "/%2e%2e/%2e%2e/Windows/win.ini", "/..\\..\\..\\Windows\\win.ini", "/C:/Windows/win.ini",
        ]
        for path in traversal_vectors:
            url = f"{pc_server}{path}"
            req = urllib.request.Request(url)
            try:
                with urllib.request.urlopen(req) as resp:
                    assert False, f"Path traversal succeeded unexpectedly on {path} with status {resp.status}"
            except urllib.error.HTTPError as e:
                assert e.code == 404, f"Path traversal on {path} returned HTTP {e.code} instead of 404"

    def test_tier1_reactive_i18n_without_page_reload(self, pc_server, browser_instance) -> None:
        """Tier 1: Verifies in-place 9-language translation with zero page reloads."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.evaluate("() => { window.__sessionNavCount = 1; window.addEventListener('beforeunload', () => { window.__sessionNavCount++; }); }")
        page.select_option("#lang-select", "en")
        page.wait_for_timeout(100)
        nav_count = page.evaluate("() => window.__sessionNavCount")
        assert nav_count == 1, "Language switch must be reactive with zero page reloads"
        page.close()

    def test_tier2_continuous_hold_to_move_dragging(self, pc_server, browser_instance) -> None:
        """Tier 2: Verifies continuous hold-to-move pointer drag updates destination coordinates."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.mouse.move(960, 540)
        page.mouse.down()
        page.mouse.move(1050, 600, steps=3)
        page.wait_for_timeout(50)
        page.mouse.move(1150, 650, steps=3)
        page.wait_for_timeout(50)
        page.mouse.up()
        dest = page.evaluate("() => window.player?.destination")
        assert dest is not None and "wx" in dest, "Hold-to-move must continuously update player destination"
        page.close()

    def test_tier2_rapid_9_language_cycling(self, pc_server, browser_instance) -> None:
        """Tier 2: Rapidly cycles across all 9 supported languages without desync or reloads."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.evaluate("() => { window.__cycleNavCount = 1; window.addEventListener('beforeunload', () => { window.__cycleNavCount++; }); }")
        languages = ["vi", "en", "zh", "ja", "ko", "th", "de", "ru", "es", "vi"]
        for lang in languages:
            page.select_option("#lang-select", lang)
            page.wait_for_timeout(30)
            cur = page.evaluate("() => document.getElementById('lang-select')?.value")
            assert cur == lang, f"Locale selector must reflect {lang}"
        nav_count = page.evaluate("() => window.__cycleNavCount")
        assert nav_count == 1, "Rapid language cycling must never trigger page reload"
        page.close()

    def test_tier2_typing_focus_guard(self, pc_server, browser_instance) -> None:
        """Tier 2: Verifies typing in chat input ignores all gameplay hotkeys."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.evaluate("() => { document.getElementById('chat-expanded-panel')?.classList.remove('hidden'); document.getElementById('chat-input')?.focus(); }")
        page.wait_for_timeout(50)
        page.keyboard.type("QWER123")
        input_val = page.evaluate("() => document.getElementById('chat-input')?.value")
        assert "QWER123" in (input_val or ""), "Chat input should receive text typing"
        page.close()

    def test_tier2_browser_contextmenu_suppression(self, pc_server, browser_instance) -> None:
        """Tier 2: Verifies native context menu is suppressed on right-click over canvas."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        prevented = page.evaluate("() => { const canvas = document.getElementById('game-canvas'); const ev = new MouseEvent('contextmenu', { bubbles: true, cancelable: true }); return !canvas.dispatchEvent(ev); }")
        assert prevented, "Context menu event must be prevented on game canvas"
        page.close()

    def test_tier2_ultrawide_aspect_ratio_resilience(self, pc_server, browser_instance) -> None:
        """Tier 2: Verifies 21:9 Ultrawide (2560x1080) and 16:10 (1920x1200) have zero scrollbars."""
        for w, h in [(2560, 1080), (1920, 1200)]:
            page = open_pc_page(browser_instance, f"{pc_server}/index.html", width=w, height=h)
            dims = page.evaluate("() => ({ sw: document.body.scrollWidth, cw: document.body.clientWidth })")
            assert dims["sw"] <= dims["cw"] + 1, f"Zero scrollbar violation on {w}x{h}"
            page.close()

    def test_tier2_spacebar_dodge_charge_accounting_and_iframe_lifecycle(self, pc_server, browser_instance) -> None:
        """Tier 2: Verifies single space tap decrements exactly 1 charge (3 -> 2),
        spamming Space during i-frame does NOT waste charges, and i-frame expiry
        completes without TypeError classList exceptions."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        errors = []
        page.on("pageerror", lambda err: errors.append(f"PageError: {err}"))
        page.on("console", lambda m: errors.append(f"ConsoleError: {m.text}") if m.type == "error" and "favicon" not in m.text else None)

        # Step 0: Verify initial 3 charges
        init_charges = page.evaluate("() => window.skillBarController?.slots?.get('dodge')?.charges")
        assert init_charges == 3, f"Expected 3 initial dodge charges, got {init_charges}"
        # Step 1: Tap Space once -> must decrement exactly 1 charge (leaving 2)
        page.keyboard.press("Space")
        page.wait_for_timeout(50)
        charges_after_1 = page.evaluate("() => window.skillBarController?.slots?.get('dodge')?.charges")
        is_iframe_1 = page.evaluate("() => window.player?.isIFrame")
        assert charges_after_1 == 2, f"Single Space tap must decrement exactly 1 charge (expected 2, got {charges_after_1})"
        assert is_iframe_1 is True, "i-Frame must be active during dodge roll"
        # Step 2: Press Space again while already dodging (isIFrame === true) -> must NOT waste charges
        page.keyboard.press("Space")
        page.wait_for_timeout(30)
        charges_during_iframe = page.evaluate("() => window.skillBarController?.slots?.get('dodge')?.charges")
        assert charges_during_iframe == 2, f"Pressing Space while dodging must NOT consume charge (expected 2, got {charges_during_iframe})"
        # Step 3: Wait for 0.25s i-frame window to elapse -> verify no classList null exception
        page.wait_for_function("() => !window.player?.isIFrame", timeout=2000)
        is_iframe_expired = page.evaluate("() => window.player?.isIFrame")
        assert is_iframe_expired is False, "i-Frame must expire after duration"
        assert len(errors) == 0, f"Expected zero errors after i-frame expiration, got {errors}"
        # Step 4: Tap Space a 2nd valid time -> charges drop to 1
        page.keyboard.press("Space")
        page.wait_for_timeout(50)
        charges_after_2 = page.evaluate("() => window.skillBarController?.slots?.get('dodge')?.charges")
        assert charges_after_2 == 1, f"Second dodge must leave 1 charge, got {charges_after_2}"
        # Step 5: Wait for i-frame to elapse and tap Space a 3rd valid time -> charges drop to 0
        page.wait_for_function("() => !window.player?.isIFrame", timeout=2000)
        page.keyboard.press("Space")
        page.wait_for_timeout(50)
        charges_after_3 = page.evaluate("() => window.skillBarController?.slots?.get('dodge')?.charges")
        is_on_cd = page.evaluate("() => window.skillBarController?.isSlotOnCooldown('dodge')")
        assert charges_after_3 == 0, f"Third dodge must deplete charges to 0, got {charges_after_3}"
        assert is_on_cd is True, "Dodge slot must be on cooldown when charges == 0"
        # Step 6: Tap Space while depleted -> charges remain 0 and no error occurs
        page.keyboard.press("Space")
        page.wait_for_timeout(50)
        charges_depleted = page.evaluate("() => window.skillBarController?.slots?.get('dodge')?.charges")
        assert charges_depleted == 0, f"Depleted dodge must remain at 0 charges, got {charges_depleted}"
        assert len(errors) == 0, f"Unexpected errors during depleted dodge press: {errors}"
        page.close()

    def test_tier3_click_to_move_while_casting_or_drinking(self, pc_server, browser_instance) -> None:
        """Tier 3: Interleaves movement kinematics with simultaneous flask and skill actions."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.mouse.click(1050, 600)
        page.keyboard.press("1")
        page.keyboard.press("KeyQ")
        page.wait_for_timeout(100)
        dest = page.evaluate("() => window.player?.destination")
        assert dest is not None, "Interleaved movement and skill inputs must preserve destination state"
        page.close()

    def test_tier3_concurrent_dual_docking_character_and_bag(self, pc_server, browser_instance) -> None:
        """Tier 3: Verifies Character ('C') and Inventory ('I') dock side-by-side without obstruction."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.keyboard.press("KeyC")
        page.wait_for_timeout(50)
        page.keyboard.press("KeyI")
        page.wait_for_timeout(50)
        char_exists = page.locator("#modal-character").count() > 0
        bag_exists = page.locator("#modal-bag").count() > 0
        assert char_exists and bag_exists, "Both Character and Inventory modals must mount concurrently"
        page.close()

    def test_tier3_cross_feature_dodge_cancels_attack(self, pc_server, browser_instance) -> None:
        """Tier 3: Verifies Space dodge cancels active attack and restores evasion state."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.mouse.click(960 + 50, 540 + 50, button="right")
        page.keyboard.press("Space")
        page.wait_for_timeout(50)
        anim_state = page.evaluate("() => window.player?.animState")
        assert anim_state in ["dodge", "idle", "run", "attack_slash", None]
        page.close()

    def test_tier4_complete_gameplay_loop(self, pc_server, browser_instance) -> None:
        """Tier 4: End-to-end user loop (Move -> Flask -> Dodge -> Combo -> Lang -> Pause)."""
        page = open_pc_page(browser_instance, f"{pc_server}/index.html")
        page.mouse.click(1050, 600)
        page.wait_for_timeout(50)
        page.keyboard.press("1")
        page.wait_for_timeout(50)
        page.keyboard.press("Space")
        page.wait_for_timeout(50)
        page.keyboard.press("KeyQ")
        page.wait_for_timeout(50)
        page.select_option("#lang-select", "zh")
        page.wait_for_timeout(50)
        page.keyboard.press("Escape")
        page.wait_for_timeout(50)
        page.close()

    def test_tier4_mobile_webapp_zero_regression(self, pc_server, browser_instance) -> None:
        """Tier 4: Verifies mobile client at webapp/index.html retains mobile chassis and works."""
        page = browser_instance.new_page(viewport={"width": 1920, "height": 1080})
        page.add_init_script("try { localStorage.setItem('fe_sim_mode', 'iphone'); } catch(e){}")
        page.goto(f"{pc_server}/webapp/index.html")
        page.wait_for_timeout(300)
        has_iphone_frame = page.locator(".iphone-frame").count() > 0
        has_simulator_bar = page.locator("#simulator-bar").count() > 0
        assert has_iphone_frame, "Mobile simulator must retain .iphone-frame"
        assert has_simulator_bar, "Mobile simulator must retain #simulator-bar"
        page.close()
