"""Screen layout logic — resolving `petctl jump` targets and describing the
setup. Pure: the monitor list is normally published by the UI, so none of this
needs a display."""

import sys
from pathlib import Path

import pytest

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

from bolt_pet import monitors as m


def grid():
    """The 2x2 setup this was built against: four 1080p screens.

        [1 HDMI-0] [2 HDMI-1]
        [3 DP-0  ] [4 DP-2  ]
    """
    return [
        m.Monitor(0, "HDMI-0", 0, 0, 1920, 1080, primary=False),
        m.Monitor(1, "HDMI-1", 1920, 0, 1920, 1080, primary=False),
        m.Monitor(2, "DP-0", 0, 1080, 1920, 1080, primary=True),
        m.Monitor(3, "DP-2", 1920, 1080, 1920, 1080, primary=False),
    ]


def two():
    return [
        m.Monitor(0, "eDP-1", 0, 0, 1920, 1080, primary=True),
        m.Monitor(1, "HDMI-1", 1920, 0, 2560, 1440),
    ]


def test_numbers_shown_to_humans_are_one_based():
    left, right = two()
    assert left.index == 0 and left.number == 1
    assert right.index == 1 and right.number == 2
    assert "1: eDP-1 1920x1080 (primary)" == left.label


def test_geometry_helpers():
    screen = m.Monitor(1, "HDMI-1", 1920, 0, 1920, 1080)
    assert screen.right == 3840 and screen.bottom == 1080
    assert screen.center == (2880, 540)
    assert screen.contains(1920, 0)
    assert screen.contains(3839, 1079)
    assert not screen.contains(3840, 0)  # right edge is exclusive
    assert not screen.contains(1919, 0)


def test_monitor_containing_and_nearest():
    screens = grid()
    assert m.monitor_containing(screens, 100, 100).name == "HDMI-0"
    assert m.monitor_containing(screens, 2000, 1500).name == "DP-2"
    assert m.monitor_containing(screens, -50, -50) is None
    # off the desktop entirely still resolves to something
    assert m.nearest_monitor(screens, -500, -500).name == "HDMI-0"


def test_resolve_by_number():
    screens = grid()
    assert m.resolve(screens, "3").name == "DP-0"
    with pytest.raises(ValueError, match="no monitor 9"):
        m.resolve(screens, "9")
    with pytest.raises(ValueError):
        m.resolve(screens, "0")


def test_resolve_next_and_prev_wrap():
    screens = grid()
    assert m.resolve(screens, "next", current=3).number == 1
    assert m.resolve(screens, "prev", current=0).number == 4
    assert m.resolve(screens, "next", current=0).number == 2


def test_resolve_primary_and_other():
    screens = grid()
    assert m.resolve(screens, "primary", current=0).name == "DP-0"
    # "other" on a two-screen setup is genuinely the other one
    pair = two()
    assert m.resolve(pair, "other", current=0).number == 2
    assert m.resolve(pair, "other", current=1).number == 1


def test_resolve_directions_on_a_grid():
    screens = grid()
    # from top-left (HDMI-0)
    assert m.resolve(screens, "right", current=0).name == "HDMI-1"
    assert m.resolve(screens, "down", current=0).name == "DP-0"
    # from bottom-right (DP-2)
    assert m.resolve(screens, "left", current=3).name == "DP-0"
    assert m.resolve(screens, "up", current=3).name == "HDMI-1"


def test_direction_prefers_the_best_aligned_screen():
    screens = grid()
    # "right" from DP-0 (bottom-left) must pick DP-2 (same row), not HDMI-1,
    # even though both are to the right.
    assert m.resolve(screens, "right", current=2).name == "DP-2"


def test_resolve_direction_with_nothing_there():
    screens = grid()
    with pytest.raises(ValueError, match="no monitor to the left"):
        m.resolve(screens, "left", current=0)


def test_resolve_by_name_is_fuzzy_but_refuses_ambiguity():
    screens = grid()
    assert m.resolve(screens, "dp-2").name == "DP-2"
    assert m.resolve(screens, "HDMI-0").name == "HDMI-0"
    with pytest.raises(ValueError, match="matches several"):
        m.resolve(screens, "hdmi")


def test_resolve_unknown_spec_lists_the_options():
    screens = two()
    with pytest.raises(ValueError) as excinfo:
        m.resolve(screens, "the big one")
    assert "eDP-1" in str(excinfo.value) and "HDMI-1" in str(excinfo.value)


def test_resolve_without_a_current_screen_falls_back_to_primary():
    screens = grid()
    # primary is index 2, so "next" from nowhere is index 3
    assert m.resolve(screens, "next", current=None).number == 4
    # an out-of-range current is treated the same way rather than exploding
    assert m.resolve(screens, "next", current=99).number == 4


def test_resolve_needs_monitors():
    with pytest.raises(ValueError, match="no monitors"):
        m.resolve([], "next")
    with pytest.raises(ValueError, match="needs a target"):
        m.resolve(grid(), "")


def test_random_always_moves_somewhere_else():
    screens = grid()
    for current in range(4):
        assert m.resolve(screens, "random", current=current).index != current


def test_summary_is_one_line_and_marks_where_the_pet_is():
    line = m.summary(grid(), current=1)
    assert "\n" not in line
    assert line.startswith("4 monitors:")
    assert "Bolt is on 2" in line
    assert m.summary([]) is None
    assert "Bolt is on" not in m.summary(grid(), current=None)


def test_annotate_matches_the_screen_context_style():
    out = m.annotate("what's on the other screen?", grid(), current=0)
    assert out.startswith("what's on the other screen?")
    assert "[4 monitors:" in out
    # nothing to say, nothing added
    assert m.annotate("hello", [], None) == "hello"
    assert m.annotate("", grid(), 0) == ""


def test_describe_lists_every_screen_and_flags_the_pet():
    text = m.describe(grid(), current=2)
    assert text.count("\n") == 4  # header + 4 screens
    assert "DP-0" in text and "+0+1080" in text
    assert text.count("Bolt is here") == 1
    assert m.describe([]) == "[pet] no monitor information available"
