"""End-to-end wiring for the multi-monitor features: `petctl jump`, `petctl
monitors`, `petctl read`, and the screen-layout note that rides along with
each utterance.

Needs a QApplication (signals), so run with QT_QPA_PLATFORM=offscreen.
"""

import sys
from pathlib import Path

import pytest
from PySide6.QtWidgets import QApplication

from bolt_pet import controller as controller_mod
from bolt_pet import pet_actions
from bolt_pet.monitors import Monitor

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

_app = QApplication.instance() or QApplication(["test"])


@pytest.fixture(autouse=True)
def no_screen_probes(monkeypatch):
    monkeypatch.setattr(controller_mod.screen_context, "is_fullscreen_active", lambda: False)
    monkeypatch.setattr(controller_mod.screen_context, "context_for", lambda text: text)


@pytest.fixture
def ctrl():
    controller = controller_mod.PetController()
    controller.set_monitors(
        [
            Monitor(0, "HDMI-0", 0, 0, 1920, 1080),
            Monitor(1, "HDMI-1", 1920, 0, 1920, 1080),
            Monitor(2, "DP-0", 0, 1080, 1920, 1080, primary=True),
        ]
    )
    controller.set_pet_monitor(0)
    return controller


def _capture(signal):
    events = []
    signal.connect(lambda *a: events.append(a[0] if len(a) == 1 else a))
    return events


# ── parsing ─────────────────────────────────────────────────────────────────

def test_jump_parses_without_validating_the_target():
    """Which monitors exist is a runtime fact, so the pure parser passes the
    spec through and monitors.resolve() judges it later."""
    assert pet_actions.parse("petctl jump 2") == {"action": "jump", "target": "2"}
    assert pet_actions.parse("petctl jump next") == {"action": "jump", "target": "next"}
    assert pet_actions.parse("petctl monitor left") == {"action": "jump", "target": "left"}
    assert pet_actions.parse("petctl screen HDMI-1") == {"action": "jump", "target": "HDMI-1"}
    # a nonsense target is still parsed — it fails at resolve time, with a
    # message listing the real monitors
    assert pet_actions.parse("petctl jump sideways") == {
        "action": "jump", "target": "sideways",
    }


def test_jump_needs_a_target():
    with pytest.raises(pet_actions.ActionError, match="needs a monitor"):
        pet_actions.parse("petctl jump")


def test_read_defaults_to_the_current_screen():
    assert pet_actions.parse("petctl read") == {"action": "read", "target": "here"}
    assert pet_actions.parse("petctl read all") == {"action": "read", "target": "all"}
    assert pet_actions.parse("petctl look 2") == {"action": "read", "target": "2"}
    assert pet_actions.parse("petctl see here") == {"action": "read", "target": "here"}


def test_monitors_verb():
    for spelling in ("monitors", "screens", "displays"):
        assert pet_actions.parse(f"petctl {spelling}") == {"action": "monitors"}


def test_help_mentions_the_new_verbs():
    assert "petctl jump" in pet_actions.HELP
    assert "petctl read" in pet_actions.HELP
    assert "petctl monitors" in pet_actions.HELP


# ── jump ────────────────────────────────────────────────────────────────────

def test_jump_resolves_to_an_index_before_reaching_the_window(ctrl):
    """The controller resolves and emits a concrete index, so the window can't
    re-resolve the spec against a different screen ordering."""
    actions = _capture(ctrl.action)
    out = ctrl._handle_command("petctl jump next")
    assert actions == [{"action": "jump", "monitor": 1}]
    assert "monitor 2: HDMI-1" in out


def test_jump_by_direction_uses_the_published_layout(ctrl):
    actions = _capture(ctrl.action)
    ctrl._handle_command("petctl jump down")
    assert actions == [{"action": "jump", "monitor": 2}]


def test_jump_tracks_where_the_pet_actually_is(ctrl):
    ctrl.set_pet_monitor(1)
    actions = _capture(ctrl.action)
    ctrl._handle_command("petctl jump left")
    assert actions == [{"action": "jump", "monitor": 0}]


def test_jump_to_a_nonexistent_monitor_reports_back_and_moves_nothing(ctrl):
    actions = _capture(ctrl.action)
    out = ctrl._handle_command("petctl jump 7")
    assert actions == []
    assert "no monitor 7" in out
    assert "you have 3" in out


