"""Nox configuration for SQLAlchemy."""

from __future__ import annotations

import os
from pathlib import Path
import sys
from typing import Dict
from typing import List
from typing import Set

import nox

if sys.version_info > (3, 12):
    nox.needs_version = ">=2025.10.16"

nox.options.default_venv_backend = "venv"

if True:
    sys.path.insert(0, ".")
    from tools.toxnox import apply_pytest_opts
    from tools.toxnox import tox_parameters


PYTHON_VERSIONS = [
    "3.7",
    "3.8",
    "3.9",
    "3.10",
    "3.11",
    "3.12",
    "3.13",
    "3.13t",
    "3.14",
    "3.14t",
]
DATABASES = ["sqlite", "sqlite_file", "postgresql", "mysql", "oracle", "mssql"]
CEXT = ["_auto", "cext", "nocext"]
GREENLET = ["_greenlet", "nogreenlet"]
BACKENDONLY = ["_all", "backendonly", "memusage"]

# table of ``--dbdriver`` names to use on the pytest command line, which
# match to dialect names
DB_CLI_NAMES = {
    "sqlite": {
        "nogreenlet": {"sqlite", "pysqlite_numeric"},
        "greenlet": {"aiosqlite"},
    },
    "sqlite_file": {
        "nogreenlet": {"sqlite"},
        "greenlet": {"aiosqlite"},
    },
    "postgresql": {
        "nogreenlet": {"psycopg2", "pg8000", "psycopg"},
        "greenlet": {"asyncpg", "psycopg_async"},
    },
    "mysql": {
        "nogreenlet": {"mysqldb", "pymysql", "mariadbconnector"},
        "greenlet": {"asyncmy", "aiomysql"},
    },
    "oracle": {
        "nogreenlet": {"cx_oracle", "oracledb"},
        "greenlet": {"oracledb_async"},
    },
    "mssql": {"nogreenlet": {"pyodbc", "pymssql"}, "greenlet": {"aioodbc"}},
}


def _setup_for_driver(
    session: nox.Session,
    cmd: List[str],
    basename: str,
    greenlet: bool = False,
) -> None:

    # install driver deps listed out in pyproject.toml
    nogreenlet_deps = f"tests-{basename.replace('_', '-')}"
    greenlet_deps = f"tests-{basename.replace('_', '-')}-asyncio"

    deps = nox.project.dependency_groups(
        pyproject,
        (greenlet_deps if greenlet else nogreenlet_deps),
    )
    if deps:
        session.install(*deps)

    # set up top level ``--db`` sent to pytest command line, which looks
    # up a base URL in the [db] section of setup.cfg.   Environment variable
    # substitution used by CI is also available.

    # e.g. TOX_POSTGRESQL, TOX_MYSQL, etc.
    dburl_env = f"TOX_{basename.upper()}"
    # e.g. --db=postgresql, --db=mysql, etc.
    default_dburl = f"--db={basename}"
    cmd.extend(os.environ.get(dburl_env, default_dburl).split())

    # set up extra drivers using --dbdriver.   this first looks in
    # an environment variable before making use of the DB_CLI_NAMES
    # lookup table

    # e.g. EXTRA_PG_DRIVERS, EXTRA_MYSQL_DRIVERS, etc.
    if basename == "postgresql":
        extra_driver_env = "EXTRA_PG_DRIVERS"
    else:
        extra_driver_env = f"EXTRA_{basename.upper()}_DRIVERS"
    env_dbdrivers = os.environ.get(extra_driver_env, None)
    if env_dbdrivers:
        cmd.extend(env_dbdrivers.split())
        return

    # use fixed names in DB_CLI_NAMES
    extra_drivers: Dict[str, Set[str]] = DB_CLI_NAMES[basename]
    dbdrivers = extra_drivers["nogreenlet"]
    if greenlet:
        dbdrivers.update(extra_drivers["greenlet"])

    # use equals sign so that we avoid
    # https://github.com/pytest-dev/pytest/issues/13913
    cmd.extend([f"--dbdriver={dbdriver}" for dbdriver in dbdrivers])


