"""Cross-process claim semantics.

The lock exists so two MCP hosts sharing one account do not both authenticate —
that would burn two of the five session slots for one logical session.
"""

import json
import os
import time

from a360_mcp.auth import _locks


class TestAcquire:
    def test_first_acquire_wins(self, tmp_path):
        assert _locks.acquire(tmp_path / "a.lock") is True

    def test_second_acquire_loses(self, tmp_path):
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        assert _locks.acquire(path) is False

    def test_release_lets_the_next_caller_win(self, tmp_path):
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        _locks.release(path)
        assert _locks.acquire(path) is True

    def test_lock_records_holder_pid(self, tmp_path):
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        assert json.loads(path.read_text())["pid"] == os.getpid()

    def test_creates_missing_parent_directory(self, tmp_path):
        assert _locks.acquire(tmp_path / "nested" / "deep" / "a.lock") is True

    def test_release_of_absent_lock_is_a_noop(self, tmp_path):
        _locks.release(tmp_path / "never-made.lock")  # no raise


class TestStaleReaping:
    def test_stale_lock_is_reaped_and_reclaimed(self, tmp_path):
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        old = time.time() - 10_000
        os.utime(path, (old, old))
        assert _locks.acquire(path, stale_after=60.0) is True

    def test_fresh_lock_is_not_reaped(self, tmp_path):
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        assert _locks.acquire(path, stale_after=10_000.0) is False

    def test_reaping_is_by_age_not_pid_liveness(self, tmp_path):
        # PIDs get reused; a reaper that trusts a recycled PID deletes a live lock.
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        path.write_text(json.dumps({"pid": 999_999, "acquired_at": time.time()}))
        assert _locks.acquire(path, stale_after=10_000.0) is False


class TestHold:
    def test_hold_yields_true_and_releases_on_exit(self, tmp_path):
        path = tmp_path / "a.lock"
        with _locks.hold(path) as acquired:
            assert acquired is True
            assert path.exists()
        assert not path.exists()

    def test_hold_yields_false_when_contended(self, tmp_path):
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        with _locks.hold(path) as acquired:
            assert acquired is False

    def test_loser_does_not_delete_the_winners_claim(self, tmp_path):
        path = tmp_path / "a.lock"
        _locks.acquire(path)
        with _locks.hold(path):
            pass
        assert path.exists()

    def test_hold_releases_even_when_body_raises(self, tmp_path):
        path = tmp_path / "a.lock"
        try:
            with _locks.hold(path):
                raise RuntimeError("boom")
        except RuntimeError:
            pass
        assert not path.exists()
