"""ORCH-022 / TC-16..TC-19, TC-21: the security sub-gate wired into advance_stage.

These are integration tests over src.stage_engine.advance_stage on the
deploy-staging -> deploy edge. The security verdict is injected by patching the
QG_CHECKS registry entry (the leaf scanner logic is unit-tested in
tests/test_security_gate.py), so we exercise the ENGINE behaviour:
  * FAIL -> rollback to development + enqueue developer + Plane comment + notify;
  * the rollback task_desc carries the verbatim findings (ORCH-046 pattern);
  * after MAX_DEVELOPER_RETRIES -> set_issue_blocked + Telegram, no bounce;
  * PASS -> the pipeline advances normally (no rollback, no noisy notify);
  * self-hosting safety: a FAIL never calls the deploy hook / restarts prod.

Network/Plane/Telegram side effects are mocked at the src.stage_engine level.
"""

import os
import tempfile

import pytest

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

from unittest.mock import MagicMock  # noqa: E402

import src.db as _db  # noqa: E402
from src.db import init_db, get_db  # noqa: E402
from src import stage_engine  # noqa: E402
from src.stage_engine import advance_stage  # noqa: E402

_BRANCH = "feature/ORCH-022-x"


# ---------------------------------------------------------------------------
# Fixtures (mirror tests/test_stage_engine.py)
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def fresh_db(monkeypatch):
    monkeypatch.setattr(_db.settings, "db_path", _test_db)
    if os.path.exists(_test_db):
        os.unlink(_test_db)
    init_db()
    yield


@pytest.fixture(autouse=True)
def silence_side_effects(monkeypatch):
    for name in (
        "notify_stage_change",
        "notify_qg_failure",
        "notify_approve_requested",
        "send_telegram",
        "plane_notify_stage",
        "plane_notify_qg",
        "plane_add_comment",
        "set_issue_in_review",
        "set_issue_needs_input",
        "set_issue_in_progress",
        "set_issue_blocked",
        "set_issue_done",
    ):
        monkeypatch.setattr(stage_engine, name, MagicMock())


def _make_task(stage, repo, branch=_BRANCH, wi="ORCH-022"):
    conn = get_db()
    cur = conn.execute(
        "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
        "VALUES (?, ?, ?, ?, ?)",
        (f"plane-{wi}", wi, repo, branch, stage),
    )
    task_id = cur.lastrowid
    conn.commit()
    conn.close()
    return task_id


def _stage(task_id):
    conn = get_db()
    row = conn.execute("SELECT stage FROM tasks WHERE id=?", (task_id,)).fetchone()
    conn.close()
    return row[0]


def _jobs():
    conn = get_db()
    rows = conn.execute("SELECT agent, repo, task_id FROM jobs ORDER BY id").fetchall()
    conn.close()
    return [dict(r) for r in rows]


def _job_contents():
    conn = get_db()
    rows = conn.execute("SELECT task_content FROM jobs ORDER BY id").fetchall()
    conn.close()
    return [r[0] for r in rows]


def _add_developer_runs(task_id, n):
    conn = get_db()
    for _ in range(n):
        conn.execute(
            "INSERT INTO agent_runs (task_id, agent) VALUES (?, 'developer')",
            (task_id,),
        )
    conn.commit()
    conn.close()


def _pass(*a, **k):
    return (True, "ok")


def _fail(reason):
    def _f(*a, **k):
        return (False, reason)
    return _f


def _qg_with_security(monkeypatch, security_result):
    """Patch QG_CHECKS so every gate passes EXCEPT the security gate, which returns
    ``security_result``. Keeps the deploy-staging edge reachable (check_staging_status
    passes) and isolates the security verdict under test."""
    patched = {k: _pass for k in stage_engine.QG_CHECKS}
    patched["check_security_gate"] = security_result
    monkeypatch.setattr(stage_engine, "QG_CHECKS", patched)


# ---------------------------------------------------------------------------
# TC-16 — FAIL -> rollback to development + enqueue developer + notify.
# ---------------------------------------------------------------------------
def test_tc16_fail_rolls_back_and_enqueues_developer(monkeypatch):
    """TC-16: security_status FAIL -> rollback deploy-staging -> development,
    enqueue developer, Plane comment + notify_qg_failure."""
    _qg_with_security(monkeypatch, _fail("2 secret(s): aws-key in src/x.py:3"))
    task_id = _make_task("deploy-staging", repo="enduro-trails")

    res = advance_stage(
        task_id, "deploy-staging", "enduro-trails", "ORCH-022", _BRANCH,
        finished_agent="deployer",
    )

    assert res.advanced is False
    assert res.rolled_back_to == "development"
    assert _stage(task_id) == "development"
    jobs = _jobs()
    assert len(jobs) == 1
    assert jobs[0]["agent"] == "developer"
    assert res.qg_name == "check_security_gate"
    # The deployer-authored Plane comment + the QG-failure notification fired.
    assert stage_engine.plane_add_comment.called
    assert stage_engine.notify_qg_failure.called


