# Copyright (c) 2023-2026, HaiyangLi <quantocean.li at gmail dot com>
# SPDX-License-Identifier: Apache-2.0

"""Tests for lionagi.studio.registry — decorator, registry mechanics, and live app integration."""

from __future__ import annotations

import pytest

fastapi = pytest.importorskip("fastapi", reason="studio extra not installed")

# Import app at module level so the lifespan routes (added by app.py at import
# time) are registered before the _isolated_registry fixture runs. Without this,
# a worker that first encounters a live-app test would import app.py AFTER the
# fixture cleared _ROUTES, resulting in an empty registry and missing routes.
from lionagi.studio.app import app as _live_app  # noqa: E402
from lionagi.studio.registry import iter_studio_routes as _iter_at_import  # noqa: E402

# Snapshot the populated registry at import time. app.py calls
# load_studio_route_modules() during the _live_app import above, so every
# migrated area is registered now — before the autouse fixture clears _ROUTES.
_AREAS_AT_IMPORT = {r.area for r in _iter_at_import()}
_KEYS_AT_IMPORT = [(r.path, r.method) for r in _iter_at_import()]

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


@pytest.fixture(autouse=True)
def _isolated_registry():
    """Snapshot and restore the global registry around each test."""
    from lionagi.studio.registry import _DEDUP_KEYS, _ROUTES, _reset_registry

    saved_routes = list(_ROUTES)
    saved_keys = set(_DEDUP_KEYS)
    _reset_registry()
    yield
    _reset_registry()
    _ROUTES.extend(saved_routes)
    _DEDUP_KEYS.update(saved_keys)


# ---------------------------------------------------------------------------
# 1. studio_route registers a StudioRoute; handler is returned unchanged
# ---------------------------------------------------------------------------


def test_registration_stores_route_and_returns_original():
    from lionagi.studio.registry import _ROUTES, StudioRoute, studio_route

    async def my_handler() -> dict:
        return {}

    original_id = id(my_handler)
    result = studio_route("/test", method="GET", area="test_area")(my_handler)

    assert result is my_handler, "decorator must return the original handler unchanged"
    assert id(result) == original_id
    assert len(_ROUTES) == 1

    route = _ROUTES[0]
    assert isinstance(route, StudioRoute)
    assert route.path == "/test"
    assert route.method == "GET"
    assert route.area == "test_area"
    assert route.handler is my_handler
    assert route.order == 0


def test_registration_stores_all_passed_fields():
    from fastapi import Depends

    from lionagi.studio.registry import _ROUTES, studio_route

    def dep():
        pass

    async def handler() -> dict:
        return {}

    studio_route(
        "/full",
        method="POST",
        area="full_area",
        response_model=None,
        dependencies=[Depends(dep)],
        status_code=201,
        tags=["a", "b"],
        name="full_name",
        summary="A summary",
        description="A description",
        responses={201: {"description": "Created"}},
        include_in_schema=False,
    )(handler)

    route = _ROUTES[0]
    assert route.status_code == 201
    assert route.name == "full_name"
    assert route.summary == "A summary"
    assert route.description == "A description"
    assert route.include_in_schema is False
    assert route.tags == ("a", "b")
    assert isinstance(route.dependencies, tuple)
    assert len(route.dependencies) == 1


# ---------------------------------------------------------------------------
# 2. tags=None defaults to (area,); explicit tags preserved verbatim
# ---------------------------------------------------------------------------


def test_tags_none_defaults_to_area():
    from lionagi.studio.registry import _ROUTES, studio_route

    async def h() -> dict:
        return {}

    studio_route("/t1", method="GET", area="myarea")(h)
    assert _ROUTES[0].tags == ("myarea",)


def test_explicit_tags_preserved_verbatim_no_dedup():
    from lionagi.studio.registry import _ROUTES, studio_route

    async def h() -> dict:
        return {}

    studio_route("/t2", method="GET", area="myarea", tags=["shows", "shows"])(h)
    assert _ROUTES[0].tags == ("shows", "shows")


def test_explicit_empty_tags_preserved():
    from lionagi.studio.registry import _ROUTES, studio_route

    async def h() -> dict:
        return {}

    studio_route("/t3", method="GET", area="myarea", tags=[])(h)
    assert _ROUTES[0].tags == ()


# ---------------------------------------------------------------------------
# 3. iter_studio_routes returns immutable tuple sorted by order; area filter works
# ---------------------------------------------------------------------------


