"""Slot allocation and orphan reaping across parallel browser-profile slots.

No real browser or Xvfb is driven; only lock files and (for the reap test) a
throwaway process standing in for one.
"""

from __future__ import annotations

import fcntl
import subprocess
import time
from pathlib import Path

import pytest

import browser


@pytest.fixture(autouse=True)
def isolated_state(tmp_path, monkeypatch):
    monkeypatch.setattr(browser, "STATE", tmp_path)
    monkeypatch.setattr(browser, "PROFILE_COPY", tmp_path / "browser-profile")
    monkeypatch.setattr(browser, "DOWNLOAD_DIR", tmp_path / "downloads")
    return tmp_path


def test_self_pick_takes_the_first_free_slot():
    session = browser.Session()
    slot = session._acquire_slot()
    try:
        assert slot == 1
    finally:
        session.lock.close()


def test_self_pick_skips_a_slot_thats_already_locked(tmp_path):
    held = browser.lock_path(1).open("w")
    fcntl.flock(held, fcntl.LOCK_EX | fcntl.LOCK_NB)
    try:
        session = browser.Session()
        slot = session._acquire_slot()
        try:
            assert slot == 2
        finally:
            session.lock.close()
    finally:
        held.close()


def test_self_pick_waits_on_slot_one_only_when_every_slot_is_locked(tmp_path, monkeypatch):
    monkeypatch.setattr(browser.time, "sleep", lambda *_: None)
    handles = []
    for slot in range(1, browser.MAX_SLOTS + 1):
        handle = browser.lock_path(slot).open("w")
        fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
        handles.append(handle)
    try:
        session = browser.Session()
        monkeypatch.setattr(session, "LOCK_WAIT_SECONDS", 0)
        with pytest.raises(browser.MarionetteError, match="another ask-gpt run"):
            session._acquire_slot()
    finally:
        for handle in handles:
            handle.close()


def test_an_explicit_slot_is_pinned_never_probed(tmp_path):
    held = browser.lock_path(1).open("w")
    fcntl.flock(held, fcntl.LOCK_EX | fcntl.LOCK_NB)
    try:
        session = browser.Session(slot=2)
        slot = session._acquire_slot()
        try:
            assert slot == 2
        finally:
            session.lock.close()
    finally:
        held.close()


def _fake_xvfb() -> subprocess.Popen:
    """A throwaway process whose argv0 reads as Xvfb, without actually being one."""
    return subprocess.Popen(["bash", "-c", "exec -a Xvfb sleep 30"])


def test_reap_orphans_kills_only_its_own_slots_xvfb(tmp_path, monkeypatch):
    monkeypatch.setattr(browser.time, "sleep", lambda *_: None)
    slot_a_proc = _fake_xvfb()
    slot_b_proc = _fake_xvfb()
    try:
        (tmp_path / "xvfb-2.pid").write_text(str(slot_a_proc.pid))
        (tmp_path / "xvfb-3.pid").write_text(str(slot_b_proc.pid))

        killed = browser.reap_orphans(tmp_path / "browser-profile-2", 2)

        assert killed == 1
        slot_a_proc.wait(5)
        assert slot_a_proc.poll() is not None, "slot 2's own recorded Xvfb must be reaped"
        time.sleep(0.2)
        assert slot_b_proc.poll() is None, "a live sibling slot's Xvfb must never be touched"
    finally:
        for proc in (slot_a_proc, slot_b_proc):
            if proc.poll() is None:
                proc.terminate()
                proc.wait(5)


def test_reap_orphans_does_not_match_a_slots_path_as_a_prefix_of_anothers(tmp_path):
    """"browser-profile" is a substring of "browser-profile-2" — a raw substring
    match on slot 1 would reap a live slot 2 browser."""
    dummy = tmp_path / "dummy-browser.sh"
    dummy.write_text("#!/bin/bash\nsleep 30\n")
    dummy.chmod(0o755)
    proc = subprocess.Popen(
        ["bash", "-c", f'exec -a librewolf "{dummy}" --profile "{tmp_path}/browser-profile-2"'])
    try:
        killed = browser.reap_orphans(tmp_path / "browser-profile", 1)
        assert killed == 0
        assert proc.poll() is None, "slot 1's reap must never touch slot 2's live browser"
    finally:
        if proc.poll() is None:
            proc.terminate()
            proc.wait(5)


def test_reap_orphans_drops_a_stale_marker_for_a_pid_thats_gone(tmp_path, monkeypatch):
    monkeypatch.setattr(browser.time, "sleep", lambda *_: None)
    marker = tmp_path / "xvfb-4.pid"
    marker.write_text("999999999")  # not a real pid
    killed = browser.reap_orphans(tmp_path / "browser-profile-4", 4)
    assert killed == 0
    assert not marker.exists()
