"""ORCH-066 fail-closed (CRITICAL) — the new status model must never wedge the
pipeline when the 6 Plane statuses are absent or Plane is unreachable.

  * TC-16 (AC-16, BR-12) — a project WITHOUT the new statuses resolves each new
    logical key to its OWN base UUID (to_analyse=in_progress, code_review=review,
    awaiting_deploy=in_review, monitoring=done); no exception.
  * TC-17 (AC-16)        — Plane API down -> get_project_states falls back to
    _DEFAULT_STATES; every set_issue_* helper is never-raise.
  * TC-18 (AC-17)        — enduro In Progress STILL starts the pipeline through
    the to_analyse alias (= in_progress UUID).

httpx is mocked; no network.
"""

import os

os.environ.setdefault("ORCH_PLANE_API_URL", "http://plane.local")
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
os.environ.setdefault("ORCH_PLANE_WORKSPACE_SLUG", "test-ws")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")

from unittest.mock import patch, MagicMock, AsyncMock  # noqa: E402

import pytest  # noqa: E402

from src import plane_sync as PS  # noqa: E402

ENDURO_PROJECT_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"

# An enduro-style states response: the 6 ORCH-066 statuses are NOT created.
_ENDURO_BASE = {
    "Backlog": "backlog-u", "Todo": "todo-u", "In Progress": "ip-u",
    "Review": "review-u", "In Review": "inrev-u", "Approved": "appr-u",
    "Rejected": "rej-u", "Done": "done-u", "Needs Input": "ni-u",
    "Blocked": "blk-u",
}


def _states_response(name_to_uuid):
    return {"results": [{"id": uid, "name": name} for name, uid in name_to_uuid.items()]}


def _fake_resp(data, status=200):
    m = MagicMock()
    m.status_code = status
    m.json.return_value = data
    m.raise_for_status.return_value = None
    return m


@pytest.fixture(autouse=True)
def _reset_cache():
    PS.reload_project_states()
    yield
    PS.reload_project_states()


# ---------------------------------------------------------------------------
# TC-16 (AC-16 / BR-12): partial project -> alias to its own base UUIDs, no raise.
# ---------------------------------------------------------------------------
def test_tc16_partial_project_aliases_to_base_uuids():
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_resp(_states_response(_ENDURO_BASE))
        states = PS.get_project_states(ENDURO_PROJECT_ID)

    # The new keys degrade to THIS project's base UUIDs (not foreign defaults).
    assert states["to_analyse"] == states["in_progress"] == "ip-u"
    assert states["analysis"] == "ip-u"
    assert states["code_review"] == states["review"] == "review-u"
    assert states["awaiting_deploy"] == states["in_review"] == "inrev-u"
    assert states["deploying"] == "ip-u"
    assert states["monitoring"] == states["done"] == "done-u"


# ---------------------------------------------------------------------------
# TC-17 (AC-16): Plane API down -> _DEFAULT_STATES; set_issue_* never-raise.
# ---------------------------------------------------------------------------
def test_tc17_api_down_falls_back_to_defaults():
    with patch("src.plane_sync.httpx.get", side_effect=Exception("plane down")):
        states = PS.get_project_states(ENDURO_PROJECT_ID)
    assert states is PS._DEFAULT_STATES
    # All new keys exist in the defaults (so callers never KeyError).
    for k in ("to_analyse", "analysis", "code_review", "awaiting_deploy",
              "deploying", "monitoring"):
        assert k in states


def test_tc17_set_issue_helpers_never_raise_when_issue_missing():
    # find_issue_id returns None (issue not in Plane) -> helpers log + return,
    # they must NOT raise. Covers every ORCH-066 setter.
    setters = [
        PS.set_issue_analysis, PS.set_issue_code_review,
        PS.set_issue_awaiting_deploy, PS.set_issue_deploying,
        PS.set_issue_monitoring,
    ]
    with patch("src.plane_sync._resolve_project_id", return_value="proj-1"), \
         patch("src.plane_sync.get_project_states", return_value=PS._DEFAULT_STATES), \
         patch("src.plane_sync.find_issue_id", return_value=None), \
         patch("src.plane_sync.httpx.patch") as mock_patch:
        for setter in setters:
            setter("ET-1")  # must not raise
        # No PATCH issued because the issue could not be resolved.
        mock_patch.assert_not_called()


def test_tc17_set_issue_helpers_never_raise_when_patch_errors():
    # The PATCH itself blows up -> _set_issue_state_direct swallows it.
    with patch("src.plane_sync._resolve_project_id", return_value="proj-1"), \
         patch("src.plane_sync.get_project_states", return_value=PS._DEFAULT_STATES), \
         patch("src.plane_sync.find_issue_id", return_value="issue-uuid"), \
         patch("src.plane_sync.httpx.patch", side_effect=Exception("boom")):
        PS.set_issue_monitoring("ET-1")  # must not raise


# ---------------------------------------------------------------------------
# TC-18 (AC-17): enduro In Progress still starts the pipeline via to_analyse alias.
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_tc18_enduro_in_progress_still_starts_via_alias():
    from src.webhooks.plane import handle_issue_updated

    with patch("src.plane_sync.httpx.get") as mock_get, \
         patch("src.webhooks.plane.handle_status_start", new_callable=AsyncMock) as mock_start, \
         patch("src.webhooks.plane.handle_verdict", new_callable=AsyncMock) as mock_verdict:
        mock_get.return_value = _fake_resp(_states_response(_ENDURO_BASE))
        # enduro never created 'To Analyse' -> to_analyse aliases In Progress (ip-u).
        data = {"id": "et-issue", "state": {"id": "ip-u", "name": "In Progress"}}
        await handle_issue_updated(data, ENDURO_PROJECT_ID)

    mock_start.assert_called_once()
    mock_verdict.assert_not_called()
