"""Direct async endpoint tests and endpoint utilities."""

from __future__ import annotations

import pytest
from pydantic import BaseModel, create_model
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from tests.conftest import Book, SessionFactory
from tests.endpoints.shared import (
    get_route,
)

from auen import (
    CrudRouterBuilder,
    FilterConfig,
    FilterFieldConfig,
    HooksConfig,
    Operation,
    PaginationConfig,
    derive_schemas,
)
from auen._endpoints import _build_filter_model
from auen.exceptions import (
    ForbiddenError,
    NotFoundError,
)
from auen.types import SelectQuery, User


async def test_direct_endpoints_roundtrip(get_session: SessionFactory) -> None:
    router = (
        CrudRouterBuilder()
        .with_pagination(PaginationConfig(include_total=True))
        .with_operations(
            {
                Operation.CREATE,
                Operation.READ,
                Operation.UPDATE,
                Operation.DELETE,
                Operation.LIST,
                Operation.BULK_CREATE,
                Operation.BULK_UPDATE,
                Operation.BULK_DELETE,
            }
        )
        .build_routers(models=[Book], session_dep=get_session)[0]
    )

    schemas = derive_schemas(Book)
    create_ep = get_route(router, "/", "POST").endpoint
    list_ep = get_route(router, "/", "GET").endpoint
    read_ep = get_route(router, "/{id}", "GET").endpoint
    update_ep = get_route(router, "/{id}", "PATCH").endpoint
    delete_ep = get_route(router, "/{id}", "DELETE").endpoint
    bulk_create_ep = get_route(router, "/bulk", "POST").endpoint
    bulk_update_ep = get_route(router, "/bulk", "PATCH").endpoint
    bulk_delete_ep = get_route(router, "/bulk", "DELETE").endpoint

    async for session in get_session():
        created = await create_ep(
            obj_in=schemas.create(title="A", isbn="1"),
            session=session,
            user=None,
        )
        created_id = created.id
        await create_ep(
            obj_in=schemas.create(title="B", isbn="2"),
            session=session,
            user=None,
        )

        payload = await list_ep(
            session=session,
            user=None,
            offset=0,
            limit=10,
            sort=None,
            filter_params=None,
        )
        assert payload["total"] >= 2

        found = await read_ep(
            id=created_id,
            session=session,
            user=None,
        )
        assert found.id == created_id

        updated = await update_ep(
            id=created_id,
            obj_in=schemas.update(pages=123),
            session=session,
            user=None,
        )
        assert updated.pages == 123

        bulk_created = await bulk_create_ep(
            objects_in=[
                schemas.create(title="C", isbn="3"),
                schemas.create(title="D", isbn="4"),
            ],
            session=session,
            user=None,
        )
        bulk_ids = [obj.id for obj in bulk_created]
        # Send back ReadSchema-shaped objects
        bulk_update = await bulk_update_ep(
            items_in=[
                schemas.read(id=bulk_ids[0], title="C2", isbn="3", pages=None),
            ],
            session=session,
            user=None,
        )
        assert bulk_update[0].title == "C2"

        # Bulk delete uses identifier schema
        from pydantic import create_model as _cm

        BookIdentifier = _cm("BookIdentifier", __base__=BaseModel, id=(int, ...))
        await bulk_delete_ep(
            items_in=[BookIdentifier(id=bulk_ids[1])],
            session=session,
            user=None,
        )

        await delete_ep(id=created_id, session=session, user=None)


