"""ORCH-10: per-project Plane state resolution tests.

Verifies:
  1. get_project_states(ET_PROJECT_ID)  -> enduro-trails UUIDs (backward compat).
  2. get_project_states(ORCH_PROJECT_ID) -> orchestrator UUIDs.
  3. get_project_states falls back to _DEFAULT_STATES when the Plane API fails.
  4. _STATES_CACHE is populated after a successful call and reload_project_states
     evicts it (per-project and full flush).
  5. stage_to_state() resolves per-project UUIDs for both projects.
  6. Webhook handle_issue_updated recognises In Progress for BOTH projects
     (ORCH-10 critical path: e331bfb3 for ORCH, b873d9eb for ET -> pipeline start).
  7. Webhook handle_issue_updated recognises Approved/Rejected per project.
"""

import os
import sys
import tempfile
from unittest.mock import patch, MagicMock, AsyncMock

import pytest

# ---------------------------------------------------------------------------
# Minimal env so src/config.py can import without a real .env file.
# ---------------------------------------------------------------------------
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")
os.environ.setdefault("ORCH_PLANE_WEBHOOK_SECRET", "")
os.environ.setdefault("ORCH_GITEA_WEBHOOK_SECRET", "")

_test_db = os.path.join(tempfile.gettempdir(), "test_orch10_states.db")
os.environ["ORCH_DB_PATH"] = _test_db

# ---------------------------------------------------------------------------
# Known UUIDs from the ТЗ (source of truth).
# ---------------------------------------------------------------------------
ET_PROJECT_ID   = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
ORCH_PROJECT_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"

ET_STATES = {
    "backlog":      "113b24f6-cce8-4be9-9a22-a359b9cf0122",
    "todo":         "2c7d3df3-9eb9-419b-92b7-d7d560bcdd10",
    "in_progress":  "b873d9eb-993c-48cd-97ac-99a9b1623967",
    "architecture": "3020bbb7-6122-4663-930c-0315ba8dfa3d",
    "development":  "9920609b-f140-4e46-ab95-89acda8412c8",
    "review":       "ba0d802c-5218-41d4-ab43-978b0ea123ed",
    "testing":      "7855d807-b1bf-42ef-8dae-6cde0df92d02",
    "approved":     "a519a341-dada-4a91-8910-7604f82b79c5",
    "rejected":     "ba958f3c-5db5-461d-8f82-89425e413b97",
    "done":         "381a2833-3c4e-4be5-bd0f-be84cb946ad8",
    "cancelled":    "b1cae7f9-961d-4889-a179-f3acea697d17",
    "needs_input":  "babf08a3-ff4d-41f3-a821-5491aa29a8ac",
    "in_review":    "38fb1f64-aa1e-48a3-92e0-0b109679046b",
    "blocked":      "6c4543f9-ac47-4ef7-ae0f-070020dc9920",
}

ORCH_STATES = {
    "backlog":      "2d5d42ff-e94d-4209-a664-8020c28c2a95",
    "todo":         "b5d3f512-4870-460f-bf6b-4ea560f00a6f",
    "in_progress":  "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
    "architecture": "795cc32f-5f5a-4244-be7b-9acffc92c7c0",
    "development":  "f5ed4705-5029-470d-89a9-54c3f0d211ee",
    "review":       "2026f3d9-0f43-4054-ab5f-3f9bae3308b8",
    "testing":      "81c5cd78-2993-4f2c-9e8c-2f52db3e5623",
    "approved":     "63f2c8fe-dcda-4ace-952f-dd88bd0118ff",
    "rejected":     "4c769e90-bf80-4a52-b97a-e1c84904bfc3",
    "done":         "3738cd3c-7610-4907-ba5e-26b9a248d9c0",
    "cancelled":    "59d1d210-8e3a-4a83-930a-cbc5dbf6ad85",
    "needs_input":  "99978b3f-72fe-46e3-8b9b-25ba02899fa0",
    "in_review":    "c52e99b9-31ae-4b31-be3f-9773eea7a747",
    "blocked":      "505f01a6-a12f-4121-aaa7-9c5dd009acc4",
}


