"""
This module contains tests for the utility functions in the test_mapping module.
"""

from contextlib import contextmanager

import pytest
from sqlalchemy import (
    Column,
    Index,
    Integer,
    UniqueConstraint,
)
from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import registry

from galaxy.model import _HasTable
from galaxy.model.unittest_utils.model_testing_utils import (
    dbcleanup,
    dbcleanup_wrapper,
    delete_from_database,
    get_stored_instance_by_id,
    get_stored_obj,
    initialize_model,
    persist,
)


def test_persist(session):
    """
    Verify item is stored in database.
    We call persist() with default arg value: `return_id=True`. Passing `False` results
    in the same functionality except the return value of the funtion is `None`.
    """
    instance = Foo()
    instance_id = persist(session, instance)  # store instance in database
    assert instance_id == 1  # id was assigned
    assert instance not in session  # instance was expunged from session

    stored_instance = get_stored_instance_by_id(session, Foo, instance_id)
    assert stored_instance.id == instance.id  # instance can be retrieved by id
    assert stored_instance is not instance  # retrieved instance is not the same object


def test_get_stored_obj_must_have_obj_id_xor_where_clause():
    """
    Verify that function must be called with either obj_id or where_clause, but not both.
    """
    with pytest.raises(AssertionError):
        get_stored_obj(None, None, obj_id=1, where_clause="a")
    with pytest.raises(AssertionError):
        get_stored_obj(None, None, obj_id=None, where_clause=None)


def test_get_stored_obj_by_id(session):
    """Verify item is retrieved from database by id."""
    instance = Foo()
    id = persist(session, instance)

    stored_instance = get_stored_obj(session, Foo, id)
    assert stored_instance.id == instance.id


def test_get_stored_obj_by_where_clause(session):
    """Verify item is retrieved from database by a custom WHERE clause."""
    instance1 = Foo()
    instance2 = Foo()
    id1 = persist(session, instance1)
    id2 = persist(session, instance2)

    where_clause = Foo.__table__.c.id == id1
    stored_instance = get_stored_obj(session, Foo, where_clause=where_clause)
    assert stored_instance.id == instance1.id

    where_clause = Foo.__table__.c.id == id2
    stored_instance = get_stored_obj(session, Foo, where_clause=where_clause)
    assert stored_instance.id == instance2.id


def test_delete_from_database_one_item(session):
    """Verify item is deleted from database."""
    instance = Foo()
    id = persist(session, instance)  # store instance in database

    stored_instance = get_stored_instance_by_id(session, Foo, id)
    assert stored_instance is not None  # instance is present in database

    delete_from_database(session, instance)  # delete instance from database

    with pytest.raises(NoResultFound):  # instance no longer present in database
        stored_instance = get_stored_instance_by_id(session, Foo, id)


def test_delete_from_database_multiple_items(session):
    """Verify multiple items are deleted from database."""
    instance1 = Foo()
    instance2 = Foo()
    id1 = persist(session, instance1)
    id2 = persist(session, instance2)

    delete_from_database(session, [instance1, instance2])

    with pytest.raises(NoResultFound):
        get_stored_instance_by_id(session, Foo, id1)
    with pytest.raises(NoResultFound):
        get_stored_instance_by_id(session, Foo, id2)


def test_dbcleanup_by_id(session):
    instance = Foo()
    with dbcleanup(session, instance) as instance_id:
        stored_instance = get_stored_instance_by_id(session, Foo, instance_id)
        assert stored_instance  # has been stored in the database

    with pytest.raises(NoResultFound):  # has been deleted from the database
        get_stored_instance_by_id(session, Foo, instance_id)


def test_dbcleanup_by_where_clause(session):
    instance = Foo()
    where_clause = Foo.__table__.c.id == 1

    with dbcleanup(session, instance, where_clause):
        stored_instance = get_stored_instance_by_id(session, Foo, where_clause)
        assert stored_instance  # has been stored in the database

    with pytest.raises(NoResultFound):  # has been deleted from the database
        get_stored_instance_by_id(session, Foo, stored_instance.id)


def test_dbcleanup_wrapper(session):
    """
    Verify dbcleanup_wrapper has same effect as dbcleanup context manager,
    and yields the object instance.
    """

    @contextmanager  # we need a context manager to similate scope
    def managed(a, b):
        yield from dbcleanup_wrapper(a, b)

    instance = Foo()
    # this will call dbcleanup_wrapper that stores instance in the database and returns a reference to it
    with managed(session, instance) as instance2:
        assert instance2 is instance  # instance2 should be a reference to instance
        stored_instance = get_stored_instance_by_id(session, Foo, instance.id)
        assert stored_instance  # has been stored in the database

    # on exit we expect the entity to be deleted from the database
    with pytest.raises(NoResultFound):  # has been deleted from the database
        get_stored_instance_by_id(session, Foo, instance.id)


# Test utilities

mapper_registry = registry()


@mapper_registry.mapped
class Foo(_HasTable):
    __tablename__ = "foo"
    id = Column(Integer, primary_key=True)
    field1 = Column(Integer)


@mapper_registry.mapped
class Bar(_HasTable):
    __tablename__ = "bar"
    id = Column(Integer, primary_key=True)
    field1 = Column(Integer)
    field2 = Column(Integer)
    __table_args__ = (
        Index("ix", "field1"),
        UniqueConstraint("field2"),
    )


@pytest.fixture(scope="module")
def init_model(engine):
    """Create model objects in the engine's database."""
    # Must use the same engine as the session fixture used by this module.
    initialize_model(mapper_registry, engine)
