"""Status-only verdict model (bug 3 fix).

The comment-based control mechanism (:approved: / :rejected: / answer-to-questions)
was removed. The pipeline is driven SOLELY by Plane status changes. These tests
lock in the new behaviour:

  * test_inreview_comment_does_not_revert       — bug 3 root: an In Review task,
    any comment arrives -> status NOT reverted, no agent launched.
  * test_any_comment_no_pipeline_action         — :approved: / :rejected: / plain
    text comment -> no status change, no enqueue.
  * test_approved_status_advances_without_inprogress_reset — Approved status
    advances WITHOUT an intermediate set_issue_in_progress reset.
  * test_rejected_status_pulls_reason_from_comment — Rejected status pulls the
    reason from the issue's latest comment (mocked GET comments).
"""

import os
import tempfile

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

import pytest  # noqa: E402
from unittest.mock import patch, AsyncMock  # noqa: E402
from fastapi.testclient import TestClient  # noqa: E402

from src.main import app  # noqa: E402
from src.db import init_db, get_db  # noqa: E402
from src import projects as P  # noqa: E402
from src.projects import reload_projects  # noqa: E402

ENDURO_PLANE_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
APPROVED = "a519a341-dada-4a91-8910-7604f82b79c5"
REJECTED = "ba958f3c-5db5-461d-8f82-89425e413b97"
IN_REVIEW = "38fb1f64-aa1e-48a3-92e0-0b109679046b"

client = TestClient(app)


@pytest.fixture(autouse=True)
def setup(monkeypatch):
    monkeypatch.setattr(P.settings, "db_path", _test_db)
    import src.db as _db
    monkeypatch.setattr(_db.settings, "db_path", _test_db)
    if os.path.exists(_test_db):
        os.unlink(_test_db)
    init_db()
    monkeypatch.setattr("src.webhooks.plane.verify_plane_signature", lambda body, sig: True)
    registry_json = (
        f'[{{"plane_project_id": "{ENDURO_PLANE_ID}", "repo": "enduro-trails",'
        f' "work_item_prefix": "ET", "name": "enduro-trails"}}]'
    )
    monkeypatch.setattr(P.settings, "projects_json", registry_json)
    reload_projects()
    # Seed a task at the 'review' stage for plane_id 'r-1'.
    conn = get_db()
    conn.execute(
        "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, plane_issue_id) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        ("r-1", "ET-700", "enduro-trails", "feature/ET-700-x", "review", "r-1"),
    )
    conn.commit()
    conn.close()
    yield
    reload_projects()
    if os.path.exists(_test_db):
        os.unlink(_test_db)


class _FakeResp:
    def __init__(self, status_code, payload):
        self.status_code = status_code
        self._payload = payload

    def json(self):
        return self._payload


def _comment(text, plane_id="r-1"):
    return client.post("/webhook/plane", json={
        "event": "issue_comment", "action": "created",
        "data": {"work_item_id": plane_id, "comment_stripped": text,
                 "project": ENDURO_PLANE_ID},
    })


def _status(state_id, plane_id="r-1", old="prev"):
    return client.post("/webhook/plane", json={
        "event": "issue", "action": "updated",
        "data": {
            "id": plane_id, "name": "Status task", "project": ENDURO_PLANE_ID,
            "state": {"id": state_id, "name": "X", "group": "started"},
        },
        "activity": {"field": "state", "new_value": state_id, "old_value": old},
    })


def _stage(plane_id="r-1"):
    conn = get_db()
    row = conn.execute("SELECT stage FROM tasks WHERE plane_id=?", (plane_id,)).fetchone()
    conn.close()
    return row[0] if row else None


