"""ORCH-026 Level B — declarative task dependencies (TC-B01/B02/B05/B07).

Real SQLite (tmp db). We drive tasks + job_deps directly and assert:
  TC-B01  add_dependency declares an edge; get_dependencies resolves it; a
          self-edge is rejected; never-raise on a bad input.
  TC-B02  is_task_ready: a task with an un-done predecessor is NOT ready; when
          every predecessor reaches 'done' it becomes ready.
  TC-B05  claim_next_job does NOT claim a dep-blocked job (no slot taken); once
          the predecessor is 'done' the job becomes claimable.
  TC-B07  reconciler skip helper: is_task_ready=False is honoured (the gate task
          is left waiting).
"""
import os
import tempfile

import pytest

os.environ["ORCH_DB_PATH"] = os.path.join(tempfile.gettempdir(), "test_orch026_task_deps.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, enqueue_job, claim_next_job  # noqa: E402
from src import task_deps  # noqa: E402


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


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


def _set_stage(task_id, stage):
    conn = get_db()
    conn.execute("UPDATE tasks SET stage=? WHERE id=?", (stage, task_id))
    conn.commit()
    conn.close()


# ----------------------------------------------------------------- TC-B01
def test_add_dependency_declares_and_resolves():
    a = _make_task(work_item_id="ORCH-10", stage="development")
    b = _make_task(work_item_id="ORCH-11", stage="development")
    assert db.add_dependency(b, a) is True
    assert db.get_dependencies(b) == [a]
    # Idempotent: re-declaring the same edge is a no-op.
    assert db.add_dependency(b, a) is False


def test_self_edge_rejected():
    a = _make_task(work_item_id="ORCH-12")
    assert db.add_dependency(a, a) is False
    assert db.get_dependencies(a) == []


def test_add_dependency_never_raises_on_bad_input():
    assert db.add_dependency(None, 1) is False
    assert db.add_dependency(1, None) is False


# ----------------------------------------------------------------- TC-B02
def test_is_task_ready_blocked_then_ready():
    a = _make_task(work_item_id="ORCH-20", stage="development")
    b = _make_task(work_item_id="ORCH-21", stage="development")
    db.add_dependency(b, a)

    ready, waiting = task_deps.is_task_ready(b)
    assert ready is False
    assert "ORCH-20" in waiting

    _set_stage(a, "done")
    ready2, waiting2 = task_deps.is_task_ready(b)
    assert ready2 is True
    assert waiting2 == []


def test_is_task_ready_no_deps_is_ready():
    a = _make_task(work_item_id="ORCH-22")
    ready, waiting = task_deps.is_task_ready(a)
    assert ready is True and waiting == []


# ----------------------------------------------------------------- TC-B05
def test_claim_skips_dep_blocked_job():
    a = _make_task(work_item_id="ORCH-30", stage="development")
    b = _make_task(work_item_id="ORCH-31", stage="development")
    db.add_dependency(b, a)

    job_b = enqueue_job("developer", "orchestrator", "do B", task_id=b)
    # B is blocked by un-done A -> claim must NOT pick it (no slot taken).
    claimed = claim_next_job()
    assert claimed is None, "dep-blocked job must not be claimed"

    # A finishes -> B becomes claimable.
    _set_stage(a, "done")
    claimed2 = claim_next_job()
    assert claimed2 is not None
    assert claimed2["id"] == job_b


def test_claim_prefers_unblocked_job_over_blocked():
    a = _make_task(work_item_id="ORCH-40", stage="development")
    b = _make_task(work_item_id="ORCH-41", stage="development")
    c = _make_task(work_item_id="ORCH-42", stage="development")
    db.add_dependency(b, a)  # b blocked by a

    job_b = enqueue_job("developer", "orchestrator", "B", task_id=b)  # older id
    job_c = enqueue_job("developer", "orchestrator", "C", task_id=c)  # not blocked

    claimed = claim_next_job()
    assert claimed is not None
    assert claimed["id"] == job_c, "blocked B skipped, unblocked C claimed"
    assert job_b  # referenced


# ----------------------------------------------------------------- TC-B07
def test_reconciler_skip_helper_honours_block(monkeypatch):
    """The reconciler reads is_task_ready; a not-ready task must be skipped."""
    from src import reconciler as rec
    a = _make_task(work_item_id="ORCH-50", stage="development")
    b = _make_task(work_item_id="ORCH-51", stage="development")
    db.add_dependency(b, a)

    advanced = {"called": False}
    monkeypatch.setattr(rec, "advance_if_gate_passed",
                        lambda *a, **k: advanced.__setitem__("called", True),
                        raising=False)
    monkeypatch.setattr(rec, "has_active_job_for_task", lambda tid: False, raising=False)
    monkeypatch.setattr(rec, "developer_retry_count", lambda tid: 0, raising=False)
    monkeypatch.setattr(rec.settings, "task_deps_enabled", True, raising=False)
    monkeypatch.setattr(rec.settings, "reconcile_enabled", True, raising=False)
    monkeypatch.setattr(rec.settings, "reconcile_grace_default_s", 0, raising=False)

    r = rec.Reconciler()
    # Bypass Guard 2 (networked) so we isolate Guard 3.
    monkeypatch.setattr(r, "_is_blocked_or_needs_input", lambda task: False)

    task_row = {"id": b, "stage": "development", "repo": "orchestrator",
                "work_item_id": "ORCH-51", "branch": "feature/ORCH-51", "age_s": 9999}
    r._reconcile_gate_task(task_row)
    assert advanced["called"] is False, "dep-blocked task must not be advanced (B-5)"