def _make_states_response(states_dict: dict) -> dict:
    """Build a fake Plane GET /states/ response."""
    name_map = {v: k for k, v in {
        "backlog":      "Backlog",
        "todo":         "Todo",
        "in_progress":  "In Progress",
        "architecture": "Architecture",
        "development":  "Development",
        "review":       "Review",
        "testing":      "Testing",
        "approved":     "Approved",
        "rejected":     "Rejected",
        "done":         "Done",
        "cancelled":    "Cancelled",
        "needs_input":  "Needs Input",
        "in_review":    "In Review",
        "blocked":      "Blocked",
    }.items()}
    logical_to_plane = {
        "backlog":      "Backlog",
        "todo":         "Todo",
        "in_progress":  "In Progress",
        "architecture": "Architecture",
        "development":  "Development",
        "review":       "Review",
        "testing":      "Testing",
        "approved":     "Approved",
        "rejected":     "Rejected",
        "done":         "Done",
        "cancelled":    "Cancelled",
        "needs_input":  "Needs Input",
        "in_review":    "In Review",
        "blocked":      "Blocked",
    }
    results = [
        {"id": uid, "name": logical_to_plane[key]}
        for key, uid in states_dict.items()
        if key in logical_to_plane
    ]
    return {"results": results}


# ---------------------------------------------------------------------------
# Helpers to build fake httpx responses.
# ---------------------------------------------------------------------------

def _fake_response(data: dict, status: int = 200):
    m = MagicMock()
    m.status_code = status
    m.json.return_value = data
    if status >= 400:
        from httpx import HTTPStatusError, Request, Response
        m.raise_for_status.side_effect = HTTPStatusError(
            "error", request=MagicMock(), response=MagicMock()
        )
    else:
        m.raise_for_status.return_value = None
    return m


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def reset_states_cache():
    """Ensure the states cache is empty before each test."""
    import src.plane_sync as ps
    ps.reload_project_states()
    yield
    ps.reload_project_states()


# ---------------------------------------------------------------------------
# 1 & 2. get_project_states returns correct UUIDs per project
# ---------------------------------------------------------------------------

def test_get_project_states_enduro():
    """ET project -> enduro-trails UUIDs."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
        states = ps.get_project_states(ET_PROJECT_ID)

    for key, expected_uuid in ET_STATES.items():
        assert states[key] == expected_uuid, (
            f"ET state '{key}': expected {expected_uuid}, got {states.get(key)}"
        )


def test_get_project_states_orchestrator():
    """ORCH project -> orchestrator UUIDs."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
        states = ps.get_project_states(ORCH_PROJECT_ID)

    for key, expected_uuid in ORCH_STATES.items():
        assert states[key] == expected_uuid, (
            f"ORCH state '{key}': expected {expected_uuid}, got {states.get(key)}"
        )


def test_get_project_states_et_in_progress_uuid():
    """ET in_progress == b873d9eb (exact UUID from ТЗ)."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
        states = ps.get_project_states(ET_PROJECT_ID)
    assert states["in_progress"] == "b873d9eb-993c-48cd-97ac-99a9b1623967"


def test_get_project_states_orch_in_progress_uuid():
    """ORCH in_progress == e331bfb3 (exact UUID from ТЗ) — the ORCH-10 blocker."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
        states = ps.get_project_states(ORCH_PROJECT_ID)
    assert states["in_progress"] == "e331bfb3-e17e-4699-ba48-4abb89c21b7b"


# ---------------------------------------------------------------------------
# 3. Fallback to _DEFAULT_STATES when API fails
# ---------------------------------------------------------------------------