def test_jump_never_reaches_the_shell(monkeypatch, ctrl):
    ran = []
    monkeypatch.setattr(controller_mod.server_client, "run_local_command", ran.append)
    ctrl._handle_command("petctl jump 2")
    ctrl._handle_command("petctl monitors")
    ctrl._handle_command("petctl read")
    assert ran == []


# ── monitors ────────────────────────────────────────────────────────────────

def test_monitors_lists_the_layout_and_where_the_pet_is(ctrl):
    ctrl.set_pet_monitor(2)
    out = ctrl._handle_command("petctl monitors")
    assert "3 monitor(s)" in out
    assert "HDMI-0" in out and "DP-0" in out
    assert out.count("Bolt is here") == 1


def test_monitors_before_the_ui_has_published_anything():
    fresh = controller_mod.PetController()
    assert "no monitor information" in fresh._handle_command("petctl monitors")


# ── read ────────────────────────────────────────────────────────────────────

def test_read_here_uses_the_pets_own_screen(monkeypatch, ctrl):
    seen = []
    monkeypatch.setattr(
        controller_mod.screen_text, "read_monitor",
        lambda monitor, limit: seen.append(monitor.number) or "text",
    )
    ctrl.set_pet_monitor(1)
    assert ctrl._handle_command("petctl read") == "text"
    assert seen == [2]


def test_read_a_named_screen(monkeypatch, ctrl):
    seen = []
    monkeypatch.setattr(
        controller_mod.screen_text, "read_monitor",
        lambda monitor, limit: seen.append(monitor.name) or "text",
    )
    ctrl._handle_command("petctl read DP-0")
    assert seen == ["DP-0"]


def test_read_all_goes_through_the_multi_screen_path(monkeypatch, ctrl):
    seen = []
    monkeypatch.setattr(
        controller_mod.screen_text, "read_monitors",
        lambda monitors, limit: seen.append(len(monitors)) or "everything",
    )
    assert ctrl._handle_command("petctl read all") == "everything"
    assert seen == [3]


def test_read_an_unknown_screen_explains_rather_than_raising(ctrl):
    out = ctrl._handle_command("petctl read 9")
    assert out.startswith("[pet]")
    assert "no monitor 9" in out


def test_read_respects_the_kill_switch(monkeypatch, ctrl):
    monkeypatch.setattr(controller_mod.config, "SCREEN_TEXT", False)
    called = []
    monkeypatch.setattr(
        controller_mod.screen_text, "read_monitor",
        lambda *a, **k: called.append(1) or "text",
    )
    out = ctrl._handle_command("petctl read")
    assert called == []
    assert "disabled" in out and "SCREEN_TEXT" in out


def test_read_passes_the_character_cap_through(monkeypatch, ctrl):
    monkeypatch.setattr(controller_mod.config, "SCREEN_TEXT_MAX_CHARS", 123)
    seen = []
    monkeypatch.setattr(
        controller_mod.screen_text, "read_monitor",
        lambda monitor, limit: seen.append(limit) or "text",
    )
    ctrl._handle_command("petctl read")
    assert seen == [123]


# ── per-turn context ────────────────────────────────────────────────────────

def test_layout_rides_along_with_each_utterance(monkeypatch, ctrl):
    monkeypatch.setattr(controller_mod.config, "MONITOR_CONTEXT", True)
    out = ctrl._with_context("what's on the other screen?")
    assert out.startswith("what's on the other screen?")
    assert "3 monitors:" in out
    assert "Bolt is on 1" in out


def test_layout_context_can_be_switched_off(monkeypatch, ctrl):
    monkeypatch.setattr(controller_mod.config, "MONITOR_CONTEXT", False)
    assert ctrl._with_context("hello") == "hello"


def test_screen_text_never_rides_along_automatically(monkeypatch, ctrl):
    """The layout is free; the *contents* cost an OCR pass and a lot of
    privacy, so they only ever move on an explicit petctl read."""
    monkeypatch.setattr(controller_mod.config, "MONITOR_CONTEXT", True)
    called = []
    monkeypatch.setattr(
        controller_mod.screen_text, "read_monitor", lambda *a, **k: called.append(1)
    )
    monkeypatch.setattr(
        controller_mod.screen_text, "read_monitors", lambda *a, **k: called.append(1)
    )
    ctrl._with_context("hello")
    assert called == []
