"""Tests for the two pipeline-start bugs surfaced by the ET-006 live run.

BUG 1: issue.updated (status -> In Progress) ships a payload WITHOUT the
       description, so start_pipeline must pull it from the Plane issue API
       before QG-0 runs (otherwise QG-0 wrongly blocks the issue).

BUG 2a: M-6 derives work_item_id from the Plane sequence_id, which can collide.
        ensure_unique_work_item_id() must hand out the next FREE id instead of
        reusing one that is already in the tasks table.

BUG 2b: two tasks with an (artificially) identical work_item_id must not share a
        branch/worktree.

launcher / Gitea / Plane network are mocked. Real FastAPI endpoint via
TestClient for the BUG 1 end-to-end path.
"""

import os
import tempfile

_test_db = os.path.join(tempfile.gettempdir(), "test_orchestrator_pipeline_bugs.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, ensure_unique_work_item_id  # noqa: E402
from src import projects as P  # noqa: E402
from src.projects import reload_projects  # noqa: E402
from src.git_worktree import get_worktree_path  # noqa: E402

ENDURO_PLANE_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
IN_PROGRESS = "b873d9eb-993c-48cd-97ac-99a9b1623967"
BACKLOG = "113b24f6-cce8-4be9-9a22-a359b9cf0122"

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()
    yield
    reload_projects()
    if os.path.exists(_test_db):
        os.unlink(_test_db)


def _insert_task(work_item_id, branch, plane_id="x"):
    conn = get_db()
    conn.execute(
        "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, plane_issue_id) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        (plane_id, work_item_id, "enduro-trails", branch, "analysis", plane_id),
    )
    conn.commit()
    conn.close()


def _count(plane_id):
    conn = get_db()
    n = conn.execute("SELECT COUNT(*) FROM tasks WHERE plane_id=?", (plane_id,)).fetchone()[0]
    conn.close()
    return n


def _task(plane_id):
    conn = get_db()
    row = conn.execute("SELECT * FROM tasks WHERE plane_id=?", (plane_id,)).fetchone()
    conn.close()
    return row


# --------------------------------------------------------------------------- #
# BUG 1
# --------------------------------------------------------------------------- #
def _to_in_progress_no_desc(plane_id="bug1"):
    """issue.updated payload WITHOUT description (only changed fields)."""
    return client.post("/webhook/plane", json={
        "event": "issue", "action": "updated",
        "data": {
            "id": plane_id, "name": "A valid backlog item title",
            # NO description / description_stripped here, exactly like Plane sends
            # on a status change.
            "project": ENDURO_PLANE_ID,
            "state": {"id": IN_PROGRESS, "name": "In Progress", "group": "started"},
        },
        "activity": {"field": "state", "new_value": IN_PROGRESS, "old_value": BACKLOG},
    })


@patch("src.webhooks.plane.enqueue_job", return_value=1)
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
@patch("src.plane_sync.fetch_issue_sequence_id", return_value=42)
@patch("src.plane_sync.fetch_issue_fields",
       return_value=("A valid backlog item title",
                     "This is a sufficiently long description fetched from Plane API."))
def test_status_start_fetches_description(
    mock_fields, mock_seq, mock_branch, mock_docs, mock_enqueue
):
    """BUG 1: empty description in payload -> start_pipeline pulls it from the
    Plane API (single fetch_issue_fields GET) -> QG-0 passes -> task created +
    analyst enqueued (NOT blocked)."""
    resp = _to_in_progress_no_desc("bug1")
    assert resp.status_code == 200
    # name + description were pulled from the API in one call
    mock_fields.assert_called_once()
    # QG-0 passed -> task created and analyst launched (NOT set_issue_blocked)
    assert _count("bug1") == 1
    assert _task("bug1")["stage"] == "analysis"
    mock_enqueue.assert_called_once()
    assert mock_enqueue.call_args.args[0] == "analyst"


@patch("src.webhooks.plane.enqueue_job", return_value=1)
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
@patch("src.plane_sync.fetch_issue_sequence_id", return_value=42)
@patch("src.plane_sync.fetch_issue_fields", return_value=("", ""))
def test_status_start_empty_api_still_blocks(
    mock_fields, mock_seq, mock_branch, mock_docs, mock_enqueue
):
    """BUG 1 negative path: if the API also returns empty, QG-0 legitimately
    fails -> NO task is created (truly empty ticket)."""
    resp = _to_in_progress_no_desc("bug1-empty")
    assert resp.status_code == 200
    mock_fields.assert_called_once()
    assert _count("bug1-empty") == 0
    mock_enqueue.assert_not_called()


# --------------------------------------------------------------------------- #
# BUG 2a
# --------------------------------------------------------------------------- #
def test_work_item_id_uniqueness():
    """BUG 2a: if ET-006 is already in tasks, the guard returns the next free
    id (ET-007), not ET-006 again."""
    _insert_task("ET-006", "feature/ET-006-gpx-upload", plane_id="old")
    assert ensure_unique_work_item_id("ET-006", "enduro-trails") == "ET-007"

    # ET-006 AND ET-007 taken -> next free is ET-008.
    _insert_task("ET-007", "feature/ET-007-something", plane_id="old2")
    assert ensure_unique_work_item_id("ET-006", "enduro-trails") == "ET-008"

    # A free id is returned unchanged.
    assert ensure_unique_work_item_id("ET-099", "enduro-trails") == "ET-099"

    # Per-repo isolation: a different repo with the same id is not a collision.
    assert ensure_unique_work_item_id("ET-006", "other-repo") == "ET-006"


@patch("src.webhooks.plane.enqueue_job", return_value=1)
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
@patch("src.plane_sync.fetch_issue_sequence_id", return_value=6)
@patch("src.plane_sync.fetch_issue_fields",
       return_value=("Popup enduro trails feature",
                     "A sufficiently long description for QG-0 to pass cleanly."))
def test_collision_reassigns_in_start_pipeline(
    mock_fields, mock_seq, mock_branch, mock_docs, mock_enqueue
):
    """BUG 2a end-to-end: ET-006 already exists -> a new In Progress issue whose
    Plane sequence_id is also 6 must NOT reuse ET-006."""
    _insert_task("ET-006", "feature/ET-006-gpx-upload", plane_id="task8")
    resp = client.post("/webhook/plane", json={
        "event": "issue", "action": "updated",
        "data": {
            "id": "task25", "name": "Popup enduro trails feature",
            "description_stripped": "A sufficiently long description for QG-0.",
            "project": ENDURO_PLANE_ID,
            "state": {"id": IN_PROGRESS, "name": "In Progress", "group": "started"},
        },
        "activity": {"field": "state", "new_value": IN_PROGRESS, "old_value": BACKLOG},
    })
    assert resp.status_code == 200
    new_id = _task("task25")["work_item_id"]
    assert new_id != "ET-006"
    assert new_id == "ET-007"


# --------------------------------------------------------------------------- #
# BUG 2b
# --------------------------------------------------------------------------- #
def test_worktree_per_task():
    """BUG 2b: two tasks must not resolve to the same worktree path. With the
    uniqueness guard the branches differ, so the worktree paths differ too."""
    _insert_task("ET-006", "feature/ET-006-gpx-upload", plane_id="task8")
    # The second task gets a unique id via the guard...
    new_id = ensure_unique_work_item_id("ET-006", "enduro-trails")
    assert new_id == "ET-007"
    branch_a = "feature/ET-006-gpx-upload"
    branch_b = f"feature/{new_id}-popup-enduro-trails"

    wt_a = get_worktree_path("enduro-trails", branch_a)
    wt_b = get_worktree_path("enduro-trails", branch_b)
    assert wt_a != wt_b, "two tasks must not share a worktree path"