# --------------------------------------------------------------------------- #
# Bug 3 root: In Review must not revert on a comment.
# --------------------------------------------------------------------------- #
@patch("src.webhooks.plane.enqueue_job")
@patch("src.plane_sync.set_issue_in_progress")
@patch("src.plane_sync._set_issue_state_direct")
@patch("src.plane_sync.update_issue_state")
def test_inreview_comment_does_not_revert(
    mock_update_state, mock_set_direct, mock_sip, mock_enqueue
):
    """Bug 3: task in In Review, ANY comment arrives -> status NOT reverted to
    In Progress, NO agent launched. The analyst's own 'waiting for approval'
    comment used to echo back and self-hit -> reverted In Review -> In Progress.
    """
    # analyst's own echo comment
    resp = _comment("Готово, жду approved")
    assert resp.status_code == 200
    # no status changes whatsoever
    mock_sip.assert_not_called()
    mock_set_direct.assert_not_called()
    mock_update_state.assert_not_called()
    # no agent launched
    mock_enqueue.assert_not_called()
    # stage untouched
    assert _stage() == "review"


# --------------------------------------------------------------------------- #
# Any comment -> zero pipeline side-effects.
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("text", [":approved:", ":rejected: bad", "plain text", ""])
@patch("src.webhooks.plane.enqueue_job")
@patch("src.webhooks.plane._try_advance_stage", new_callable=AsyncMock)
@patch("src.webhooks.plane._rollback_stage", new_callable=AsyncMock)
@patch("src.plane_sync.set_issue_in_progress")
@patch("src.plane_sync._set_issue_state_direct")
def test_any_comment_no_pipeline_action(
    mock_set_direct, mock_sip, mock_rollback, mock_advance, mock_enqueue, text
):
    resp = _comment(text)
    assert resp.status_code == 200
    mock_advance.assert_not_called()
    mock_rollback.assert_not_called()
    mock_sip.assert_not_called()
    mock_set_direct.assert_not_called()
    mock_enqueue.assert_not_called()
    assert _stage() == "review"


# --------------------------------------------------------------------------- #
# Approved status advances WITHOUT in_progress reset.
# --------------------------------------------------------------------------- #
@patch("src.plane_sync.set_issue_in_progress")
@patch("src.webhooks.plane._try_advance_stage", new_callable=AsyncMock)
def test_approved_status_advances_without_inprogress_reset(mock_advance, mock_sip):
    resp = _status(APPROVED)
    assert resp.status_code == 200
    mock_advance.assert_awaited_once()
    # work_item_id passed positionally
    assert "ET-700" in mock_advance.call_args.args
    # bug 3 (cause B): NO intermediate set_issue_in_progress before advance.
    mock_sip.assert_not_called()


# --------------------------------------------------------------------------- #
# Rejected status pulls reason from latest comment.
# --------------------------------------------------------------------------- #
@patch("src.webhooks.plane.httpx.get")
@patch("src.webhooks.plane._rollback_stage", new_callable=AsyncMock)
def test_rejected_status_pulls_reason_from_comment(mock_rollback, mock_get):
    mock_get.return_value = _FakeResp(200, {"results": [
        {"comment_stripped": "old comment", "created_at": "2026-06-03T09:00:00Z"},
        {"comment_html": "<p>Needs more test coverage</p>",
         "created_at": "2026-06-03T11:30:00Z"},
    ]})
    resp = _status(REJECTED)
    assert resp.status_code == 200
    mock_rollback.assert_awaited_once()
    reason = mock_rollback.call_args.args[-1]
    # latest by created_at, HTML stripped
    assert "Needs more test coverage" in reason
    assert "<p>" not in reason


@patch("src.webhooks.plane.httpx.get")
@patch("src.webhooks.plane._rollback_stage", new_callable=AsyncMock)
def test_rejected_status_no_comment_uses_fallback(mock_rollback, mock_get):
    mock_get.return_value = _FakeResp(200, {"results": []})
    resp = _status(REJECTED)
    assert resp.status_code == 200
    mock_rollback.assert_awaited_once()
    reason = mock_rollback.call_args.args[-1]
    assert "no reason comment" in reason
