"""Integration tests for POST /openapi/v1/apps/<id>/run."""

from __future__ import annotations

import uuid
from collections.abc import Generator

import pytest
from flask import Flask

from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_database import db
from models import App


def test_run_chat_dispatches_to_chat_handler(flask_app, account_token, app_in_workspace, monkeypatch):
    captured = {}

    def _fake_generate(*, app_model, user, args, invoke_from, streaming):
        captured["mode"] = app_model.mode
        captured["args"] = args
        captured["invoke_from"] = invoke_from
        return {
            "event": "message",
            "task_id": "t",
            "id": "m",
            "message_id": "m",
            "conversation_id": "c",
            "mode": "chat",
            "answer": "ok",
            "created_at": 0,
        }

    monkeypatch.setattr("controllers.openapi.app_run.AppGenerateService.generate", staticmethod(_fake_generate))
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app_in_workspace.id}/run",
        json={"inputs": {}, "query": "hi", "response_mode": "blocking", "user": "spoof@x.com"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 200
    assert res.get_json()["mode"] == "chat"
    assert captured["mode"] == "chat"
    assert captured["invoke_from"] == InvokeFrom.OPENAPI
    assert "user" not in captured["args"], "server must strip body.user; identity comes from bearer"


@pytest.fixture
def app_with_mode(flask_app: Flask, workspace_account):
    """Factory that creates an App row in the workspace_account tenant with
    a specified mode. Tracks rows for teardown.
    """
    _, tenant, _ = workspace_account
    created: list[App] = []

    def _make(mode: str) -> App:
        with flask_app.app_context():
            app = App(
                tenant_id=tenant.id,
                name=f"a-{mode}",
                mode=mode,
                status="normal",
                enable_site=True,
                enable_api=True,
            )
            db.session.add(app)
            db.session.commit()
            db.session.refresh(app)
            db.session.expunge(app)
            created.append(app)
            return app

    yield _make

    with flask_app.app_context():
        for app in created:
            db.session.delete(db.session.merge(app))
        db.session.commit()


def test_run_chat_without_query_returns_422(flask_app, account_token, app_in_workspace, monkeypatch):
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app_in_workspace.id}/run",
        json={"inputs": {}, "response_mode": "blocking"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 422
    assert b"query_required_for_chat" in res.data


def test_run_completion_dispatches_to_completion_handler(flask_app, account_token, app_with_mode, monkeypatch):
    app = app_with_mode("completion")

    captured: dict = {}

    def _fake_generate(*, app_model, user, args, invoke_from, streaming):
        captured["mode"] = app_model.mode
        captured["args"] = args
        return {
            "event": "message",
            "task_id": "t",
            "id": "m",
            "message_id": "m",
            "mode": "completion",
            "answer": "ok",
            "created_at": 0,
        }

    monkeypatch.setattr("controllers.openapi.app_run.AppGenerateService.generate", staticmethod(_fake_generate))
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app.id}/run",
        json={"inputs": {}, "response_mode": "blocking"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 200
    assert res.get_json()["mode"] == "completion"
    assert captured["mode"] == "completion"


def test_run_workflow_with_query_returns_422(flask_app, account_token, app_with_mode, monkeypatch):
    app = app_with_mode("workflow")
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app.id}/run",
        json={"inputs": {}, "query": "hi", "response_mode": "blocking"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 422
    assert b"query_not_supported_for_workflow" in res.data


def test_run_workflow_no_query_dispatches_to_workflow_handler(flask_app, account_token, app_with_mode, monkeypatch):
    app = app_with_mode("workflow")

    def _fake_generate(*, app_model, user, args, invoke_from, streaming):
        return {
            "workflow_run_id": "wfr",
            "task_id": "t",
            "data": {"id": "wf-d", "workflow_id": "wf", "status": "succeeded"},
        }

    monkeypatch.setattr("controllers.openapi.app_run.AppGenerateService.generate", staticmethod(_fake_generate))
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app.id}/run",
        json={"inputs": {}, "response_mode": "blocking"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 200
    body = res.get_json()
    assert body["mode"] == "workflow"
    assert body["workflow_run_id"] == "wfr"


def test_run_unsupported_mode_returns_422(flask_app, account_token, app_with_mode, monkeypatch):
    app = app_with_mode("channel")
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app.id}/run",
        json={"inputs": {}, "response_mode": "blocking"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 422
    assert b"mode_not_runnable" in res.data


def test_run_without_bearer_returns_401(flask_app, app_in_workspace):
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app_in_workspace.id}/run",
        json={"inputs": {}, "query": "hi"},
    )
    assert res.status_code == 401


def test_run_with_insufficient_scope_returns_403(flask_app, account_token, app_in_workspace, monkeypatch):
    """Stub the authenticator to return an AuthContext with empty scopes."""
    from libs import oauth_bearer

    real_authenticate = oauth_bearer.BearerAuthenticator.authenticate

    def _stub_authenticate(self, token: str):
        ctx = real_authenticate(self, token)
        from dataclasses import replace

        return replace(ctx, scopes=frozenset())

    monkeypatch.setattr(oauth_bearer.BearerAuthenticator, "authenticate", _stub_authenticate)

    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app_in_workspace.id}/run",
        json={"inputs": {}, "query": "hi"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 403


def test_run_with_unknown_app_returns_404(flask_app, account_token):
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{uuid.uuid4()}/run",
        json={"inputs": {}, "query": "hi"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 404


def test_run_streaming_returns_event_stream(flask_app, account_token, app_in_workspace, monkeypatch):
    def _stream() -> Generator[str, None, None]:
        yield 'event: message\ndata: {"x": 1}\n\n'

    monkeypatch.setattr(
        "controllers.openapi.app_run.AppGenerateService.generate",
        staticmethod(lambda **kw: _stream()),
    )

    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app_in_workspace.id}/run",
        json={"inputs": {}, "query": "hi", "response_mode": "streaming"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 200
    assert res.headers["Content-Type"].startswith("text/event-stream")
    assert b"event: message" in res.data


def test_run_without_inputs_returns_422(flask_app, account_token, app_in_workspace):
    client = flask_app.test_client()
    res = client.post(
        f"/openapi/v1/apps/{app_in_workspace.id}/run",
        json={"query": "hi"},
        headers={"Authorization": f"Bearer {account_token}"},
    )
    assert res.status_code == 422
