"""Tests for YOLO mode toggle in Web UI (Issue #467).

Covers:
- GET /api/session/yolo — query YOLO state for a session
- POST /api/session/yolo — enable/disable YOLO for a session
- /yolo slash command registration in commands.js
- YOLO pill HTML element presence in index.html
- Skip-all button presence in approval card
- CSS classes for .yolo-pill and .approval-btn.yolo
- i18n keys present in all 6 locales
"""
import os
import re
import json
import pathlib
import shutil
import subprocess
import pytest

from tests.conftest import requires_agent_modules

TEST_BASE = f"http://127.0.0.1:{os.environ.get('HERMES_WEBUI_TEST_PORT', '8788')}"


def _read_static_file(name: str) -> str:
    return (pathlib.Path(__file__).resolve().parents[1] / "static" / name).read_text(
        encoding="utf-8"
    )


@pytest.fixture(scope="module")
def commands_js():
    return _read_static_file("commands.js")


@pytest.fixture(scope="module")
def messages_js():
    return _read_static_file("messages.js")


@pytest.fixture(scope="module")
def index_html():
    return _read_static_file("index.html")


@pytest.fixture(scope="module")
def style_css():
    return _read_static_file("style.css")


@pytest.fixture(scope="module")
def i18n_js():
    return _read_static_file("i18n.js")


def _get(path, expect_ok=True):
    import urllib.request, urllib.error
    try:
        with urllib.request.urlopen(TEST_BASE + path, timeout=10) as r:
            return json.loads(r.read())
    except urllib.error.HTTPError as e:
        try:
            body = json.loads(e.read())
        except Exception:
            body = {}
        if expect_ok:
            return body
        return body


