"""Common setup and fixtures for the pytest suite used by this service."""
import pytest
from flask_migrate import Migrate, upgrade
from sqlalchemy import event, text
from sqlalchemy.schema import DropConstraint, MetaData

from api import create_app, setup_jwt_manager
from api.models import db as _db
from formsflow_api_utils.utils import jwt as _jwt, Service


@pytest.fixture(scope="session")
def app():
    """Return a session-wide application configured in TEST mode."""
    _app = create_app("testing")

    return _app


@pytest.fixture(scope="function")
def app_ctx(event_loop):  # pylint: disable=unused-argument
    # def app_ctx():
    """Return a session-wide application configured in TEST mode."""
    _app = create_app("testing")
    with _app.app_context():
        yield _app


@pytest.fixture
def config(app):  # pylint: disable=redefined-outer-name
    """Return the application config."""
    return app.config


@pytest.fixture(scope="function")
def app_request():
    """Return a session-wide application configured in TEST mode."""
    _app = create_app("testing")

    return _app


@pytest.fixture(scope="session")
def client(app):  # pylint: disable=redefined-outer-name
    """Return a session-wide Flask test client."""
    return app.test_client()


@pytest.fixture(scope="session")
def client_ctx(app):  # pylint: disable=redefined-outer-name
    """Return session-wide Flask test client."""
    with app.test_client() as _client:
        yield _client


@pytest.fixture(scope="session")
def db(app):  # pylint: disable=redefined-outer-name, invalid-name
    """Return a session-wide initialised database.

    Drops all existing tables - Meta follows Postgres FKs
    """
    if app.config["DATABASE_SUPPORT"] == Service.ENABLED.value:
        with app.app_context():
            # Clear out any existing tables
            metadata = MetaData(_db.engine)
            metadata.reflect()
            for table in metadata.tables.values():
                for fk in table.foreign_keys:  # pylint: disable=invalid-name
                    _db.engine.execute(DropConstraint(fk.constraint))
            metadata.drop_all()
            _db.drop_all()

            sequence_sql = """SELECT sequence_name FROM information_schema.sequences
                            WHERE sequence_schema='public'
                        """

            sess = _db.session()
            for seq in [name for (name,) in sess.execute(text(sequence_sql))]:
                try:
                    sess.execute(text("DROP SEQUENCE public.%s ;" % seq))
                    print("DROP SEQUENCE public.%s " % seq)
                except Exception as err:  # pylint: disable=broad-except
                    print(f"Error: {err}")
            sess.commit()

            # ############################################
            # There are 2 approaches, an empty database, or the same one that the app
            # will use create the tables
            #     _db.create_all()
            # or
            # Use Alembic to load all of the DB revisions including supporting lookup data
            # This is the path we'll use in selfservice_api!!

            # even though this isn't referenced directly,
            # it sets up the internal configs that upgrade needs
            Migrate(app, _db)
            upgrade()

            return _db


@pytest.fixture(scope="function")
def session(app, db):  # pylint: disable=redefined-outer-name, invalid-name
    """Return a function-scoped session."""
    with app.app_context():
        if app.config["DATABASE_SUPPORT"] == Service.ENABLED.value:
            conn = db.engine.connect()
            txn = conn.begin()

            options = dict(bind=conn, binds={})
            sess = db.create_scoped_session(options=options)

            # establish  a SAVEPOINT just before beginning the test
            # (http://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#using-savepoint)
            sess.begin_nested()

            @event.listens_for(sess(), "after_transaction_end")
            def restart_savepoint(sess2, trans):  # pylint: disable=unused-variable
                # Detecting whether this is indeed the nested transaction of the test
                if (
                    trans.nested and not trans._parent.nested
                ):  # pylint: disable=protected-access
                    # Handle where test DOESN'T session.commit(),
                    sess2.expire_all()
                    sess.begin_nested()

            db.session = sess

            sql = text("select 1")
            sess.execute(sql)

            yield sess

            # Cleanup
            sess.remove()
            # This instruction rollsback any commit that were executed in the tests.
            txn.rollback()
            conn.close()
        else:
            yield app


@pytest.fixture(scope="session")
def jwt(app):
    """Return session-wide jwt manager."""
    return _jwt


@pytest.fixture(scope="session", autouse=True)
def auto(docker_services, app):
    """Spin up a keycloak instance and initialize jwt."""
    if app.config.get("USE_DOCKER_MOCK"):
        docker_services.start("keycloak")
        docker_services.wait_for_service("keycloak", 8081)
        setup_jwt_manager(app, _jwt)


@pytest.fixture(scope="session")
def docker_compose_files(pytestconfig):
    """Get the docker-compose.yml absolute path."""
    import os

    return [
        os.path.join(str(pytestconfig.rootdir), "tests/docker", "docker-compose.yml")
    ]
