"""Tests for Phase 84: Distributed Locks."""

import json
import time
import pytest
from pathlib import Path
from unittest.mock import patch

import brynq.distributed_locks as dl


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect storage to tmp_path."""
    monkeypatch.setattr(dl, "LOCKS_DIR", tmp_path)
    dl.reset()


# ── acquire_lock ───────────────────────────────────────────────────────────

class TestAcquireLock:
    def test_acquire_basic(self):
        acquired, info = dl.acquire_lock("file.txt", "agent-1")
        assert acquired is True
        assert info["resource"] == "file.txt"
        assert info["owner_id"] == "agent-1"
        assert info["ttl_seconds"] == 300

    def test_acquire_custom_ttl(self):
        acquired, info = dl.acquire_lock("res", "a1", ttl_seconds=60)
        assert acquired is True
        assert info["ttl_seconds"] == 60

    def test_acquire_already_locked(self):
        dl.acquire_lock("res", "a1")
        acquired, info = dl.acquire_lock("res", "a2")
        assert acquired is False
        assert info["owner_id"] == "a1"

    def test_acquire_reentrant(self):
        dl.acquire_lock("res", "a1", ttl_seconds=100)
        acquired, info = dl.acquire_lock("res", "a1", ttl_seconds=200)
        assert acquired is True
        assert info["owner_id"] == "a1"

    def test_acquire_expired_lock(self):
        dl.acquire_lock("res", "a1", ttl_seconds=1)
        # Simulate expiration
        lock = dl._load_lock("res")
        lock["expires_at"] = time.time() - 10
        dl._save_lock("res", lock)
        acquired, info = dl.acquire_lock("res", "a2")
        assert acquired is True
        assert info["owner_id"] == "a2"

    def test_acquire_persists(self, tmp_path):
        dl.acquire_lock("file.txt", "a1")
        files = list(tmp_path.glob("*.json"))
        assert len(files) == 1

    def test_acquire_multiple_resources(self):
        a1, _ = dl.acquire_lock("file1", "agent-1")
        a2, _ = dl.acquire_lock("file2", "agent-2")
        assert a1 is True
        assert a2 is True
        locks = dl.list_locks()
        assert len(locks) == 2


# ── release_lock ───────────────────────────────────────────────────────────

class TestReleaseLock:
    def test_release_own(self):
        dl.acquire_lock("res", "a1")
        assert dl.release_lock("res", "a1") is True
        assert dl.check_lock("res") is None

    def test_release_not_owner(self):
        dl.acquire_lock("res", "a1")
        assert dl.release_lock("res", "a2") is False
        assert dl.check_lock("res") is not None

    def test_release_no_lock(self):
        assert dl.release_lock("ghost", "a1") is False


# ── check_lock ─────────────────────────────────────────────────────────────

class TestCheckLock:
    def test_check_active(self):
        dl.acquire_lock("res", "a1", ttl_seconds=600)
        info = dl.check_lock("res")
        assert info is not None
        assert info["owner_id"] == "a1"
        assert info["remaining_seconds"] > 0
        assert info["expired"] is False

    def test_check_no_lock(self):
        assert dl.check_lock("ghost") is None

    def test_check_expired_cleans_up(self):
        dl.acquire_lock("res", "a1", ttl_seconds=1)
        lock = dl._load_lock("res")
        lock["expires_at"] = time.time() - 10
        dl._save_lock("res", lock)
        assert dl.check_lock("res") is None


# ── force_release ──────────────────────────────────────────────────────────

class TestForceRelease:
    def test_force_release_existing(self):
        dl.acquire_lock("res", "a1")
        assert dl.force_release("res", reason="admin override") is True
        assert dl.check_lock("res") is None

    def test_force_release_missing(self):
        assert dl.force_release("ghost") is False


# ── list_locks ─────────────────────────────────────────────────────────────

class TestListLocks:
    def test_list_all(self):
        dl.acquire_lock("f1", "a1")
        dl.acquire_lock("f2", "a2")
        dl.acquire_lock("f3", "a1")
        locks = dl.list_locks()
        assert len(locks) == 3

    def test_list_by_owner(self):
        dl.acquire_lock("f1", "a1")
        dl.acquire_lock("f2", "a2")
        dl.acquire_lock("f3", "a1")
        assert len(dl.list_locks(owner_id="a1")) == 2
        assert len(dl.list_locks(owner_id="a2")) == 1

    def test_list_excludes_expired(self):
        dl.acquire_lock("f1", "a1", ttl_seconds=1)
        lock = dl._load_lock("f1")
        lock["expires_at"] = time.time() - 10
        dl._save_lock("f1", lock)
        dl.acquire_lock("f2", "a2")
        locks = dl.list_locks()
        assert len(locks) == 1
        assert locks[0]["owner_id"] == "a2"

    def test_list_empty(self):
        assert dl.list_locks() == []


# ── renew_lock ─────────────────────────────────────────────────────────────

class TestRenewLock:
    def test_renew_existing(self):
        dl.acquire_lock("res", "a1", ttl_seconds=60)
        ok, info = dl.renew_lock("res", "a1", extend_seconds=600)
        assert ok is True
        assert info["ttl_seconds"] == 600
        assert info["renewed_at"] is not None

    def test_renew_not_owner(self):
        dl.acquire_lock("res", "a1")
        ok, info = dl.renew_lock("res", "a2")
        assert ok is False
        assert info is None

    def test_renew_missing(self):
        ok, info = dl.renew_lock("ghost", "a1")
        assert ok is False

    def test_renew_expired(self):
        dl.acquire_lock("res", "a1", ttl_seconds=1)
        lock = dl._load_lock("res")
        lock["expires_at"] = time.time() - 10
        dl._save_lock("res", lock)
        ok, info = dl.renew_lock("res", "a1")
        assert ok is False


# ── cleanup_expired ────────────────────────────────────────────────────────

class TestCleanupExpired:
    def test_cleanup_removes_expired(self):
        dl.acquire_lock("f1", "a1", ttl_seconds=1)
        dl.acquire_lock("f2", "a2", ttl_seconds=600)
        # Expire f1
        lock = dl._load_lock("f1")
        lock["expires_at"] = time.time() - 10
        dl._save_lock("f1", lock)
        cleaned = dl.cleanup_expired()
        assert cleaned == 1
        assert dl.check_lock("f1") is None
        assert dl.check_lock("f2") is not None

    def test_cleanup_nothing_expired(self):
        dl.acquire_lock("f1", "a1", ttl_seconds=600)
        assert dl.cleanup_expired() == 0

    def test_cleanup_empty(self):
        assert dl.cleanup_expired() == 0

    def test_cleanup_all_expired(self):
        for i in range(5):
            dl.acquire_lock(f"f{i}", f"a{i}", ttl_seconds=1)
            lock = dl._load_lock(f"f{i}")
            lock["expires_at"] = time.time() - 10
            dl._save_lock(f"f{i}", lock)
        assert dl.cleanup_expired() == 5