async def test_create_forbidden(get_session: SessionFactory) -> None:
    class DenyCreate:
        def can_create(self, user: User, obj_in: BaseModel) -> bool:
            return False

        def can_read(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
            return True

        def can_delete(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def filter_list_query(
            self, user: User, query: SelectQuery[SQLModel]
        ) -> SelectQuery[SQLModel]:
            return query

    schemas = derive_schemas(Book)
    router = (
        CrudRouterBuilder()
        .with_policy(DenyCreate())
        .with_operations({Operation.CREATE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint

    async for session in get_session():
        with pytest.raises(ForbiddenError):
            await create_ep(
                obj_in=schemas.create(title="Nope", isbn="X"),
                session=session,
                user=None,
            )


async def test_create_after_hook(get_session: SessionFactory) -> None:
    schemas = derive_schemas(Book)
    called = []

    async def after_create(session: AsyncSession, obj: Book, user: User) -> None:
        called.append(obj.title)

    router = (
        CrudRouterBuilder()
        .with_hooks(HooksConfig(after_create=after_create))
        .with_operations({Operation.CREATE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint

    async for session in get_session():
        await create_ep(
            obj_in=schemas.create(title="Hooked", isbn="H"),
            session=session,
            user=None,
        )

    assert called == ["Hooked"]


def test_build_filter_model_skips_missing_fields() -> None:
    cfg = FilterConfig(fields={"missing": FilterFieldConfig()})
    model = _build_filter_model(Book, cfg)
    assert model.model_fields == {}


async def test_list_without_total_returns_items(get_session: SessionFactory) -> None:
    schemas = derive_schemas(Book)
    router = (
        CrudRouterBuilder()
        .with_pagination(PaginationConfig(include_total=False))
        .with_operations({Operation.CREATE, Operation.LIST})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint
    list_ep = get_route(router, "/", "GET").endpoint
    async for session in get_session():
        await create_ep(
            obj_in=schemas.create(title="NoTotal", isbn="NT"),
            session=session,
            user=None,
        )
        payload = await list_ep(
            session=session,
            user=None,
            offset=0,
            limit=10,
            sort=None,
            filter_params=None,
        )
        assert isinstance(payload, list)


async def test_read_not_found_and_forbidden(get_session: SessionFactory) -> None:
    class DenyRead:
        def can_create(self, user: User, obj_in: BaseModel) -> bool:
            return True

        def can_read(self, user: User, db_obj: SQLModel) -> bool:
            return False

        def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
            return True

        def can_delete(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def filter_list_query(
            self, user: User, query: SelectQuery[SQLModel]
        ) -> SelectQuery[SQLModel]:
            return query

    schemas = derive_schemas(Book)
    router = (
        CrudRouterBuilder()
        .with_policy(DenyRead())
        .with_operations({Operation.CREATE, Operation.READ})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint
    read_ep = get_route(router, "/{id}", "GET").endpoint
    async for session in get_session():
        created = await create_ep(
            obj_in=schemas.create(title="Denied", isbn="DR"),
            session=session,
            user=None,
        )
        with pytest.raises(ForbiddenError):
            await read_ep(id=created.id, session=session, user=None)
        with pytest.raises(NotFoundError):
            await read_ep(id=9999, session=session, user=None)


async def test_update_branches(get_session: SessionFactory) -> None:
    class DenyUpdate:
        def can_create(self, user: User, obj_in: BaseModel) -> bool:
            return True

        def can_read(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
            return False

        def can_delete(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def filter_list_query(
            self, user: User, query: SelectQuery[SQLModel]
        ) -> SelectQuery[SQLModel]:
            return query

    schemas = derive_schemas(Book)
    called = []

    async def before_update(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("before")

    async def after_update(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("after")

    router = (
        CrudRouterBuilder()
        .with_hooks(HooksConfig(before_update=before_update, after_update=after_update))
        .with_operations({Operation.CREATE, Operation.UPDATE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint
    update_ep = get_route(router, "/{id}", "PATCH").endpoint
    async for session in get_session():
        created = await create_ep(
            obj_in=schemas.create(title="Up", isbn="U"),
            session=session,
            user=None,
        )
        result = await update_ep(
            id=created.id,
            obj_in=schemas.update(title="Up2"),
            session=session,
            user=None,
        )
        assert result.title == "Up2"
        assert called == ["before", "after"]

    router_forbidden = (
        CrudRouterBuilder()
        .with_policy(DenyUpdate())
        .with_operations({Operation.CREATE, Operation.UPDATE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_forbidden = get_route(router_forbidden, "/", "POST").endpoint
    update_forbidden = get_route(router_forbidden, "/{id}", "PATCH").endpoint
    async for session in get_session():
        created = await create_forbidden(
            obj_in=schemas.create(title="DeniedUp", isbn="DU"),
            session=session,
            user=None,
        )
        with pytest.raises(ForbiddenError):
            await update_forbidden(
                id=created.id,
                obj_in=schemas.update(title="x"),
                session=session,
                user=None,
            )
        with pytest.raises(NotFoundError):
            await update_forbidden(
                id=9999,
                obj_in=schemas.update(title="x"),
                session=session,
                user=None,
            )


async def test_delete_branches(get_session: SessionFactory) -> None:
    class DenyDelete:
        def can_create(self, user: User, obj_in: BaseModel) -> bool:
            return True

        def can_read(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
            return True

        def can_delete(self, user: User, db_obj: SQLModel) -> bool:
            return False

        def filter_list_query(
            self, user: User, query: SelectQuery[SQLModel]
        ) -> SelectQuery[SQLModel]:
            return query

    schemas = derive_schemas(Book)
    called = []

    async def before_delete(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("before")

    async def after_delete(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("after")

    router = (
        CrudRouterBuilder()
        .with_hooks(HooksConfig(before_delete=before_delete, after_delete=after_delete))
        .with_operations({Operation.CREATE, Operation.DELETE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint
    delete_ep = get_route(router, "/{id}", "DELETE").endpoint
    async for session in get_session():
        created = await create_ep(
            obj_in=schemas.create(title="Del", isbn="D"),
            session=session,
            user=None,
        )
        await delete_ep(id=created.id, session=session, user=None)
        assert called == ["before", "after"]

    router_forbidden = (
        CrudRouterBuilder()
        .with_policy(DenyDelete())
        .with_operations({Operation.CREATE, Operation.DELETE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_forbidden = get_route(router_forbidden, "/", "POST").endpoint
    delete_forbidden = get_route(router_forbidden, "/{id}", "DELETE").endpoint
    async for session in get_session():
        created = await create_forbidden(
            obj_in=schemas.create(title="DeniedDel", isbn="DD"),
            session=session,
            user=None,
        )
        with pytest.raises(ForbiddenError):
            await delete_forbidden(id=created.id, session=session, user=None)
        with pytest.raises(NotFoundError):
            await delete_forbidden(id=9999, session=session, user=None)


async def test_bulk_create_forbidden(get_session: SessionFactory) -> None:
    class DenyCreate:
        def can_create(self, user: User, obj_in: BaseModel) -> bool:
            return False

        def can_read(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
            return True

        def can_delete(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def filter_list_query(
            self, user: User, query: SelectQuery[SQLModel]
        ) -> SelectQuery[SQLModel]:
            return query

    router = (
        CrudRouterBuilder()
        .with_policy(DenyCreate())
        .with_operations({Operation.BULK_CREATE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    bulk_create_ep = get_route(router, "/bulk", "POST").endpoint
    async for session in get_session():
        with pytest.raises(ForbiddenError):
            await bulk_create_ep(
                objects_in=[derive_schemas(Book).create(title="T", isbn="I")],
                session=session,
                user=None,
            )


async def test_bulk_update_forbidden_and_not_found(
    get_session: SessionFactory,
) -> None:
    class DenyUpdate:
        def can_create(self, user: User, obj_in: BaseModel) -> bool:
            return True

        def can_read(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
            return False

        def can_delete(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def filter_list_query(
            self, user: User, query: SelectQuery[SQLModel]
        ) -> SelectQuery[SQLModel]:
            return query

    schemas = derive_schemas(Book)
    router = (
        CrudRouterBuilder()
        .with_operations({Operation.CREATE, Operation.BULK_UPDATE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint
    bulk_update_ep = get_route(router, "/bulk", "PATCH").endpoint

    BulkUpdateItem = create_model(
        "BulkUpdateForbidden",
        __base__=BaseModel,
        id=(int, ...),
        title=(str | None, None),
    )

    async for session in get_session():
        await create_ep(
            obj_in=schemas.create(title="Bulk", isbn="B"),
            session=session,
            user=None,
        )
        with pytest.raises(NotFoundError):
            await bulk_update_ep(
                items_in=[BulkUpdateItem(id=9999, title="X")],
                session=session,
                user=None,
            )

    router_forbidden = (
        CrudRouterBuilder()
        .with_policy(DenyUpdate())
        .with_operations({Operation.CREATE, Operation.BULK_UPDATE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep2 = get_route(router_forbidden, "/", "POST").endpoint
    bulk_update_forbidden = get_route(router_forbidden, "/bulk", "PATCH").endpoint
    async for session in get_session():
        created = await create_ep2(
            obj_in=schemas.create(title="Bulk2", isbn="B2"),
            session=session,
            user=None,
        )
        with pytest.raises(ForbiddenError):
            await bulk_update_forbidden(
                items_in=[BulkUpdateItem(id=created.id, title="X")],
                session=session,
                user=None,
            )


async def test_bulk_delete_branches(get_session: SessionFactory) -> None:
    schemas = derive_schemas(Book)
    router = (
        CrudRouterBuilder()
        .with_operations({Operation.CREATE, Operation.BULK_DELETE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint
    bulk_delete_ep = get_route(router, "/bulk", "DELETE").endpoint

    from pydantic import create_model as _cm

    BookIdentifier = _cm("BookIdentifier", __base__=BaseModel, id=(int, ...))

    async for session in get_session():
        created = await create_ep(
            obj_in=schemas.create(title="SB", isbn="SB1"),
            session=session,
            user=None,
        )
        await bulk_delete_ep(
            items_in=[BookIdentifier(id=created.id)], session=session, user=None
        )
        with pytest.raises(NotFoundError):
            await bulk_delete_ep(
                items_in=[BookIdentifier(id=9999)], session=session, user=None
            )


async def test_bulk_delete_forbidden(get_session: SessionFactory) -> None:
    class DenyDelete:
        def can_create(self, user: User, obj_in: BaseModel) -> bool:
            return True

        def can_read(self, user: User, db_obj: SQLModel) -> bool:
            return True

        def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
            return True

        def can_delete(self, user: User, db_obj: SQLModel) -> bool:
            return False

        def filter_list_query(
            self, user: User, query: SelectQuery[SQLModel]
        ) -> SelectQuery[SQLModel]:
            return query

    schemas = derive_schemas(Book)
    router = (
        CrudRouterBuilder()
        .with_policy(DenyDelete())
        .with_operations({Operation.CREATE, Operation.BULK_DELETE})
        .build_routers(models=[Book], session_dep=get_session)[0]
    )
    create_ep = get_route(router, "/", "POST").endpoint
    bulk_delete_ep = get_route(router, "/bulk", "DELETE").endpoint
    async for session in get_session():
        created = await create_ep(
            obj_in=schemas.create(title="FD", isbn="FD"),
            session=session,
            user=None,
        )
        from pydantic import create_model as _cm2

        BookId = _cm2("BookIdentifier", __base__=BaseModel, id=(int, ...))
        with pytest.raises(ForbiddenError):
            await bulk_delete_ep(
                items_in=[BookId(id=created.id)], session=session, user=None
            )