# ---------------------------------------------------------------------------
# TC-17 — the rollback task_desc carries the verbatim findings (ORCH-046).
# ---------------------------------------------------------------------------
def test_tc17_task_desc_has_verbatim_findings(monkeypatch, tmp_path):
    """TC-17: the re-launched developer's task_desc embeds the verbatim finding
    substance (not just a link), following the ORCH-046 pattern."""
    reason = "2 secret(s): aws-access-key in src/config.py:12"
    _qg_with_security(monkeypatch, _fail(reason))
    task_id = _make_task("deploy-staging", repo="enduro-trails")

    # Isolate the worktree base under tmp_path so this test never touches the real
    # shared /repos/_wt host path (PermissionError in CI). Mirrors the pattern in
    # tests/test_git_worktree.py / test_merge_gate.py.
    from src import git_worktree
    monkeypatch.setattr(git_worktree.settings, "worktrees_dir", str(tmp_path / "_wt"))

    # Seed a real 17-security-report.md in the worktree so extract_security_findings
    # has a verbatim body to excerpt.
    wt = stage_engine.get_worktree_path("enduro-trails", _BRANCH)
    report_dir = os.path.join(wt, "docs", "work-items", "ORCH-022")
    os.makedirs(report_dir, exist_ok=True)
    with open(os.path.join(report_dir, "17-security-report.md"), "w", encoding="utf-8") as f:
        f.write(
            "---\nsecurity_status: FAIL\nsecrets_found: 1\n---\n"
            "# Security Report — ORCH-022\n\n"
            "## Verdict\n1 secret(s): aws-access-key in src/config.py:12\n\n"
            "## Secrets\n- `src/config.py:12` — aws-access-key (match `AKIA…YZ`)\n\n"
            "## Dependencies (blocking)\n- None\n"
        )

    advance_stage(
        task_id, "deploy-staging", "enduro-trails", "ORCH-022", _BRANCH,
        finished_agent="deployer",
    )

    contents = _job_contents()
    assert len(contents) == 1
    desc = contents[0]
    # The verbatim reason AND the excerpted finding line are present.
    assert "aws-access-key in src/config.py:12" in desc
    assert "src/config.py:12" in desc
    # Plus the link to the full artefact.
    assert "17-security-report.md" in desc


# ---------------------------------------------------------------------------
# TC-18 — after MAX_DEVELOPER_RETRIES -> block + Telegram, no bounce.
# ---------------------------------------------------------------------------
def test_tc18_retry_cap_blocks_and_alerts(monkeypatch):
    """TC-18: after MAX_DEVELOPER_RETRIES developer attempts -> set_issue_blocked +
    Telegram alert; no infinite bounce (no new developer job)."""
    _qg_with_security(monkeypatch, _fail("blocking CVE"))
    task_id = _make_task("deploy-staging", repo="enduro-trails")
    _add_developer_runs(task_id, stage_engine.MAX_DEVELOPER_RETRIES)

    res = advance_stage(
        task_id, "deploy-staging", "enduro-trails", "ORCH-022", _BRANCH,
        finished_agent="deployer",
    )

    assert res.rolled_back_to == "development"
    assert res.alerted is True
    assert stage_engine.set_issue_blocked.called
    assert stage_engine.send_telegram.called
    # No further developer job past the cap.
    assert _jobs() == []


# ---------------------------------------------------------------------------
# TC-19 — PASS -> the pipeline advances normally.
# ---------------------------------------------------------------------------
def test_tc19_pass_advances_normally(monkeypatch):
    """TC-19: security_status PASS -> advance deploy-staging -> deploy with the
    deployer launched, no rollback, no QG-failure notification."""
    _qg_with_security(monkeypatch, lambda *a, **k: (True, "security clean"))
    task_id = _make_task("deploy-staging", repo="enduro-trails")

    res = advance_stage(
        task_id, "deploy-staging", "enduro-trails", "ORCH-022", _BRANCH,
        finished_agent="deployer",
    )

    assert res.advanced is True
    assert res.to_stage == "deploy"
    assert _stage(task_id) == "deploy"
    assert res.rolled_back_to is None
    # No noisy QG-failure notification on the happy path.
    assert not stage_engine.notify_qg_failure.called


# ---------------------------------------------------------------------------
# TC-21 — self-hosting safety: a FAIL never deploys / restarts prod.
# ---------------------------------------------------------------------------
def test_tc21_fail_never_triggers_deploy(monkeypatch):
    """TC-21: on a security FAIL the gate only rolls back + enqueues developer; it
    never calls the deploy hook / restarts the prod container (self-hosting safety)."""
    _qg_with_security(monkeypatch, _fail("secret found"))
    # Spy on the self-deploy entrypoints — none must be invoked on a FAIL.
    monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", MagicMock())
    monkeypatch.setattr(stage_engine.self_deploy, "self_deploy_applies", MagicMock(return_value=True))
    task_id = _make_task("deploy-staging", repo="orchestrator")

    res = advance_stage(
        task_id, "deploy-staging", "orchestrator", "ORCH-022", _BRANCH,
        finished_agent="deployer",
    )

    assert res.rolled_back_to == "development"
    # The security FAIL returns BEFORE the self-deploy block -> no deploy initiated.
    assert not stage_engine.self_deploy.initiate_deploy.called
    # Only the developer is re-enqueued; no deployer job.
    jobs = _jobs()
    assert all(j["agent"] == "developer" for j in jobs)
