"""ORCH-026 Level B — dependency cycle / deadlock detection (TC-B03, TC-B04).

TC-B03  detect_cycle is deterministic: A->B->A (and longer) is detected; an
        acyclic graph yields None. Pure function (edges injected).
TC-B04  a detected cycle escalates: set_issue_blocked + a Telegram alert, with
        no worker crash and no blocking of other tasks (never-raise).
"""
import os
import tempfile

import pytest

os.environ["ORCH_DB_PATH"] = os.path.join(tempfile.gettempdir(), "test_orch026_cycles.db")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")

import src.db as db  # noqa: E402
from src.db import init_db, get_db  # noqa: E402
from src import task_deps  # noqa: E402


@pytest.fixture(autouse=True)
def fresh_db(tmp_path, monkeypatch):
    dbfile = tmp_path / "cycles.db"
    monkeypatch.setattr(db.settings, "db_path", str(dbfile))
    monkeypatch.setattr(db.settings, "task_deps_enabled", True, raising=False)
    init_db()
    yield


# ----------------------------------------------------------------- TC-B03
def test_detect_two_node_cycle():
    # edge (B, A) means "B depends on A"; 1->2 and 2->1 is a 2-cycle.
    edges = [(1, 2), (2, 1)]
    cyc = task_deps.detect_cycle(1, edges=edges)
    assert cyc is not None
    assert cyc[0] == cyc[-1]  # closed cycle
    assert set(cyc) == {1, 2}


def test_detect_longer_cycle():
    edges = [(1, 2), (2, 3), (3, 1)]
    cyc = task_deps.detect_cycle(1, edges=edges)
    assert cyc is not None
    assert set(cyc) >= {1, 2, 3}


def test_acyclic_graph_has_no_cycle():
    edges = [(1, 2), (2, 3), (1, 3)]  # DAG
    assert task_deps.detect_cycle(1, edges=edges) is None
    assert task_deps.find_any_cycle(edges=edges) is None


def test_find_any_cycle_scans_whole_graph():
    # A disconnected cycle 10<->11 not reachable from node 1.
    edges = [(1, 2), (10, 11), (11, 10)]
    assert task_deps.detect_cycle(1, edges=edges) is None
    cyc = task_deps.find_any_cycle(edges=edges)
    assert cyc is not None
    assert set(cyc) == {10, 11}


def test_detect_cycle_never_raises_on_garbage():
    assert task_deps.detect_cycle(None) is None
    # Malformed edge list -> swallowed -> None.
    assert task_deps.detect_cycle(1, edges="not-a-list") is None


# ----------------------------------------------------------------- TC-B04
def _make_task(work_item_id, stage="development"):
    conn = get_db()
    cur = conn.execute(
        "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
        "VALUES (?, ?, ?, ?, ?)",
        (work_item_id, work_item_id, "orchestrator", f"feature/{work_item_id}", stage),
    )
    tid = cur.lastrowid
    conn.commit()
    conn.close()
    return tid


def test_handle_cycle_blocks_and_alerts(monkeypatch):
    a = _make_task("ORCH-60")
    b = _make_task("ORCH-61")
    db.add_dependency(a, b)
    db.add_dependency(b, a)  # cycle a<->b

    blocked = []
    alerts = []
    import src.plane_sync as plane_sync
    import src.notifications as notifications
    monkeypatch.setattr(plane_sync, "set_issue_blocked",
                        lambda wi, *a, **k: blocked.append(wi), raising=False)
    monkeypatch.setattr(notifications, "send_telegram",
                        lambda text, *a, **k: alerts.append(text), raising=False)

    cyc = task_deps.detect_cycle(a)
    assert cyc is not None
    ok = task_deps.handle_cycle(cyc)
    assert ok is True
    assert set(blocked) == {"ORCH-60", "ORCH-61"}
    assert len(alerts) == 1
    assert "ORCH-60" in alerts[0] and "ORCH-61" in alerts[0]


def test_handle_cycle_never_raises_when_notify_fails(monkeypatch):
    a = _make_task("ORCH-70")
    b = _make_task("ORCH-71")
    db.add_dependency(a, b)
    db.add_dependency(b, a)
    import src.plane_sync as plane_sync
    import src.notifications as notifications

    def _boom(*a, **k):
        raise RuntimeError("plane down")

    monkeypatch.setattr(plane_sync, "set_issue_blocked", _boom, raising=False)
    monkeypatch.setattr(notifications, "send_telegram", _boom, raising=False)
    cyc = task_deps.detect_cycle(a)
    # Must not propagate the exception (AC-G1).
    assert task_deps.handle_cycle(cyc) in (True, False)


def test_declare_dependency_escalates_cycle(monkeypatch):
    """declare_dependency surfaces a freshly-introduced cycle at declaration."""
    a = _make_task("ORCH-80")
    b = _make_task("ORCH-81")
    handled = []
    monkeypatch.setattr(task_deps, "handle_cycle",
                        lambda cyc: handled.append(cyc), raising=False)
    assert task_deps.declare_dependency(a, b) is True
    assert handled == []  # no cycle yet
    # Closing the loop -> handle_cycle invoked.
    assert task_deps.declare_dependency(b, a) is True
    assert len(handled) == 1