def test_get_project_states_api_error_fallback():
    """Network failure -> returns _DEFAULT_STATES (ET values)."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get", side_effect=Exception("network error")):
        states = ps.get_project_states(ORCH_PROJECT_ID)
    # Should return _DEFAULT_STATES (ET values) as fallback.
    assert states is ps._DEFAULT_STATES


def test_get_project_states_non_200_fallback():
    """Non-2xx response -> returns _DEFAULT_STATES."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response({}, status=500)
        states = ps.get_project_states(ORCH_PROJECT_ID)
    assert states is ps._DEFAULT_STATES


def test_get_project_states_empty_response_fallback():
    """Empty results list -> returns _DEFAULT_STATES."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response({"results": []})
        states = ps.get_project_states(ORCH_PROJECT_ID)
    assert states is ps._DEFAULT_STATES


def test_get_project_states_none_project_id_fallback():
    """None project_id -> _DEFAULT_STATES immediately (no API call)."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        states = ps.get_project_states(None)
    mock_get.assert_not_called()
    assert states is ps._DEFAULT_STATES


# ---------------------------------------------------------------------------
# 4. Caching & reload_project_states
# ---------------------------------------------------------------------------

def test_get_project_states_caches_result():
    """Second call returns cached result without hitting the API again."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
        _ = ps.get_project_states(ET_PROJECT_ID)
        _ = ps.get_project_states(ET_PROJECT_ID)
    assert mock_get.call_count == 1


def test_reload_project_states_per_project():
    """reload_project_states(project_id) evicts only that project."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
        ps.get_project_states(ET_PROJECT_ID)
        assert ET_PROJECT_ID in ps._STATES_CACHE

    ps.reload_project_states(ET_PROJECT_ID)
    assert ET_PROJECT_ID not in ps._STATES_CACHE


def test_reload_project_states_full_flush():
    """reload_project_states() with no args clears entire cache."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
        ps.get_project_states(ET_PROJECT_ID)
    ps.reload_project_states()
    assert ps._STATES_CACHE == {}


# ---------------------------------------------------------------------------
# 5. stage_to_state() resolves per-project
# ---------------------------------------------------------------------------

def test_stage_to_state_et_analysis():
    """ET analysis -> in_progress UUID b873d9eb."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
        uid = ps.stage_to_state("analysis", ET_PROJECT_ID)
    assert uid == "b873d9eb-993c-48cd-97ac-99a9b1623967"


def test_stage_to_state_orch_analysis():
    """ORCH analysis -> in_progress UUID e331bfb3."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
        uid = ps.stage_to_state("analysis", ORCH_PROJECT_ID)
    assert uid == "e331bfb3-e17e-4699-ba48-4abb89c21b7b"


def test_stage_to_state_unknown_stage():
    """Unknown stage -> None."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
        uid = ps.stage_to_state("nonexistent_stage", ET_PROJECT_ID)
    assert uid is None


