import os
import sys
import tempfile
from glob import glob
from subprocess import check_call

import pytest
from packaging.version import parse as V
from pytest import raises
from traitlets.config import Config

from .. import orm
from ..app import NewToken, UpgradeDB
from ..scopes import _check_scopes_exist

here = os.path.abspath(os.path.dirname(__file__))
populate_db = os.path.join(here, 'populate_db.py')


def generate_old_db(env_dir, hub_version, db_url):
    """Generate an old jupyterhub database

    Installs a particular jupyterhub version in a virtualenv
    and runs populate_db.py to populate a database
    """
    env_pip = os.path.join(env_dir, 'bin', 'pip')
    env_py = os.path.join(env_dir, 'bin', 'python')
    check_call([sys.executable, '-m', 'virtualenv', env_dir])
    pkgs = ['jupyterhub==' + hub_version]

    # older jupyterhub needs older sqlachemy version
    if V(hub_version) < V("2"):
        pkgs.append('sqlalchemy<1.4')
    elif V(hub_version) < V("3.1.1"):
        pkgs.append('sqlalchemy<2')

    if 'mysql' in db_url:
        pkgs.append('mysqlclient')
    elif 'postgres' in db_url:
        if V(hub_version) < V("3.1.1"):
            pkgs.append('psycopg2-binary')
            db_url = db_url.replace("psycopg:", "psycopg2:")
        else:
            pkgs.append('psycopg[binary]')
    check_call([env_pip, 'install'] + pkgs)
    check_call([env_py, populate_db, db_url])


# changes to this version list must also be reflected
# in ci/init-db.sh
@pytest.mark.parametrize(
    'hub_version', ['1.1.0', '1.2.2', '1.3.0', '1.5.0', '2.1.1', '3.1.1', '4.1.6']
)
async def test_upgrade(tmpdir, hub_version):
    if sys.version_info >= (3, 13) and V(hub_version) < V("4"):
        pytest.skip("jupyterhub < 4 needs Python 3.12 or earlier")
    db_url = os.getenv('JUPYTERHUB_TEST_DB_URL')
    if db_url:
        db_url += '_upgrade_' + hub_version.replace('.', '')
    else:
        db_url = 'sqlite:///jupyterhub.sqlite'
    tmpdir.chdir()

    # use persistent temp env directory
    # to reuse across multiple runs
    env_dir = os.path.join(tempfile.gettempdir(), f'test-hub-upgrade-{hub_version}')

    generate_old_db(env_dir, hub_version, db_url)

    cfg = Config()
    cfg.JupyterHub.db_url = db_url

    tokenapp = NewToken(config=cfg)
    tokenapp.initialize(['admin'])
    with raises(SystemExit):
        tokenapp.start()

    if 'sqlite' in db_url:
        fname = db_url.split(':///')[1]
        sqlite_files = glob(os.path.join(str(tmpdir), 'jupyterhub.sqlite*'))
        assert len(sqlite_files) == 1

    upgradeapp = UpgradeDB(config=cfg)
    upgradeapp.initialize([])
    upgradeapp.start()

    # check that backup was created:
    if 'sqlite' in db_url:
        sqlite_files = glob(os.path.join(str(tmpdir), 'jupyterhub.sqlite*'))
        assert len(sqlite_files) == 2

    # run tokenapp again, it should work
    tokenapp.start()

    db = orm.new_session_factory(db_url)()
    query = db.query(orm.APIToken)
    assert query.count() >= 1
    for token in query:
        assert token.scopes, f"Upgraded token {token} has no scopes"
        _check_scopes_exist(token.scopes)

    # make sure migrated roles are not managed or null
    for role in db.query(orm.Role):
        assert role.managed_by_auth is False
    for assignment_table in orm._role_associations.values():
        for assignment in db.query(assignment_table):
            assert assignment.managed_by_auth is False
