import os
import sys
import subprocess
import time
import pytest
import shutil
from pathlib import Path

REPO_ROOT = Path(__file__).parent.parent
LOCK_SCRIPT = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "loop_lock.sh"
RUN_LOOP = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"


def run_lock_cmd(action, marker_path, env=None):
    cmd = ["bash", str(LOCK_SCRIPT), action, str(marker_path)]
    run_env = dict(os.environ)
    if env:
        run_env.update(env)
    res = subprocess.run(cmd, capture_output=True, text=True, env=run_env)
    return res


def acquire_bg(marker_path, sleep_secs=10):
    cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker_path}' && sleep {sleep_secs}"
    proc = subprocess.Popen(["bash", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    for _ in range(40):
        time.sleep(0.05)
        if marker_path.is_file():
            try:
                content = marker_path.read_text()
                if f"pid={proc.pid}" in content:
                    break
            except Exception:
                pass
        if proc.poll() is not None:
            break
    return proc


def get_lstart(pid=None):
    if pid is None:
        pid = os.getpid()
    res = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="], capture_output=True, text=True)
    return res.stdout.strip()


def test_o2_1_acquire_empty_marker(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc = acquire_bg(marker)
    try:
        assert marker.is_file()
        content = marker.read_text()
        assert f"pid={proc.pid}" in content
        assert "lstart=" in content
        assert "started=" in content
    finally:
        proc.kill()
        proc.wait()


def test_o2_2_live_holder_rejected(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc = acquire_bg(marker)
    try:
        res = run_lock_cmd("acquire", marker)
        assert res.returncode == 3
        assert f"pid={proc.pid}" in marker.read_text()
    finally:
        proc.kill()
        proc.wait()


def test_o2_3_dead_holder_reclaimed(tmp_path):
    marker = tmp_path / "loop-guard-active"
    marker.write_text("pid=999999\nlstart=Sat Jan  1 00:00:00 2000\nstarted=2000-01-01T00:00:00Z\n")
    
    proc = acquire_bg(marker)
    try:
        assert marker.is_file()
        assert f"pid={proc.pid}" in marker.read_text()
    finally:
        proc.kill()
        proc.wait()


def test_o2_4_pid_rollover_reclaimed(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc_dummy = subprocess.Popen(["sleep", "10"])
    try:
        # Same PID as proc_dummy, but lstart differs -> stale!
        marker.write_text(f"pid={proc_dummy.pid}\nlstart=Sat Jan  1 00:00:00 2000\nstarted=2000-01-01T00:00:00Z\n")
        
        proc = acquire_bg(marker)
        try:
            assert marker.is_file()
            assert f"pid={proc.pid}" in marker.read_text()
        finally:
            proc.kill()
            proc.wait()
    finally:
        proc_dummy.kill()
        proc_dummy.wait()


def test_o2_5_release_other_marker_fails(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc = acquire_bg(marker)
    try:
        res = run_lock_cmd("release", marker)
        assert res.returncode == 1
        assert marker.is_file()
    finally:
        proc.kill()
        proc.wait()


def test_o2_6_release_own_marker_succeeds(tmp_path):
    marker = tmp_path / "loop-guard-active"
    cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}' && mam_release_loop_lock '{marker}'"
    res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
    assert res.returncode == 0
    assert not marker.exists()


def test_o2_7_no_leftover_temp_files(tmp_path):
    marker = tmp_path / "loop-guard-active"
    cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}' && mam_release_loop_lock '{marker}'"
    subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
    
    files = list(tmp_path.glob("*"))
    assert files == []


def test_o2_8_concurrent_acquisition_single_winner(tmp_path):
    marker = tmp_path / "loop-guard-active"
    script = f"""
source '{LOCK_SCRIPT}'
if mam_acquire_loop_lock '{marker}'; then
  sleep 2
  exit 0
else
  exit 3
fi
"""
    procs = []
    for _ in range(8):
        p = subprocess.Popen(["bash", "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        procs.append(p)
        
    rcs = [p.wait() for p in procs]
    winners = [rc for rc in rcs if rc == 0]
    losers = [rc for rc in rcs if rc == 3]
    
    assert len(winners) == 1
    assert len(losers) == 7


def test_o2_9_marker_never_observed_partial(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc = acquire_bg(marker)
    try:
        content = marker.read_text()
        assert content.startswith("pid=")
        assert "lstart=" in content
    finally:
        proc.kill()
        proc.wait()


def test_o2_10_ps_unavailable_fails_safe(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc = acquire_bg(marker)
    try:
        cmd = f"export PATH=/bin/nonexistent:$PATH && source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}'"
        res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
        assert res.returncode in (3, 4)
    finally:
        proc.kill()
        proc.wait()


def test_o2_11_legacy_marker_respected(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc_dummy = subprocess.Popen(["sleep", "10"])
    try:
        marker.write_text(f"pid={proc_dummy.pid}\n")
        
        cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}'"
        res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
        assert res.returncode == 3
    finally:
        proc_dummy.kill()
        proc_dummy.wait()


def test_o2_12_run_loop_exits_on_lock_failure(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc = acquire_bg(marker)
    try:
        cmd = ["bash", str(RUN_LOOP), "--creator", "dummy-agent", "--task", "test"]
        run_env = dict(os.environ)
        run_env["MAM_LOOP_MARKER"] = str(marker)
        res = subprocess.run(cmd, capture_output=True, text=True, cwd=str(tmp_path), env=run_env)
        assert res.returncode != 0
        assert "Another multi-agent-mux-loop is already running" in res.stdout or "Another multi-agent-mux-loop is already running" in res.stderr
    finally:
        proc.kill()
        proc.wait()


def test_o2_13_o3_guard_compatibility(mam_sandbox):
    marker = mam_sandbox / ".mam" / "loop-guard-active"
    marker.parent.mkdir(parents=True, exist_ok=True)
    
    proc = acquire_bg(marker)
    try:
        guard_script = REPO_ROOT / ".agents" / "hooks" / "loop_delegation_guard.sh"
        payload = f'{{"toolCall":{{"name":"file_change"}},"workspacePaths":["{mam_sandbox}"]}}'
        res = subprocess.run(["bash", str(guard_script)], input=payload, capture_output=True, text=True, cwd=str(mam_sandbox))
        assert res.returncode == 0
        assert '"decision":"deny"' in res.stdout.replace(" ", "")
    finally:
        proc.kill()
        proc.wait()


def test_o2_14_bash_syntax_clean():
    res1 = subprocess.run(["bash", "-n", str(LOCK_SCRIPT)], capture_output=True)
    res2 = subprocess.run(["bash", "-n", str(RUN_LOOP)], capture_output=True)
    assert res1.returncode == 0, res1.stderr
    assert res2.returncode == 0, res2.stderr


def test_o2_15_rejected_loop_preserves_holder_marker(tmp_path):
    marker = tmp_path / "loop-guard-active"
    proc = acquire_bg(marker)
    try:
        holder_content = marker.read_text()
        
        # Subshell attempt that gets rejected
        cmd = f"source '{LOCK_SCRIPT}' && (mam_acquire_loop_lock '{marker}' || true) && mam_release_loop_lock '{marker}'"
        subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
        
        assert marker.is_file()
        assert marker.read_text() == holder_content
    finally:
        proc.kill()
        proc.wait()


def test_o2_16_concurrent_stale_recovery(tmp_path):
    marker = tmp_path / "loop-guard-active"
    marker.write_text("pid=999999\nlstart=Sat Jan  1 00:00:00 2000\n")
    
    script = f"""
source '{LOCK_SCRIPT}'
if mam_acquire_loop_lock '{marker}'; then
  sleep 2
  exit 0
else
  exit 3
fi
"""
    procs = []
    for _ in range(6):
        p = subprocess.Popen(["bash", "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        procs.append(p)
        
    rcs = [p.wait() for p in procs]
    winners = [rc for rc in rcs if rc == 0]
    assert len(winners) == 1


def test_o2_17_staggered_stale_recovery(tmp_path):
    marker = tmp_path / "loop-guard-active"
    marker.write_text("pid=999999\nlstart=Sat Jan  1 00:00:00 2000\n")
    
    procs = []
    for i in range(5):
        script = f"""
sleep 0.05
source '{LOCK_SCRIPT}'
if mam_acquire_loop_lock '{marker}'; then
  sleep 2
  exit 0
else
  exit 3
fi
"""
        p = subprocess.Popen(["bash", "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        procs.append(p)
        
    rcs = [p.wait() for p in procs]
    winners = [rc for rc in rcs if rc == 0]
    assert len(winners) == 1


def test_o2_18_orphan_steal_lock_recovered(tmp_path):
    marker = tmp_path / "loop-guard-active"
    marker.write_text("pid=999999\nlstart=Sat Jan  1 00:00:00 2000\n")
    
    steal_lock = tmp_path / "loop-guard-active.steal"
    steal_lock.write_text("pid=999998\nlstart=Sat Jan  1 00:00:00 2000\n")
    
    proc = acquire_bg(marker)
    try:
        assert marker.is_file()
        assert f"pid={proc.pid}" in marker.read_text()
    finally:
        proc.kill()
        proc.wait()


def test_o2_19_active_steal_in_progress_not_displaced(tmp_path):
    marker = tmp_path / "loop-guard-active"
    marker.write_text("pid=999999\nlstart=Sat Jan  1 00:00:00 2000\n")
    steal_lock = tmp_path / "loop-guard-active.steal"
    
    proc_steal = acquire_bg(steal_lock)
    try:
        cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}'"
        res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
        assert res.returncode == 3
    finally:
        proc_steal.kill()
        proc_steal.wait()


def test_o2_20_contentless_steal_lock_reclaimed(tmp_path):
    marker = tmp_path / "loop-guard-active"
    marker.write_text("pid=999999\nlstart=Sat Jan  1 00:00:00 2000\n")
    
    steal_lock = tmp_path / "loop-guard-active.steal"
    steal_lock.write_text("")
    
    proc = acquire_bg(marker)
    try:
        assert marker.is_file()
        assert f"pid={proc.pid}" in marker.read_text()
    finally:
        proc.kill()
        proc.wait()


def test_o2_21_legacy_steal_directory_cleaned(tmp_path):
    marker = tmp_path / "loop-guard-active"
    steal_dir = tmp_path / "loop-guard-active.steal"
    steal_dir.mkdir()
    
    proc = acquire_bg(marker)
    try:
        assert marker.is_file()
        assert not steal_dir.exists()
    finally:
        proc.kill()
        proc.wait()


def test_o2_22_repeated_invocations_converge(tmp_path):
    marker = tmp_path / "loop-guard-active"
    for i in range(5):
        cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}' && mam_release_loop_lock '{marker}'"
        res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
        assert res.returncode == 0
    assert not marker.exists()
