"""ORCH-026 Level A serialization (TC-A02..A05).

The merge-lease window (ORCH-043/065) is what serialises "merge -> main-updated"
per repo; ORCH-026 reuses it unchanged. These tests confirm the properties the
ADR relies on:

  TC-A02  extended window: while A holds the lease, B of the SAME repo gets
          "merge-lock busy" -> defer (not rollback); holder-aware release does
          NOT delete A's lease.
  TC-A03  strict per-repo: an orchestrator lease never blocks an enduro-trails
          acquire (both claimable in parallel).
  TC-A04  restart-safe + proactive reclaim: a dead holder's lease is reclaimed
          (reclaim_stale_lease) so the pipeline never wedges forever.
  TC-A05  anti-livelock defer budget: merge_defer_max_attempts is bounded and
          positive -> exhaustion escalates instead of looping forever.
"""
import os
import tempfile

import pytest

os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_orch026_serialize.db"))
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")

from src import merge_gate  # noqa: E402


@pytest.fixture
def leases_dir(tmp_path, monkeypatch):
    monkeypatch.setattr(merge_gate.settings, "repos_dir", str(tmp_path), raising=False)
    monkeypatch.setattr(merge_gate.settings, "merge_lock_timeout_s", 300, raising=False)
    monkeypatch.setattr(merge_gate.settings, "merge_gate_repos", "", raising=False)
    monkeypatch.setattr(merge_gate.settings, "lease_reclaim_enabled", True, raising=False)
    return tmp_path


# ----------------------------------------------------------------- TC-A02
def test_second_task_same_repo_defers_not_rollback(leases_dir):
    okA, reasonA = merge_gate.acquire_merge_lease("orchestrator", "feature/A", "ORCH-1")
    assert okA is True

    okB, reasonB = merge_gate.acquire_merge_lease("orchestrator", "feature/B", "ORCH-2")
    assert okB is False
    assert reasonB == "merge-lock busy"  # -> caller DEFERS, never a rollback signal


def test_holder_aware_release_keeps_foreign_lease(leases_dir):
    merge_gate.acquire_merge_lease("orchestrator", "feature/A", "ORCH-1")
    # A delayed release from B (which never held it) must NOT delete A's lease.
    merge_gate.release_merge_lease("orchestrator", "feature/B")
    okB, reasonB = merge_gate.acquire_merge_lease("orchestrator", "feature/B", "ORCH-2")
    assert okB is False and reasonB == "merge-lock busy"
    # A's own release frees it.
    merge_gate.release_merge_lease("orchestrator", "feature/A")
    okB2, _ = merge_gate.acquire_merge_lease("orchestrator", "feature/B", "ORCH-2")
    assert okB2 is True


# ----------------------------------------------------------------- TC-A03
def test_serialization_is_strictly_per_repo(leases_dir):
    okA, _ = merge_gate.acquire_merge_lease("orchestrator", "feature/A", "ORCH-1")
    okET, _ = merge_gate.acquire_merge_lease("enduro-trails", "feature/E", "ET-1")
    assert okA is True
    assert okET is True, "a different repo must be claimable in parallel (AC-A3)"


# ----------------------------------------------------------------- TC-A04
def test_dead_holder_lease_is_reclaimed(leases_dir, monkeypatch):
    merge_gate.acquire_merge_lease("orchestrator", "feature/A", "ORCH-1")
    # Holder pid is THIS process; simulate it being dead.
    monkeypatch.setattr(merge_gate, "pid_alive", lambda pid: False, raising=False)
    reclaimed = merge_gate.reclaim_stale_lease("orchestrator")
    assert reclaimed is True
    # After reclaim B can acquire -> pipeline does not wedge forever.
    okB, _ = merge_gate.acquire_merge_lease("orchestrator", "feature/B", "ORCH-2")
    assert okB is True


def test_stale_lease_age_reclaimed_on_acquire(leases_dir, monkeypatch):
    # A very short timeout makes the existing lease look stale on B's acquire.
    merge_gate.acquire_merge_lease("orchestrator", "feature/A", "ORCH-1")
    monkeypatch.setattr(merge_gate.settings, "merge_lock_timeout_s", 0, raising=False)
    okB, reasonB = merge_gate.acquire_merge_lease("orchestrator", "feature/B", "ORCH-2")
    assert okB is True
    assert "reclaimed" in reasonB


# ----------------------------------------------------------------- TC-A05
def test_defer_budget_is_bounded(monkeypatch):
    """The defer budget is a positive finite int -> exhaustion escalates (AC-A6)."""
    from src.config import settings
    assert isinstance(settings.merge_defer_max_attempts, int)
    assert settings.merge_defer_max_attempts > 0
    assert settings.merge_defer_delay_s > 0
