"""Integration tests for ``api.services.pipecat.run_pipeline._run_pipeline``.

Drives the actual ``_run_pipeline`` against the test database with real
DB rows (organization, user, user configuration, workflow, workflow run)
and pipecat's real ``MockTransport`` / ``Pipeline`` / ``PipelineWorker``.
The only patches are for things that talk to genuinely external systems;
those are applied via ``patch_run_pipeline_externals`` from the shared
helpers module.

Verifies that the wiring done by ``_run_pipeline`` (in particular
``register_event_handlers``) produces the right behaviour end-to-end:
``maybe_trigger_initial_response`` fires (``engine.set_node`` runs), and
on shutdown the workflow run is persisted with the expected state,
completion flag, and ``gathered_context`` entries.
"""

import asyncio

import pytest
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams

from api.enums import WorkflowRunMode, WorkflowRunState
from api.services.observability import active_calls
from api.services.pipecat.audio_config import create_audio_config
from api.services.pipecat.run_pipeline import _run_pipeline
from api.services.pipecat.worker_runner import wait_for_pipeline_worker_started
from api.tests.integrations._run_pipeline_helpers import (
    create_workflow_run_rows,
    patch_run_pipeline_externals,
)

WORKFLOW_DEFINITION = {
    "nodes": [
        {
            "id": "start",
            "type": "startCall",
            "position": {"x": 0, "y": 0},
            "data": {
                "name": "Start",
                "prompt": "You are a helpful assistant. Greet the user briefly.",
                "is_start": True,
                "allow_interrupt": False,
                "add_global_prompt": False,
            },
        },
        {
            "id": "end",
            "type": "endCall",
            "position": {"x": 0, "y": 200},
            "data": {
                "name": "End",
                "prompt": "End the call politely.",
                "is_end": True,
                "allow_interrupt": False,
                "add_global_prompt": False,
            },
        },
    ],
    "edges": [
        {
            "id": "start-end",
            "source": "start",
            "target": "end",
            "data": {"label": "End", "condition": "When the user wants to end."},
        }
    ],
}


@pytest.fixture
async def workflow_run_setup(db_session, async_session):
    """Create org/user/user_configuration/workflow/workflow_run rows in the
    test database. Returns (workflow_run, user, workflow)."""
    return await create_workflow_run_rows(
        db_session,
        async_session,
        workflow_definition=WORKFLOW_DEFINITION,
        name_prefix="Event Handler Integration",
        provider_id_suffix="event-handlers",
    )


@pytest.mark.asyncio
async def test_run_pipeline_fires_initial_response_and_completes_run(
    workflow_run_setup, db_session
):
    """End-to-end: _run_pipeline boots, register_event_handlers wires up,
    on_pipeline_started + on_client_connected both fire, the initial
    response is triggered (set_node), and on_pipeline_finished updates
    the workflow_run row to COMPLETED."""
    workflow_run, user, workflow = workflow_run_setup
    transport = MockTransport(
        TransportParams(
            audio_in_enabled=True,
            audio_out_enabled=True,
            audio_out_end_silence_secs=0,
        )
    )

    captured_task: list = []
    audio_config = create_audio_config(WorkflowRunMode.SMALLWEBRTC.value)
    with patch_run_pipeline_externals(captured_task):
        run_coro = _run_pipeline(
            transport=transport,
            workflow_id=workflow.id,
            workflow_run_id=workflow_run.id,
            user_id=user.id,
            audio_config=audio_config,
            user_provider_id=user.provider_id,
        )
        run_task = asyncio.create_task(run_coro)

        # Wait until create_pipeline_task is invoked. Surface any
        # exception from _run_pipeline immediately rather than swallowing
        # it during the wait loop.
        for _ in range(60):
            if captured_task or run_task.done():
                break
            await asyncio.sleep(0.05)
        if run_task.done() and not captured_task:
            run_task.result()  # re-raise the failure
        assert captured_task, "create_pipeline_task was never invoked"
        pipeline_task = captured_task[0]
        await wait_for_pipeline_worker_started(
            pipeline_task, timeout=3.0, run_task=run_task
        )
        # Let the initial response handler (set_node, queue LLMContextFrame)
        # complete before tearing things down.
        await asyncio.sleep(0.1)
        await pipeline_task.cancel()
        await asyncio.wait_for(run_task, timeout=5.0)

    # Verify the run was completed end-to-end via the real on_pipeline_finished
    # handler — DB side effects, not mock assertions.
    refreshed = await db_session.get_workflow_run_by_id(workflow_run.id)
    assert refreshed.is_completed is True
    assert refreshed.state == WorkflowRunState.COMPLETED.value
    # set_node("start") populates "nodes_visited" via _gathered_context, and
    # on_pipeline_finished merges call_tags into gathered_context.
    assert "Start" in refreshed.gathered_context.get("nodes_visited", [])
    assert "call_tags" in refreshed.gathered_context


@pytest.mark.asyncio
async def test_call_stays_registered_for_drain_until_artifacts_uploaded(
    workflow_run_setup, monkeypatch
):
    """The active-call registry must not drop a run while its artifacts are
    still uploading: deploy draining polls the registry and SIGTERMs the
    worker at zero, which would kill in-flight uploads.

    The ordering rests on pipecat waiting for spawned ``on_pipeline_finished``
    handler tasks before ``PipelineWorker.run()`` — and therefore
    ``run_pipeline_worker()`` — returns, with ``unregister_active_call`` in
    the ``finally`` after that. This test pins the guarantee: a slow upload
    samples the registry mid-flight and must still see the call registered.
    """
    workflow_run, user, workflow = workflow_run_setup
    active_calls._active_run_ids.clear()

    run_task_ref: list[asyncio.Task] = []
    observed: dict = {}

    async def slow_upload(workflow_run_id, **_kwargs):
        # Give _run_pipeline a chance to (incorrectly) return and unregister
        # while the upload is still in flight, then sample the registry.
        await asyncio.sleep(0.2)
        observed["count_during_upload"] = active_calls.active_call_count()
        observed["run_task_done_during_upload"] = run_task_ref[0].done()

    monkeypatch.setattr(
        "api.services.pipecat.event_handlers.upload_workflow_run_artifacts",
        slow_upload,
    )

    transport = MockTransport(
        TransportParams(
            audio_in_enabled=True,
            audio_out_enabled=True,
            audio_out_end_silence_secs=0,
        )
    )
    captured_task: list = []
    audio_config = create_audio_config(WorkflowRunMode.SMALLWEBRTC.value)
    with patch_run_pipeline_externals(captured_task):
        run_task = asyncio.create_task(
            _run_pipeline(
                transport=transport,
                workflow_id=workflow.id,
                workflow_run_id=workflow_run.id,
                user_id=user.id,
                audio_config=audio_config,
                user_provider_id=user.provider_id,
            )
        )
        run_task_ref.append(run_task)

        for _ in range(60):
            if captured_task or run_task.done():
                break
            await asyncio.sleep(0.05)
        if run_task.done() and not captured_task:
            run_task.result()  # re-raise the failure
        assert captured_task, "create_pipeline_task was never invoked"
        pipeline_task = captured_task[0]
        await wait_for_pipeline_worker_started(
            pipeline_task, timeout=3.0, run_task=run_task
        )
        assert active_calls.active_call_count() == 1
        await pipeline_task.cancel()
        await asyncio.wait_for(run_task, timeout=5.0)

    assert observed, "upload_workflow_run_artifacts was never called"
    assert observed["count_during_upload"] == 1
    assert observed["run_task_done_during_upload"] is False
    assert active_calls.active_call_count() == 0