pyproject = nox.project.load_toml("pyproject.toml")

nox.options.sessions = ["tests"]
nox.options.tags = ["py"]


@nox.session()
@tox_parameters(
    ["python", "database", "cext", "greenlet", "backendonly"],
    [
        PYTHON_VERSIONS,
        DATABASES,
        CEXT,
        GREENLET,
        BACKENDONLY,
    ],
)
def tests(
    session: nox.Session,
    database: str,
    greenlet: str,
    backendonly: str,
    cext: str,
) -> None:
    """run the main test suite"""

    _tests(
        session,
        database,
        greenlet=greenlet == "_greenlet",
        backendonly=backendonly == "backendonly",
        platform_intensive=backendonly == "memusage",
        cext=cext,
    )


@nox.session(name="coverage")
@tox_parameters(
    ["database", "cext", "backendonly"],
    [DATABASES, CEXT, ["_all", "backendonly"]],
    base_tag="coverage",
)
def coverage(
    session: nox.Session, database: str, cext: str, backendonly: str
) -> None:
    """Run tests with coverage."""

    _tests(
        session,
        database,
        cext,
        timing_intensive=False,
        backendonly=backendonly == "backendonly",
        coverage=True,
    )


@nox.session(name="github-cext-greenlet")
def github_cext_greenlet(session: nox.Session) -> None:
    """run tests for github actions"""

    _tests(session, "sqlite", "cext", greenlet=True, timing_intensive=False)


@nox.session(name="github-cext")
def github_cext(session: nox.Session) -> None:
    """run tests for github actions"""

    _tests(session, "sqlite", "cext", greenlet=False, timing_intensive=False)


@nox.session(name="github-nocext")
def github_nocext(session: nox.Session) -> None:
    """run tests for github actions"""

    _tests(session, "sqlite", "cext", greenlet=False)


def _tests(
    session: nox.Session,
    database: str,
    cext: str = "_auto",
    greenlet: bool = True,
    backendonly: bool = False,
    platform_intensive: bool = False,
    timing_intensive: bool = True,
    coverage: bool = False,
) -> None:

    # ensure external PYTHONPATH not interfering
    session.env["PYTHONPATH"] = ""

    # PYTHONNOUSERSITE - this *MUST* be set so that the ./lib/ import
    # set up explicitly in test/conftest.py is *disabled*, so that
    # when SQLAlchemy is built into the .nox area, we use that and not the
    # local checkout, at least when usedevelop=False
    session.env["PYTHONNOUSERSITE"] = "1"

    freethreaded = isinstance(session.python, str) and session.python.endswith(
        "t"
    )

    if freethreaded:
        session.env["PYTHON_GIL"] = "0"

        # greenlet frequently crashes with freethreading, so omit
        # for the near future
        greenlet = False

    session.env["SQLALCHEMY_WARN_20"] = "1"

    if cext == "cext":
        session.env["REQUIRE_SQLALCHEMY_CEXT"] = "1"
    elif cext == "nocext":
        session.env["DISABLE_SQLALCHEMY_CEXT"] = "1"

    includes_excludes: dict[str, list[str]] = {"k": [], "m": []}

    if coverage:
        timing_intensive = False

    if platform_intensive:
        # platform_intensive refers to test/aaa_profiling/test_memusage.py.
        # it's only run exclusively of all other tests.   does not include
        # greenlet related tests
        greenlet = False
        # with "-m memory_intensive", only that suite will run, all
        # other tests will be deselected by pytest
        includes_excludes["m"].append("memory_intensive")
    elif backendonly:
        # with "-m backendonly", only tests with the backend pytest mark
        # (or pytestplugin equivalent, like __backend__) will be selected
        # by pytest.
        # memory intensive is deselected to prevent these from running
        includes_excludes["m"].extend(["backend", "not memory_intensive"])
    else:
        includes_excludes["m"].append("not memory_intensive")

        # the mypy suite is also run exclusively from the test_mypy
        # session
        includes_excludes["m"].append("not mypy")

        if not timing_intensive:
            includes_excludes["m"].append("not timing_intensive")

    cmd = ["python", "-m", "pytest"]

    cmd.extend(os.environ.get("TOX_WORKERS", "-n4").split())

    if coverage:
        assert not platform_intensive
        includes_excludes["k"].append("not aaa_profiling")
        session.install("-e", ".")
        session.install(*nox.project.dependency_groups(pyproject, "coverage"))
    else:
        session.install(".")

    session.install(*nox.project.dependency_groups(pyproject, "tests"))

    if greenlet:
        session.install(
            *nox.project.dependency_groups(pyproject, "tests_greenlet")
        )
    else:
        # note: if on SQLAlchemy 2.0, for "nogreenlet" need to do an explicit
        # uninstall of greenlet since it's included in sqlalchemy dependencies
        # in 2.1 it's an optional dependency
        session.run("pip", "uninstall", "-y", "greenlet")

    _setup_for_driver(session, cmd, database, greenlet=greenlet)

    for letter, collection in includes_excludes.items():
        if collection:
            cmd.extend([f"-{letter}", " and ".join(collection)])

    posargs = apply_pytest_opts(
        session,
        "sqlalchemy",
        [
            database,
            cext,
            "_greenlet" if greenlet else "nogreenlet",
            "memusage" if platform_intensive else "_nomemusage",
            "backendonly" if backendonly else "_notbackendonly",
        ],
        coverage=coverage,
    )

    if database in ["oracle", "mssql"]:
        cmd.extend(["--low-connections"])

    if database in ["oracle", "mssql", "sqlite_file"]:
        # use equals sign so that we avoid
        # https://github.com/pytest-dev/pytest/issues/13913
        cmd.extend(["--write-idents=db_idents.txt"])

    cmd.extend(posargs)

    try:
        session.run(*cmd)
    finally:
        # Run cleanup for oracle/mssql
        if database in ["oracle", "mssql", "sqlite_file"] and os.path.exists(
            "db_idents.txt"
        ):
            session.run("python", "reap_dbs.py", "db_idents.txt")
            os.unlink("db_idents.txt")


