from __future__ import annotations

from contextlib import nullcontext
from inspect import getsource
from types import SimpleNamespace
from unittest.mock import MagicMock
from uuid import uuid4

import pytest
from sqlalchemy import Select
from sqlalchemy.orm import Session

from controllers.common import session as session_module
from controllers.common.session import with_session
from controllers.console.app import completion as completion_module
from controllers.console.app import workflow as workflow_module
from controllers.console.app import wraps as wraps_module
from controllers.console.app.error import AppNotFoundError
from models.model import App, AppMode, TrialApp


def _persist_app(sqlite_session: Session, *, mode: AppMode = AppMode.CHAT) -> App:
    app_model = App(
        tenant_id=str(uuid4()),
        name="Test App",
        mode=mode,
        enable_site=True,
        enable_api=True,
    )
    app_model.id = str(uuid4())
    sqlite_session.add(app_model)
    sqlite_session.commit()
    return app_model


@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True)
def test_get_app_model_injects_model(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None:
    app_model = _persist_app(sqlite_session)
    monkeypatch.setattr(wraps_module, "current_account_with_tenant", lambda: (None, app_model.tenant_id))
    monkeypatch.setattr(wraps_module.db, "session", sqlite_session)

    @wraps_module.get_app_model
    def handler(app_model):
        return app_model.id

    assert handler(app_id=app_model.id) == app_model.id


@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True)
def test_get_app_model_rejects_wrong_mode(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None:
    app_model = _persist_app(sqlite_session)
    monkeypatch.setattr(wraps_module, "current_account_with_tenant", lambda: (None, app_model.tenant_id))
    monkeypatch.setattr(wraps_module.db, "session", sqlite_session)

    @wraps_module.get_app_model(mode=[AppMode.COMPLETION])
    def handler(app_model):
        return app_model.id

    with pytest.raises(AppNotFoundError):
        handler(app_id=app_model.id)


def test_get_app_model_with_trial_requires_trial_app_registration(monkeypatch: pytest.MonkeyPatch) -> None:
    app_model = SimpleNamespace(id="app-1", mode=AppMode.CHAT.value, status="normal", tenant_id="t1")
    session = MagicMock(spec=Session)

    def scalar(statement: Select[tuple[App]]) -> object | None:
        has_trial_app_join = any(
            from_clause.is_derived_from(TrialApp.__table__) for from_clause in statement.get_final_froms()
        )
        return None if has_trial_app_join else app_model

    monkeypatch.setattr(session, "scalar", scalar)
    recommended_get_app = MagicMock(return_value=None)
    monkeypatch.setattr(wraps_module.RecommendedAppService, "get_app", recommended_get_app)

    class Handler:
        @wraps_module.get_app_model_with_trial
        def get(self, _injected_session, app_model):
            return app_model.id

    with pytest.raises(AppNotFoundError):
        Handler().get(session, app_id="app-1")

    recommended_get_app.assert_called_once_with("app-1", session=session)


def test_get_app_model_with_trial_falls_back_to_recommended_app(monkeypatch: pytest.MonkeyPatch) -> None:
    app_model = SimpleNamespace(id="app-1", mode=AppMode.CHAT.value, status="normal", tenant_id="t1")
    session = MagicMock(spec=Session)
    trial_app_loader = MagicMock(return_value=None)
    recommended_get_app = MagicMock(return_value=app_model)
    monkeypatch.setattr(wraps_module, "_load_app_model_with_trial", trial_app_loader)
    monkeypatch.setattr(wraps_module.RecommendedAppService, "get_app", recommended_get_app)

    class Handler:
        @wraps_module.get_app_model_with_trial
        def get(self, _injected_session, app_model):
            return app_model.id

    assert Handler().get(session, app_id="app-1") == "app-1"
    trial_app_loader.assert_called_once_with(session, "app-1")
    recommended_get_app.assert_called_once_with("app-1", session=session)


def test_get_app_model_with_trial_prefers_trial_registration(monkeypatch: pytest.MonkeyPatch) -> None:
    app_model = SimpleNamespace(id="app-1", mode=AppMode.CHAT.value, status="normal", tenant_id="t1")
    session = MagicMock(spec=Session)
    trial_app_loader = MagicMock(return_value=app_model)
    recommended_get_app = MagicMock()
    monkeypatch.setattr(wraps_module, "_load_app_model_with_trial", trial_app_loader)
    monkeypatch.setattr(wraps_module.RecommendedAppService, "get_app", recommended_get_app)

    class Handler:
        @wraps_module.get_app_model_with_trial
        def get(self, _injected_session, app_model):
            return app_model.id

    assert Handler().get(session, app_id="app-1") == "app-1"
    trial_app_loader.assert_called_once_with(session, "app-1")
    recommended_get_app.assert_not_called()


def test_get_app_model_requires_app_id() -> None:
    @wraps_module.get_app_model
    def handler(app_model):
        return app_model.id

    with pytest.raises(ValueError):
        handler()


def test_wraps_with_session_reexports_common_session_decorator() -> None:
    assert wraps_module.with_session is with_session


@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True)
def test_get_app_model_prefers_injected_session(
    monkeypatch: pytest.MonkeyPatch,
    sqlite_session: Session,
) -> None:
    app_model = _persist_app(sqlite_session)
    monkeypatch.setattr(wraps_module, "current_account_with_tenant", lambda: (None, app_model.tenant_id))

    class Handler:
        @wraps_module.get_app_model
        def get(self, _injected_session, app_model):
            return app_model.id

    # An unbound real Session fails on query, so success proves the injected
    # request Session was preferred over the legacy scoped-session fallback.
    with Session() as scoped_session:
        monkeypatch.setattr(wraps_module.db, "session", scoped_session)
        assert Handler().get(sqlite_session, app_id=app_model.id) == app_model.id


def test_get_app_model_with_trial_prefers_injected_session(monkeypatch: pytest.MonkeyPatch) -> None:
    app_model = SimpleNamespace(id="app-1", mode=AppMode.CHAT.value, status="normal")
    session = MagicMock(spec=Session)
    session.scalar.return_value = app_model
    monkeypatch.setattr(
        wraps_module.db,
        "session",
        SimpleNamespace(scalar=lambda *_args, **_kwargs: pytest.fail("db.session should not be used")),
    )
    monkeypatch.setattr(session_module.session_factory, "create_session", lambda: nullcontext(session))

    class Handler:
        @with_session(write=False)
        @wraps_module.get_app_model_with_trial(None)
        def get(self, injected_session, app_model):
            assert injected_session is session
            return app_model.id

    assert Handler().get(app_id="app-1") == "app-1"
    session.scalar.assert_called_once()


def test_get_app_model_with_trial_requires_injected_session() -> None:
    @wraps_module.get_app_model_with_trial(None)
    def handler(app_model):
        return app_model.id

    with pytest.raises(RuntimeError, match="requires @with_session"):
        handler(app_id="app-1")


@pytest.mark.parametrize(
    "resource",
    [
        completion_module.CompletionMessageApi,
        completion_module.ChatMessageApi,
        workflow_module.AdvancedChatDraftWorkflowRunApi,
        workflow_module.DraftWorkflowRunApi,
        workflow_module.DraftWorkflowTriggerRunApi,
        workflow_module.DraftWorkflowTriggerRunAllApi,
    ],
)
def test_migrated_handlers_open_session_before_app_lookup(resource: type) -> None:
    assert "@with_session\n    @get_app_model" in getsource(resource)