def _post(path, body=None, expect_ok=True):
    import urllib.request, urllib.error
    data = json.dumps(body or {}).encode()
    req = urllib.request.Request(
        TEST_BASE + path, data=data, headers={"Content-Type": "application/json"}
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return json.loads(r.read())
    except urllib.error.HTTPError as e:
        try:
            body = json.loads(e.read())
        except Exception:
            body = {}
        return body


# ── Backend endpoint tests ──

@requires_agent_modules
class TestYoloEndpointGet:
    """GET /api/session/yolo should return yolo_enabled state.

    Agent-dependent: the endpoint reads from ``tools.approval._session_yolo``
    in the hermes-agent process. When the agent isn't installed, routes.py
    falls back to a no-op lambda that always returns ``False`` regardless of
    POST state — every assertion here would either silently false-pass or
    flake. Skip cleanly when modules aren't importable.
    """

    def test_yolo_get_returns_false_by_default(self):
        """A fresh session should not have YOLO enabled."""
        data = _get("/api/session/yolo?session_id=test-yolo-fresh-001")
        assert data is not None
        assert data.get("yolo_enabled") is False

    def test_yolo_get_requires_session_id(self):
        """Missing session_id returns an error response."""
        resp = _get("/api/session/yolo?session_id=")
        # Empty session_id may return 400 or empty response
        assert resp is not None


@requires_agent_modules
class TestYoloEndpointPost:
    """POST /api/session/yolo should toggle YOLO for a session.

    Agent-dependent: the endpoint writes to ``tools.approval._session_yolo``
    in the hermes-agent process. Without the agent, routes.py falls back to
    a no-op lambda; the response shape ``{"yolo_enabled": <input>}`` echoes
    the request body, so naive POST-only tests false-pass. The
    ``test_yolo_post_persists_within_session`` test catches this by reading
    state back via GET — it only succeeds when the agent is wired.
    """

    def test_yolo_post_enable(self):
        """Enabling YOLO returns ok=True and yolo_enabled=True."""
        sid = "test-yolo-enable-001"
        data = _post("/api/session/yolo", {"session_id": sid, "enabled": True})
        assert data.get("ok") is True
        assert data.get("yolo_enabled") is True

    def test_yolo_post_disable(self):
        """Disabling YOLO returns ok=True and yolo_enabled=False."""
        sid = "test-yolo-disable-001"
        _post("/api/session/yolo", {"session_id": sid, "enabled": True})
        data = _post("/api/session/yolo", {"session_id": sid, "enabled": False})
        assert data.get("ok") is True
        assert data.get("yolo_enabled") is False

    def test_yolo_post_persists_within_session(self):
        """After enabling, GET should reflect the enabled state."""
        sid = "test-yolo-persist-001"
        _post("/api/session/yolo", {"session_id": sid, "enabled": True})
        data = _get(f"/api/session/yolo?session_id={sid}")
        assert data.get("yolo_enabled") is True

    def test_yolo_post_cross_session_isolation(self):
        """Enabling YOLO for one session doesn't affect another."""
        sid_a = "test-yolo-iso-a"
        sid_b = "test-yolo-iso-b"
        _post("/api/session/yolo", {"session_id": sid_a, "enabled": True})
        data = _get(f"/api/session/yolo?session_id={sid_b}")
        assert data.get("yolo_enabled") is False

    def test_yolo_post_defaults_to_enabled(self):
        """POST without 'enabled' key defaults to True."""
        sid = "test-yolo-default-001"
        data = _post("/api/session/yolo", {"session_id": sid})
        assert data.get("yolo_enabled") is True


# ── Frontend JS tests (static file analysis — no server needed) ──

class TestYoloCommandRegistration:
    """/yolo slash command should be registered in commands.js."""

    def test_yolo_command_in_array(self, commands_js):
        assert "'yolo'" in commands_js or '"yolo"' in commands_js

    def test_yolo_uses_cmdYolo(self, commands_js):
        assert "cmdYolo" in commands_js

    def test_cmdYolo_function_exists(self, commands_js):
        assert re.search(r"function\s+cmdYolo\s*\(", commands_js)

    def test_cmdYolo_calls_yolo_endpoint(self, commands_js):
        assert "/api/session/yolo" in commands_js


class TestYoloBusySendPath:
    """/yolo should be recognized by the busy-send command intercept."""

    def test_yolo_in_busy_send_allowlist(self, messages_js):
        send_idx = messages_js.find("async function send(")
        assert send_idx >= 0, "send() not found in messages.js"
        busy_start = messages_js.find("S.busy||compressionRunning", send_idx)
        assert busy_start >= 0, "busy block not found in send()"
        intercept_start = messages_js.find("if(text.startsWith('/')", busy_start)
        assert intercept_start >= 0, "busy slash intercept block not found in send()"
        intercept_idx = messages_js.find("'steer','interrupt','queue','terminal','goal','yolo'", intercept_start)
        busymode_idx = messages_js.find("_defaultMessageMode||'steer'", busy_start)
        assert intercept_idx >= 0, "Busy-path slash allowlist must include yolo in the mid-turn branch"
        assert intercept_idx < busymode_idx, "Busy-path intercept must run before busyMode routing"

        intercept_block = messages_js[intercept_idx:busymode_idx]
        assert "_bc.fn(_pc.args)" in intercept_block, (
            "Busy-path slash intercept should dispatch directly through the command handler"
        )
        assert "await _bc.fn" in intercept_block, (
            "Busy-path slash intercept should await the command handler"
        )
        clear_idx = intercept_block.find("$('msg').value=''")
        await_idx = intercept_block.find("await _bc.fn")
        assert clear_idx >= 0, "Busy-path intercept should clear composer text before await"
        assert clear_idx < await_idx, "Composer clear must happen before awaiting the handler"


class TestYoloPillHTML:
    """YOLO pill element should exist in index.html."""

    def test_yolo_pill_element_exists(self, index_html):
        assert 'id="yoloPill"' in index_html

    def test_yolo_pill_has_onclick(self, index_html):
        assert 'onclick="cmdYolo()"' in index_html

    def test_yolo_pill_hidden_by_default(self, index_html):
        pill_match = re.search(r'<button[^>]*id="yoloPill"[^>]*>', index_html)
        assert pill_match
        assert "display:none" in pill_match.group(0)

    def test_skip_all_button_exists(self, index_html):
        assert 'id="approvalSkipAll"' in index_html


class TestYoloCSS:
    """YOLO-related CSS classes should exist."""

    def test_yolo_pill_class(self, style_css):
        assert ".yolo-pill{" in style_css or ".yolo-pill {" in style_css

    def test_yolo_pill_uses_amber(self, style_css):
        assert "#f59e0b" in style_css

    def test_approval_skip_all_class(self, style_css):
        assert ".approval-btn.yolo{" in style_css or ".approval-btn.yolo {" in style_css


class TestYoloI18n:
    """YOLO-related i18n keys should exist in all 6 locales."""

    REQUIRED_KEYS = [
        "cmd_yolo",
        "yolo_no_session",
        "yolo_enabled",
        "yolo_disabled",
        "yolo_pill_label",
        "yolo_pill_title_active",
        "approval_skip_all",
        "approval_skip_all_title",
    ]

    LOCALES = ["en", "ru", "es", "de", "zh", "ko"]

    @pytest.mark.parametrize("locale", LOCALES)
    def test_locale_has_all_yolo_keys(self, i18n_js, locale):
        pattern = rf"\s{locale}:\s*\{{"
        match = re.search(pattern, i18n_js)
        assert match, f"Locale '{locale}' not found in i18n.js"
        start = match.end()
        next_locale = re.search(r"\n  \w{2}:\s*\{", i18n_js[start:])
        if next_locale:
            block = i18n_js[start:start + next_locale.start()]
        else:
            block = i18n_js[start:]

        for key in self.REQUIRED_KEYS:
            assert key in block, f"Key '{key}' missing in locale '{locale}'"


@pytest.mark.skipif(shutil.which("node") is None, reason="node not available")
def test_yolo_command_ignores_stale_same_session_generation():
    commands_js = _read_static_file("commands.js")
    start = commands_js.index("async function cmdYolo(")
    cmd_yolo = commands_js[start:commands_js.index("\n// ── Branch / fork command", start)]

    def run(api_source, expected_calls):
        script = "\n".join([
            "const calls=[]; const toasts=[]; let pillUpdates=0; let cardHides=0;",
            "const S={session:{session_id:'old-session'}};",
            "let _loadSessionGeneration=1;",
            "let _yoloEnabled=false; let _approvalSessionId=null; let _approvalCurrentId=null;",
            "const t=k=>k; const showToast=msg=>toasts.push(msg);",
            "const $=()=>({classList:{contains:()=>false}});",
            "const _updateYoloPill=()=>{pillUpdates+=1;};",
            "const hideApprovalCard=()=>{cardHides+=1;};",
            "const toggleYoloFromApproval=async()=>{throw new Error('unexpected card path');};",
            f"const api={api_source};",
            cmd_yolo,
            "(async()=>{",
            " await cmdYolo();",
            f" if(JSON.stringify(calls)!==JSON.stringify({expected_calls!r})) throw new Error('wrong calls '+JSON.stringify(calls));",
            " if(_yoloEnabled!==false) throw new Error('stale response changed new session state');",
            " if(pillUpdates!==0) throw new Error('stale response updated the new session pill');",
            " if(cardHides!==0) throw new Error('stale response hid the new session card');",
            " if(toasts.length!==0) throw new Error('stale response emitted a toast');",
            "})().catch(e=>{console.error(e.stack||e);process.exit(1)});",
        ])
        result = subprocess.run([shutil.which("node"), "-e", script], text=True, capture_output=True)
        assert result.returncode == 0, result.stderr

    run(
        "async path=>{calls.push(path);_loadSessionGeneration=2;return {yolo_enabled:false};}",
        ["/api/session/yolo?session_id=old-session"],
    )
    run(
        "async (path,opts)=>{calls.push(path);if(path.includes('?'))return {yolo_enabled:false};_loadSessionGeneration=2;const e=new Error('relay failed');e.body=JSON.stringify({error:'relay failed',yolo_enabled:true});throw e;}",
        ["/api/session/yolo?session_id=old-session", "/api/session/yolo"],
    )