@nox.session(name="pep484")
def test_pep484(session: nox.Session) -> None:
    """Run mypy type checking."""

    session.install(*nox.project.dependency_groups(pyproject, "mypy"))

    session.install("-e", ".")

    session.run(
        "mypy",
        "noxfile.py",
        "./lib/sqlalchemy",
    )


@nox.session(name="mypy")
def test_mypy(session: nox.Session) -> None:
    """run the typing integration test suite"""

    session.install(*nox.project.dependency_groups(pyproject, "mypy"))

    session.install("-e", ".")

    posargs = apply_pytest_opts(
        session,
        "sqlalchemy",
        ["mypy"],
    )

    cmd = ["pytest", "-m", "mypy"]

    session.run(*cmd, *posargs)


@nox.session(name="pep8")
def test_pep8(session: nox.Session) -> None:
    """Run linting and formatting checks."""

    for pattern in ["*.so", "*.pyd", "*.dylib"]:
        for filepath in Path("lib/sqlalchemy").rglob(pattern):
            filepath.unlink()

    session.install("-e", ".")

    session.install(*nox.project.dependency_groups(pyproject, "lint"))

    for cmd in [
        "flake8p ./lib/ ./test/ ./examples/ noxfile.py "
        "setup.py doc/build/conf.py",
        # run "unused argument" lints on asyncio, as we have a lot of
        # proxy methods here
        "flake8p  --ignore='' --select='U100,U101' "
        "./lib/sqlalchemy/ext/asyncio "
        "./lib/sqlalchemy/orm/scoping.py",
        "black --check ./lib/ ./test/ ./examples/ setup.py doc/build/conf.py",
        "slotscheck -m sqlalchemy",
        "python ./tools/format_docs_code.py --check",
        "python ./tools/generate_tuple_map_overloads.py --check",
        "python ./tools/generate_proxy_methods.py --check",
        "python ./tools/sync_test_files.py --check",
        "python ./tools/generate_sql_functions.py --check",
        "python ./tools/normalize_file_headers.py --check",
        "python ./tools/walk_packages.py",
    ]:

        session.run(*cmd.split())