def test_iter_returns_tuple_sorted_by_order():
    from lionagi.studio.registry import iter_studio_routes, studio_route

    async def h1() -> dict:
        return {}

    async def h2() -> dict:
        return {}

    studio_route("/b", method="GET", area="x")(h2)
    studio_route("/a", method="GET", area="x")(h1)

    result = iter_studio_routes()
    assert isinstance(result, tuple)
    assert result[0].path == "/b"
    assert result[1].path == "/a"
    assert result[0].order < result[1].order


def test_iter_area_filter():
    from lionagi.studio.registry import iter_studio_routes, studio_route

    async def h1() -> dict:
        return {}

    async def h2() -> dict:
        return {}

    studio_route("/x1", method="GET", area="alpha")(h1)
    studio_route("/x2", method="GET", area="beta")(h2)

    alpha = iter_studio_routes(area="alpha")
    assert len(alpha) == 1
    assert alpha[0].area == "alpha"

    beta = iter_studio_routes(area="beta")
    assert len(beta) == 1
    assert beta[0].area == "beta"

    all_routes = iter_studio_routes()
    assert len(all_routes) == 2


def test_iter_returns_immutable_tuple():
    from lionagi.studio.registry import iter_studio_routes

    result = iter_studio_routes()
    assert isinstance(result, tuple)


# ---------------------------------------------------------------------------
# 4. Dedup guard raises on same (path, method, module, qualname)
# ---------------------------------------------------------------------------


def test_dedup_guard_raises_on_duplicate():
    from lionagi.studio.registry import studio_route

    async def handler() -> dict:
        return {}

    studio_route("/dup", method="GET", area="a")(handler)

    with pytest.raises(ValueError, match="Duplicate studio_route registration"):
        studio_route("/dup", method="GET", area="b")(handler)


def test_dedup_same_path_different_method_allowed():
    from lionagi.studio.registry import _ROUTES, studio_route

    async def get_h() -> dict:
        return {}

    async def post_h() -> dict:
        return {}

    studio_route("/dup2", method="GET", area="a")(get_h)
    studio_route("/dup2", method="POST", area="a")(post_h)
    assert len(_ROUTES) == 2


# ---------------------------------------------------------------------------
# 5. load_studio_route_modules registers every migrated area (phase 1+)
# ---------------------------------------------------------------------------

_MIGRATED_AREAS = {
    "casts",
    "runs",
    "engine-runs",
    "definitions",
    "agents",
    "playbooks",
    "shows",
    "skills",
    "plugins",
    "teams",
    "invocations",
    "launches",
    "projects",
    "engine-defs",
    "workflow-defs",
    "sessions",
    "leo",
    "admin",
    "schedules",
    "stats",
}


def test_load_studio_route_modules_registers_all_areas():
    # Asserted against the import-time snapshot: the autouse fixture clears
    # _ROUTES in the test body and the area modules are already cached, so a
    # re-import here is a no-op. The contract that matters is that importing
    # the app wired every migrated area onto the registry.
    assert _AREAS_AT_IMPORT == _MIGRATED_AREAS, (
        f"registered areas drifted: missing={_MIGRATED_AREAS - _AREAS_AT_IMPORT}, "
        f"unexpected={_AREAS_AT_IMPORT - _MIGRATED_AREAS}"
    )


def test_loaded_registry_has_no_duplicate_routes():
    # Re-invoking load is idempotent (module caching + _DEDUP_KEYS); the
    # observable invariant is that no (path, method) pair is registered twice.
    from lionagi.studio.registry import load_studio_route_modules

    load_studio_route_modules()  # must not raise on a second call
    assert len(_KEYS_AT_IMPORT) == len(set(_KEYS_AT_IMPORT)), "duplicate (path, method) in registry"


# ---------------------------------------------------------------------------
# 6. Live app still exposes its full route table (behavior-preservation gate)
# ---------------------------------------------------------------------------


def test_live_app_health_route_present():
    paths = {getattr(r, "path", None) for r in _live_app.routes}
    assert "/health" in paths


def test_live_app_stats_route_present():
    paths = {getattr(r, "path", None) for r in _live_app.routes}
    assert "/api/stats" in paths


def test_live_app_projects_route_present():
    paths = {getattr(r, "path", None) for r in _live_app.routes}
    assert "/api/projects/" in paths


def test_live_app_sessions_signals_route_present():
    paths = {getattr(r, "path", None) for r in _live_app.routes}
    assert "/api/sessions/{session_id}/signals" in paths