def test_stage_to_state_orch_done():
    """ORCH done -> 3738cd3c."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
        uid = ps.stage_to_state("done", ORCH_PROJECT_ID)
    assert uid == "3738cd3c-7610-4907-ba5e-26b9a248d9c0"


# ---------------------------------------------------------------------------
# 6 & 7. Webhook handle_issue_updated — ORCH-10 critical path
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_webhook_in_progress_et_starts_pipeline():
    """ET In Progress (b873d9eb) -> handle_status_start called."""
    from src.webhooks.plane import handle_issue_updated
    import src.plane_sync as ps

    et_states_resp = _make_states_response(ET_STATES)
    with patch("src.plane_sync.httpx.get") as mock_httpx, \
         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_httpx.return_value = _fake_response(et_states_resp)
        data = {
            "id": "et-issue-uuid",
            "state": {"id": "b873d9eb-993c-48cd-97ac-99a9b1623967", "name": "In Progress"},
        }
        await handle_issue_updated(data, ET_PROJECT_ID)

    mock_start.assert_called_once()
    mock_verdict.assert_not_called()


@pytest.mark.asyncio
async def test_webhook_in_progress_orch_starts_pipeline():
    """ORCH In Progress (e331bfb3) -> handle_status_start called.

    This is the ORCH-10 blocker: previously the webhook compared against the
    hardcoded ET UUID (b873d9eb) and the ORCH UUID (e331bfb3) was silently
    ignored — the pipeline never started for ORCH tasks.
    """
    from src.webhooks.plane import handle_issue_updated
    import src.plane_sync as ps

    orch_states_resp = _make_states_response(ORCH_STATES)
    with patch("src.plane_sync.httpx.get") as mock_httpx, \
         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_httpx.return_value = _fake_response(orch_states_resp)
        data = {
            "id": "orch-issue-uuid",
            "state": {"id": "e331bfb3-e17e-4699-ba48-4abb89c21b7b", "name": "In Progress"},
        }
        await handle_issue_updated(data, ORCH_PROJECT_ID)

    mock_start.assert_called_once()
    mock_verdict.assert_not_called()


@pytest.mark.asyncio
async def test_webhook_approved_orch():
    """ORCH Approved (63f2c8fe) -> handle_verdict(approved=True)."""
    from src.webhooks.plane import handle_issue_updated
    orch_states_resp = _make_states_response(ORCH_STATES)
    with patch("src.plane_sync.httpx.get") as mock_httpx, \
         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_httpx.return_value = _fake_response(orch_states_resp)
        data = {
            "id": "orch-issue-uuid",
            "state": {"id": "63f2c8fe-dcda-4ace-952f-dd88bd0118ff", "name": "Approved"},
        }
        await handle_issue_updated(data, ORCH_PROJECT_ID)

    mock_verdict.assert_called_once_with(data, ORCH_PROJECT_ID, approved=True)
    mock_start.assert_not_called()


@pytest.mark.asyncio
async def test_webhook_rejected_orch():
    """ORCH Rejected (4c769e90) -> handle_verdict(approved=False)."""
    from src.webhooks.plane import handle_issue_updated
    orch_states_resp = _make_states_response(ORCH_STATES)
    with patch("src.plane_sync.httpx.get") as mock_httpx, \
         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_httpx.return_value = _fake_response(orch_states_resp)
        data = {
            "id": "orch-issue-uuid",
            "state": {"id": "4c769e90-bf80-4a52-b97a-e1c84904bfc3", "name": "Rejected"},
        }
        await handle_issue_updated(data, ORCH_PROJECT_ID)

    mock_verdict.assert_called_once_with(data, ORCH_PROJECT_ID, approved=False)
    mock_start.assert_not_called()


@pytest.mark.asyncio
async def test_webhook_other_state_no_action():
    """A non-trigger state (e.g. 'Needs Input') -> no pipeline action."""
    from src.webhooks.plane import handle_issue_updated
    orch_states_resp = _make_states_response(ORCH_STATES)
    with patch("src.plane_sync.httpx.get") as mock_httpx, \
         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_httpx.return_value = _fake_response(orch_states_resp)
        data = {
            "id": "orch-issue-uuid",
            "state": {"id": "99978b3f-72fe-46e3-8b9b-25ba02899fa0", "name": "Needs Input"},
        }
        await handle_issue_updated(data, ORCH_PROJECT_ID)

    mock_start.assert_not_called()
    mock_verdict.assert_not_called()


@pytest.mark.asyncio
async def test_webhook_et_in_progress_not_confused_with_orch():
    """ET In Progress UUID does NOT trigger pipeline for ORCH project.

    This guards against the reverse confusion: if somehow an ET UUID was sent
    for an ORCH project event, it should NOT start the pipeline (wrong UUID).
    """
    from src.webhooks.plane import handle_issue_updated
    orch_states_resp = _make_states_response(ORCH_STATES)
    with patch("src.plane_sync.httpx.get") as mock_httpx, \
         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_httpx.return_value = _fake_response(orch_states_resp)
        # Send ET's in_progress UUID for an ORCH project event.
        data = {
            "id": "orch-issue-uuid",
            "state": {"id": "b873d9eb-993c-48cd-97ac-99a9b1623967", "name": "In Progress"},
        }
        await handle_issue_updated(data, ORCH_PROJECT_ID)

    # Since ORCH in_progress is e331bfb3, ET's b873d9eb should NOT trigger start.
    mock_start.assert_not_called()
    mock_verdict.assert_not_called()


# ---------------------------------------------------------------------------
# 8. _DEFAULT_STATES / PLANE_STATES alias preserved
# ---------------------------------------------------------------------------

def test_plane_states_alias_is_default_states():
    """PLANE_STATES is still exported and equals _DEFAULT_STATES (backward compat)."""
    import src.plane_sync as ps
    assert ps.PLANE_STATES is ps._DEFAULT_STATES


def test_default_states_et_values():
    """_DEFAULT_STATES contains the original enduro-trails UUIDs."""
    import src.plane_sync as ps
    for key, expected in ET_STATES.items():
        assert ps._DEFAULT_STATES[key] == expected, (
            f"_DEFAULT_STATES['{key}']: expected {expected}, got {ps._DEFAULT_STATES.get(key)}"
        )


# ---------------------------------------------------------------------------
# ORCH-066 TC-19 (AC-18): resolve-by-name — when a project DEFINES one of the
# new statuses, get_project_states must use its OWN UUID, not the default alias.
# ---------------------------------------------------------------------------
def test_orch066_tc19_name_resolution_beats_alias():
    """A project that created 'Analysis' / 'Code-Review' / 'Awaiting Deploy' /
    'Deploying' / 'Monitoring after Deploy' resolves each to its own project
    UUID (via _PLANE_NAME_TO_KEY), NOT the aliased base-key UUID."""
    import src.plane_sync as ps

    new_uuids = {
        "Analysis":                "11111111-0000-0000-0000-000000000001",
        "Code-Review":             "11111111-0000-0000-0000-000000000002",
        "Awaiting Deploy":         "11111111-0000-0000-0000-000000000003",
        "Deploying":               "11111111-0000-0000-0000-000000000004",
        "Monitoring after Deploy": "11111111-0000-0000-0000-000000000005",
        "To Analyse":              "11111111-0000-0000-0000-000000000006",
    }
    # Start from the full ORCH base set, then add the dedicated new statuses.
    results = _make_states_response(ORCH_STATES)["results"]
    results += [{"id": uid, "name": name} for name, uid in new_uuids.items()]

    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response({"results": results})
        states = ps.get_project_states(ORCH_PROJECT_ID)

    # Each new key resolved to the project's OWN UUID, not the base-key alias.
    assert states["analysis"] == new_uuids["Analysis"]
    assert states["code_review"] == new_uuids["Code-Review"]
    assert states["awaiting_deploy"] == new_uuids["Awaiting Deploy"]
    assert states["deploying"] == new_uuids["Deploying"]
    assert states["monitoring"] == new_uuids["Monitoring after Deploy"]
    assert states["to_analyse"] == new_uuids["To Analyse"]
    # Sanity: they are NOT the aliased base UUIDs.
    assert states["analysis"] != states["in_progress"]
    assert states["code_review"] != states["review"]
    assert states["awaiting_deploy"] != states["in_review"]


def test_orch066_tc19_missing_new_status_aliases_to_project_base():
    """BR-12: a project WITHOUT the new statuses degrades each new key to its OWN
    base UUID (not a foreign enduro UUID) — keeping the PATCH state valid."""
    import src.plane_sync as ps
    with patch("src.plane_sync.httpx.get") as mock_get:
        mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
        states = ps.get_project_states(ORCH_PROJECT_ID)

    # No dedicated new statuses -> alias to THIS project's base UUIDs.
    assert states["analysis"] == ORCH_STATES["in_progress"]
    assert states["to_analyse"] == ORCH_STATES["in_progress"]
    assert states["code_review"] == ORCH_STATES["review"]
    assert states["awaiting_deploy"] == ORCH_STATES["in_review"]
    assert states["deploying"] == ORCH_STATES["in_progress"]
    assert states["monitoring"] == ORCH_STATES["done"]
