"""Wspólne uruchamianie harnessów jsdom z tego katalogu.

Logika frontendu siedzi w JavaScripcie, więc właściwe sprawdzenie robi harness
w node. Ten moduł odpala wskazany harness raz (na proces) i wystawia jego
przypadki tak, żeby wrapper pytest robił z każdego osobny test.

Harness ma wypisać na stdout JSON: {"results": [{"id", "name", "ok", "detail"}, ...]}.
"""

from __future__ import annotations

import importlib.util
import json
import shutil
import subprocess
from functools import lru_cache
from pathlib import Path

HERE = Path(__file__).resolve().parent
NODE_MODULES = HERE / "node_modules"

SETUP_HINT = f"uruchom `cd {HERE} && npm install` albo zainstaluj Python Playwright + Chromium"


def _has_jsdom() -> bool:
    return shutil.which("node") is not None and (NODE_MODULES / "jsdom").is_dir()


def _has_browser_fallback() -> bool:
    return (
        importlib.util.find_spec("playwright") is not None
        and shutil.which("chromium") is not None
    )


def engine() -> str | None:
    """Zwraca dostępny silnik testów JS: jsdom lub chromium."""
    if _has_jsdom():
        return "jsdom"
    if _has_browser_fallback():
        return "chromium"
    return None


def missing_requirement() -> str | None:
    """Zwraca powód, dla którego testów nie da się uruchomić, albo None."""
    if engine() is not None:
        return None
    return f"brak node+jsdom oraz fallbacku Chromium/Playwright — {SETUP_HINT}"


@lru_cache(maxsize=None)
def run_harness(harness: str) -> tuple[dict, ...]:
    """Uruchamia harness przez jsdom, a gdy go brak — przez Chromium."""
    selected = engine()
    if selected == "jsdom":
        proc = subprocess.run(
            ["node", str(HERE / harness)],
            cwd=HERE,
            capture_output=True,
            text=True,
            timeout=120,
        )
        stdout = proc.stdout.strip()
        if not stdout:
            raise RuntimeError(f"{harness} nic nie wypisał (kod {proc.returncode})\n{proc.stderr}")
        try:
            payload = json.loads(stdout.splitlines()[-1])
        except json.JSONDecodeError as exc:
            raise RuntimeError(
                f"nieczytelny wynik {harness}: {exc}\nstdout:\n{stdout}\nstderr:\n{proc.stderr}"
            ) from exc
    elif selected == "chromium":
        from browser_harness import run as run_in_browser
        payload = run_in_browser(harness)
    else:
        raise RuntimeError(missing_requirement() or "brak silnika testów JS")

    if "error" in payload:
        raise RuntimeError(f"{harness} przerwany: {payload['error']}\n{payload.get('stack', '')}")

    results = payload.get("results", [])
    if not results:
        raise RuntimeError(f"{harness} nie zwrócił żadnych przypadków")

    return tuple(results)


def case_ids(harness: str) -> list[str]:
    """Identyfikatory przypadków, zbierane przed uruchomieniem testów."""
    if missing_requirement():
        return []
    try:
        return [case["id"] for case in run_harness(harness)]
    except Exception:
        # Właściwy błąd pokaże test sprawdzający sam harness — tu tylko nie
        # wywalamy kolekcji testów.
        return []


def assert_case(harness: str, case_id: str) -> None:
    """Podnosi AssertionError z opisem, gdy dany przypadek nie przeszedł."""
    case = next(c for c in run_harness(harness) if c["id"] == case_id)
    detail = f" ({case['detail']})" if case["detail"] else ""
    assert case["ok"], f"{case['name']}{detail}"


def report(harness: str) -> int:
    """Tryb bez pytest: odpala harness i wypisuje raport."""
    reason = missing_requirement()
    if reason:
        print(f"POMINIĘTO: {reason}")
        return 0

    try:
        results = run_harness(harness)
    except RuntimeError as exc:
        print(f"BŁĄD: {exc}")
        return 2

    for case in results:
        status = "OK  " if case["ok"] else "FAIL"
        detail = f"  → {case['detail']}" if case["detail"] else ""
        print(f"{status} {case['id']}: {case['name']}{detail}")

    failed = [c for c in results if not c["ok"]]
    print(f"\n{len(results) - len(failed)}/{len(results)} przeszło")
    return 1 if failed else 0
