"""M-6: work_item_id derived from Plane sequence_id (source of truth = Plane).

Covers:
  * fetch_issue_sequence_id returns int on a valid Plane response (mocked httpx);
  * returns None on network error / missing field WITHOUT raising;
  * handle_work_item_created uses prefix-NNN when seq is available, and falls
    back to get_next_work_item_id when seq is None (Plane down => autonomy);
  * find_issue_id no longer hardcodes 'ET-' and matches an arbitrary prefix
    (e.g. ORCH-005) by sequence_id.
"""

import os
import tempfile

import pytest

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

from unittest.mock import patch, AsyncMock, MagicMock  # 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
import src.plane_sync as plane_sync  # noqa: E402

ORCH_PLANE_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
ENDURO_PLANE_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"

# ORCH-39: after ORCH-10 the webhook resolves Plane state UUIDs per-project via
# get_project_states(project_id). Mock it deterministically (no network) and
# send each request with the UUID that matches its own project.
_PROJECT_STATES = {
    ENDURO_PLANE_ID: {
        "in_progress": "b873d9eb-993c-48cd-97ac-99a9b1623967",
        # ORCH-066: To Analyse is the start trigger; with the status absent it
        # aliases to in_progress (the real get_project_states fallback).
        "to_analyse": "b873d9eb-993c-48cd-97ac-99a9b1623967",
        "approved": "a519a341-dada-4a91-8910-7604f82b79c5",
        "rejected": "ba958f3c-5db5-461d-8f82-89425e413b97",
    },
    ORCH_PLANE_ID: {
        "in_progress": "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
        "to_analyse": "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
        "approved": "63f2c8fe-dcda-4ace-952f-dd88bd0118ff",
        "rejected": "4c769e90-bf80-4a52-b97a-e1c84904bfc3",
    },
}


def _fake_get_project_states(project_id):
    return _PROJECT_STATES.get(project_id, _PROJECT_STATES[ENDURO_PLANE_ID])


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)

    # ORCH-39: deterministic per-project Plane states, clean cache per test.
    plane_sync.reload_project_states()
    monkeypatch.setattr(plane_sync, "get_project_states", _fake_get_project_states)

    registry_json = (
        f'[{{"plane_project_id": "{ENDURO_PLANE_ID}", "repo": "enduro-trails",'
        f' "work_item_prefix": "ET", "name": "enduro-trails"}},'
        f' {{"plane_project_id": "{ORCH_PLANE_ID}", "repo": "orchestrator",'
        f' "work_item_prefix": "ORCH", "name": "orchestrator"}}]'
    )
    monkeypatch.setattr(P.settings, "projects_json", registry_json)
    reload_projects()

    yield

    reload_projects()
    plane_sync.reload_project_states()
    if os.path.exists(_test_db):
        os.unlink(_test_db)


def _mock_resp(json_body, status=200):
    m = MagicMock()
    m.json.return_value = json_body
    m.raise_for_status.return_value = None
    if status >= 400:
        def _raise():
            raise RuntimeError(f"HTTP {status}")
        m.raise_for_status.side_effect = _raise
    return m


# ---------------------------------------------------------------------------
# fetch_issue_sequence_id
# ---------------------------------------------------------------------------

def test_fetch_sequence_id_returns_int():
    with patch.object(plane_sync.httpx, "get", return_value=_mock_resp({"sequence_id": 42})):
        seq = plane_sync.fetch_issue_sequence_id("issue-uuid", "proj-uuid")
    assert seq == 42
    assert isinstance(seq, int)


def test_fetch_sequence_id_network_error_returns_none():
    with patch.object(plane_sync.httpx, "get", side_effect=RuntimeError("connection refused")):
        seq = plane_sync.fetch_issue_sequence_id("issue-uuid", "proj-uuid")
    assert seq is None  # must not raise


def test_fetch_sequence_id_missing_field_returns_none():
    with patch.object(plane_sync.httpx, "get", return_value=_mock_resp({"error": "not found"})):
        seq = plane_sync.fetch_issue_sequence_id("missing-uuid", "proj-uuid")
    assert seq is None


# ---------------------------------------------------------------------------
# handle_work_item_created: seq available -> prefix-NNN
# ---------------------------------------------------------------------------

# Feature 1: pipeline starts on a status change to In Progress, not on creation.
# ORCH-39: in_progress UUID is project-specific; derive it from the project.
def _post(plane_id, plane_project_id=ORCH_PLANE_ID, name="A valid work item title"):
    in_progress = _fake_get_project_states(plane_project_id)["in_progress"]
    return client.post(
        "/webhook/plane",
        json={
            "event": "issue",
            "action": "updated",
            "data": {
                "id": plane_id,
                "name": name,
                "description_stripped": "This is a sufficiently long description.",
                "project": plane_project_id,
                "state": {"id": in_progress, "name": "In Progress", "group": "started"},
            },
        },
    )


@patch("src.webhooks.plane.launcher")
@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=7)
def test_created_uses_plane_sequence_id(mock_fetch, mock_branch, mock_docs, mock_launcher):
    mock_launcher.launch.return_value = 1
    resp = _post("seq-issue")
    assert resp.status_code == 200
    conn = get_db()
    task = conn.execute("SELECT work_item_id FROM tasks WHERE plane_id='seq-issue'").fetchone()
    conn.close()
    assert task is not None
    assert task["work_item_id"] == "ORCH-007"
    mock_fetch.assert_called_once()


@patch("src.webhooks.plane.launcher")
@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=None)
@patch("src.webhooks.plane.get_next_work_item_id", return_value="ORCH-099")
def test_created_falls_back_to_db_when_plane_down(
    mock_next, mock_fetch, mock_branch, mock_docs, mock_launcher
):
    """Plane unavailable (seq=None) => fall back to DB increment; task still created."""
    mock_launcher.launch.return_value = 1
    resp = _post("fallback-issue")
    assert resp.status_code == 200
    conn = get_db()
    task = conn.execute("SELECT work_item_id FROM tasks WHERE plane_id='fallback-issue'").fetchone()
    conn.close()
    assert task is not None  # autonomy: Plane down does not block creation
    assert task["work_item_id"] == "ORCH-099"
    mock_next.assert_called_once()


# ---------------------------------------------------------------------------
# find_issue_id: no hardcoded ET- prefix, matches arbitrary prefix by seq
# ---------------------------------------------------------------------------

def test_find_issue_id_matches_arbitrary_prefix_by_sequence():
    """ORCH-005 must resolve via the issue whose sequence_id == 5 (no ET- assumption)."""
    issues = {"results": [
        {"id": "uuid-a", "sequence_id": 3, "name": "something"},
        {"id": "uuid-b", "sequence_id": 5, "name": "ORCH-005: target"},
        {"id": "uuid-c", "sequence_id": 9, "name": "other"},
    ]}
    # No DB row for this work_item_id => goes to the Plane API search branch.
    with patch.object(plane_sync.httpx, "get", return_value=_mock_resp(issues)):
        found = plane_sync.find_issue_id("ORCH-005", project_id="proj-uuid")
    assert found == "uuid-b"


def test_find_issue_id_matches_et_prefix_too():
    """Backward compat: ET-002 still resolves by sequence_id == 2."""
    issues = {"results": [
        {"id": "uuid-x", "sequence_id": 2, "name": "ET item"},
        {"id": "uuid-y", "sequence_id": 7, "name": "other"},
    ]}
    with patch.object(plane_sync.httpx, "get", return_value=_mock_resp(issues)):
        found = plane_sync.find_issue_id("ET-002", project_id="proj-uuid")
    assert found == "uuid-x"
