import os
import platform
import re
import shlex
import subprocess
import sys
import textwrap
from collections import namedtuple
from pathlib import Path

import pytest

from numpy.f2py.f2py2e import main as f2pycli
from numpy.testing._private.utils import NOGIL_BUILD

from . import util

#######################
# F2PY Test utilities #
######################

# Tests for CLI commands which call meson will fail if no compilers are present, these are to be skipped

def compiler_check_f2pycli():
    if not util.has_fortran_compiler():
        pytest.skip("CLI command needs a Fortran compiler")
    else:
        f2pycli()

#########################
# CLI utils and classes #
#########################


PPaths = namedtuple("PPaths", "finp, f90inp, pyf, wrap77, wrap90, cmodf")


def get_io_paths(fname_inp, mname="untitled"):
    """Takes in a temporary file for testing and returns the expected output and input paths

    Here expected output is essentially one of any of the possible generated
    files.

    ..note::

         Since this does not actually run f2py, none of these are guaranteed to
         exist, and module names are typically incorrect

    Parameters
    ----------
    fname_inp : str
                The input filename
    mname : str, optional
                The name of the module, untitled by default

    Returns
    -------
    genp : NamedTuple PPaths
            The possible paths which are generated, not all of which exist
    """
    bpath = Path(fname_inp)
    return PPaths(
        finp=bpath.with_suffix(".f"),
        f90inp=bpath.with_suffix(".f90"),
        pyf=bpath.with_suffix(".pyf"),
        wrap77=bpath.with_name(f"{mname}-f2pywrappers.f"),
        wrap90=bpath.with_name(f"{mname}-f2pywrappers2.f90"),
        cmodf=bpath.with_name(f"{mname}module.c"),
    )


################
# CLI Fixtures #
################


@pytest.fixture(scope="session")
def hello_world_f90(tmpdir_factory):
    """Generates a single f90 file for testing"""
    fdat = util.getpath("tests", "src", "cli", "hiworld.f90").read_text()
    fn = tmpdir_factory.getbasetemp() / "hello.f90"
    fn.write_text(fdat, encoding="ascii")
    return fn


@pytest.fixture(scope="session")
def gh23598_warn(tmpdir_factory):
    """F90 file for testing warnings in gh23598"""
    fdat = util.getpath("tests", "src", "crackfortran", "gh23598Warn.f90").read_text()
    fn = tmpdir_factory.getbasetemp() / "gh23598Warn.f90"
    fn.write_text(fdat, encoding="ascii")
    return fn


@pytest.fixture(scope="session")
def gh22819_cli(tmpdir_factory):
    """F90 file for testing disallowed CLI arguments in ghff819"""
    fdat = util.getpath("tests", "src", "cli", "gh_22819.pyf").read_text()
    fn = tmpdir_factory.getbasetemp() / "gh_22819.pyf"
    fn.write_text(fdat, encoding="ascii")
    return fn


@pytest.fixture(scope="session")
def hello_world_f77(tmpdir_factory):
    """Generates a single f77 file for testing"""
    fdat = util.getpath("tests", "src", "cli", "hi77.f").read_text()
    fn = tmpdir_factory.getbasetemp() / "hello.f"
    fn.write_text(fdat, encoding="ascii")
    return fn


@pytest.fixture(scope="session")
def retreal_f77(tmpdir_factory):
    """Generates a single f77 file for testing"""
    fdat = util.getpath("tests", "src", "return_real", "foo77.f").read_text()
    fn = tmpdir_factory.getbasetemp() / "foo.f"
    fn.write_text(fdat, encoding="ascii")
    return fn

@pytest.fixture(scope="session")
def f2cmap_f90(tmpdir_factory):
    """Generates a single f90 file for testing"""
    fdat = util.getpath("tests", "src", "f2cmap", "isoFortranEnvMap.f90").read_text()
    f2cmap = util.getpath("tests", "src", "f2cmap", ".f2py_f2cmap").read_text()
    fn = tmpdir_factory.getbasetemp() / "f2cmap.f90"
    fmap = tmpdir_factory.getbasetemp() / "mapfile"
    fn.write_text(fdat, encoding="ascii")
    fmap.write_text(f2cmap, encoding="ascii")
    return fn

#########
# Tests #
#########

def test_gh22819_cli(capfd, gh22819_cli, monkeypatch):
    """Check that module names are handled correctly
    gh-22819
    Essentially, the -m name cannot be used to import the module, so the module
    named in the .pyf needs to be used instead

    CLI :: -m and a .pyf file
    """
    ipath = Path(gh22819_cli)
    monkeypatch.setattr(sys, "argv", f"f2py -m blah {ipath}".split())
    with util.switchdir(ipath.parent):
        f2pycli()
        gen_paths = [item.name for item in ipath.parent.rglob("*") if item.is_file()]
        assert "blahmodule.c" not in gen_paths  # shouldn't be generated
        assert "blah-f2pywrappers.f" not in gen_paths
        assert "test_22819-f2pywrappers.f" in gen_paths
        assert "test_22819module.c" in gen_paths


def test_gh22819_many_pyf(capfd, gh22819_cli, monkeypatch):
    """Only one .pyf file allowed
    gh-22819
    CLI :: .pyf files
    """
    ipath = Path(gh22819_cli)
    monkeypatch.setattr(sys, "argv", f"f2py -m blah {ipath} hello.pyf".split())
    with util.switchdir(ipath.parent):
        with pytest.raises(ValueError, match="Only one .pyf file per call"):
            f2pycli()


def test_gh23598_warn(capfd, gh23598_warn, monkeypatch):
    foutl = get_io_paths(gh23598_warn, mname="test")
    ipath = foutl.f90inp
    monkeypatch.setattr(
        sys, "argv",
        f'f2py {ipath} -m test'.split())

    with util.switchdir(ipath.parent):
        f2pycli()  # Generate files
        wrapper = foutl.wrap90.read_text()
        assert "intproductf2pywrap, intpr" not in wrapper


def test_gen_pyf(capfd, hello_world_f90, monkeypatch):
    """Ensures that a signature file is generated via the CLI
    CLI :: -h
    """
    ipath = Path(hello_world_f90)
    opath = Path(hello_world_f90).stem + ".pyf"
    monkeypatch.setattr(sys, "argv", f'f2py -h {opath} {ipath}'.split())

    with util.switchdir(ipath.parent):
        f2pycli()  # Generate wrappers
        out, _ = capfd.readouterr()
        assert "Saving signatures to file" in out
        assert Path(f'{opath}').exists()


def test_gen_pyf_stdout(capfd, hello_world_f90, monkeypatch):
    """Ensures that a signature file can be dumped to stdout
    CLI :: -h
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -h stdout {ipath}'.split())
    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "Saving signatures to file" in out
        assert "function hi() ! in " in out


def test_gen_pyf_no_overwrite(capfd, hello_world_f90, monkeypatch):
    """Ensures that the CLI refuses to overwrite signature files
    CLI :: -h without --overwrite-signature
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -h faker.pyf {ipath}'.split())

    with util.switchdir(ipath.parent):
        Path("faker.pyf").write_text("Fake news", encoding="ascii")
        with pytest.raises(SystemExit):
            f2pycli()  # Refuse to overwrite
            _, err = capfd.readouterr()
            assert "Use --overwrite-signature to overwrite" in err


@pytest.mark.skipif(sys.version_info <= (3, 12), reason="Python 3.12 required")
@pytest.mark.slow
def test_untitled_cli(capfd, hello_world_f90, monkeypatch):
    """Check that modules are named correctly

    CLI :: defaults
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f"f2py --backend meson -c {ipath}".split())
    with util.switchdir(ipath.parent):
        compiler_check_f2pycli()
        out, _ = capfd.readouterr()
        assert "untitledmodule.c" in out

@pytest.mark.slow
def test_no_distutils_backend(capfd, hello_world_f90, monkeypatch):
    """Check that distutils backend and related options fail
    CLI :: --fcompiler --help-link --backend distutils
    """
    MNAME = "hi"
    foutl = get_io_paths(hello_world_f90, mname=MNAME)
    ipath = foutl.f90inp
    monkeypatch.setattr(
        sys, "argv", f"f2py {ipath} -c --fcompiler=gfortran -m {MNAME}".split()
    )
    with util.switchdir(ipath.parent):
        compiler_check_f2pycli()
        out, _ = capfd.readouterr()
        assert "--fcompiler cannot be used with meson" in out

    monkeypatch.setattr(
        sys, "argv", ["f2py", "--help-link"]
    )
    with pytest.raises(SystemExit):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "Unknown option --help-link" in out

    monkeypatch.setattr(
        sys, "argv", ["f2py", "--backend", "distutils"]
    )
    with pytest.raises(SystemExit):
        compiler_check_f2pycli()
        f2pycli()
        out, _ = capfd.readouterr()
        assert "'distutils' backend was removed" in out

@pytest.mark.xfail
def test_f2py_skip(capfd, retreal_f77, monkeypatch):
    """Tests that functions can be skipped
    CLI :: skip:
    """
    foutl = get_io_paths(retreal_f77, mname="test")
    ipath = foutl.finp
    toskip = "t0 t4 t8 sd s8 s4"
    remaining = "td s0"
    monkeypatch.setattr(
        sys, "argv",
        f'f2py {ipath} -m test skip: {toskip}'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, err = capfd.readouterr()
        for skey in toskip.split():
            assert (
                f'buildmodule: Could not found the body of interfaced routine "{skey}". Skipping.'
                in err)
        for rkey in remaining.split():
            assert f'Constructing wrapper function "{rkey}"' in out


def test_f2py_only(capfd, retreal_f77, monkeypatch):
    """Test that functions can be kept by only:
    CLI :: only:
    """
    foutl = get_io_paths(retreal_f77, mname="test")
    ipath = foutl.finp
    toskip = "t0 t4 t8 sd s8 s4"
    tokeep = "td s0"
    monkeypatch.setattr(
        sys, "argv",
        f'f2py {ipath} -m test only: {tokeep}'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, err = capfd.readouterr()
        for skey in toskip.split():
            assert (
                f'buildmodule: Could not find the body of interfaced routine "{skey}". Skipping.'
                in err)
        for rkey in tokeep.split():
            assert f'Constructing wrapper function "{rkey}"' in out


def test_file_processing_switch(capfd, hello_world_f90, retreal_f77,
                                monkeypatch):
    """Tests that it is possible to return to file processing mode
    CLI :: :
    BUG: numpy-gh #20520
    """
    foutl = get_io_paths(retreal_f77, mname="test")
    ipath = foutl.finp
    toskip = "t0 t4 t8 sd s8 s4"
    ipath2 = Path(hello_world_f90)
    tokeep = "td s0 hi"  # hi is in ipath2
    mname = "blah"
    monkeypatch.setattr(
        sys,
        "argv",
        f'f2py {ipath} -m {mname} only: {tokeep} : {ipath2}'.split(
        ),
    )

    with util.switchdir(ipath.parent):
        f2pycli()
        out, err = capfd.readouterr()
        for skey in toskip.split():
            assert (
                f'buildmodule: Could not find the body of interfaced routine "{skey}". Skipping.'
                in err)
        for rkey in tokeep.split():
            assert f'Constructing wrapper function "{rkey}"' in out


def test_mod_gen_f77(capfd, hello_world_f90, monkeypatch):
    """Checks the generation of files based on a module name
    CLI :: -m
    """
    MNAME = "hi"
    foutl = get_io_paths(hello_world_f90, mname=MNAME)
    ipath = foutl.f90inp
    monkeypatch.setattr(sys, "argv", f'f2py {ipath} -m {MNAME}'.split())
    with util.switchdir(ipath.parent):
        f2pycli()

    # Always generate C module
    assert Path.exists(foutl.cmodf)
    # File contains a function, check for F77 wrappers
    assert Path.exists(foutl.wrap77)


def test_mod_gen_gh25263(capfd, hello_world_f77, monkeypatch):
    """Check that pyf files are correctly generated with module structure
    CLI :: -m <name> -h pyf_file
    BUG: numpy-gh #20520
    """
    MNAME = "hi"
    foutl = get_io_paths(hello_world_f77, mname=MNAME)
    ipath = foutl.finp
    monkeypatch.setattr(sys, "argv", f'f2py {ipath} -m {MNAME} -h hi.pyf'.split())
    with util.switchdir(ipath.parent):
        f2pycli()
        with Path('hi.pyf').open() as hipyf:
            pyfdat = hipyf.read()
            assert "python module hi" in pyfdat


def test_lower_cmod(capfd, hello_world_f77, monkeypatch):
    """Lowers cases by flag or when -h is present

    CLI :: --[no-]lower
    """
    foutl = get_io_paths(hello_world_f77, mname="test")
    ipath = foutl.finp
    capshi = re.compile(r"HI\(\)")
    capslo = re.compile(r"hi\(\)")
    # Case I: --lower is passed
    monkeypatch.setattr(sys, "argv", f'f2py {ipath} -m test --lower'.split())
    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert capslo.search(out) is not None
        assert capshi.search(out) is None
    # Case II: --no-lower is passed
    monkeypatch.setattr(sys, "argv",
                        f'f2py {ipath} -m test --no-lower'.split())
    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert capslo.search(out) is None
        assert capshi.search(out) is not None


def test_lower_sig(capfd, hello_world_f77, monkeypatch):
    """Lowers cases in signature files by flag or when -h is present

    CLI :: --[no-]lower -h
    """
    foutl = get_io_paths(hello_world_f77, mname="test")
    ipath = foutl.finp
    # Signature files
    capshi = re.compile(r"Block: HI")
    capslo = re.compile(r"Block: hi")
    # Case I: --lower is implied by -h
    # TODO: Clean up to prevent passing --overwrite-signature
    monkeypatch.setattr(
        sys,
        "argv",
        f'f2py {ipath} -h {foutl.pyf} -m test --overwrite-signature'.split(),
    )

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert capslo.search(out) is not None
        assert capshi.search(out) is None

    # Case II: --no-lower overrides -h
    monkeypatch.setattr(
        sys,
        "argv",
        f'f2py {ipath} -h {foutl.pyf} -m test --overwrite-signature --no-lower'
        .split(),
    )

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert capslo.search(out) is None
        assert capshi.search(out) is not None


def test_build_dir(capfd, hello_world_f90, monkeypatch):
    """Ensures that the build directory can be specified

    CLI :: --build-dir
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    odir = "tttmp"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --build-dir {odir}'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert f"Wrote C/API module \"{mname}\"" in out


def test_overwrite(capfd, hello_world_f90, monkeypatch):
    """Ensures that the build directory can be specified

    CLI :: --overwrite-signature
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(
        sys, "argv",
        f'f2py -h faker.pyf {ipath} --overwrite-signature'.split())

    with util.switchdir(ipath.parent):
        Path("faker.pyf").write_text("Fake news", encoding="ascii")
        f2pycli()
        out, _ = capfd.readouterr()
        assert "Saving signatures to file" in out


def test_latexdoc(capfd, hello_world_f90, monkeypatch):
    """Ensures that TeX documentation is written out

    CLI :: --latex-doc
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --latex-doc'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "Documentation is saved to file" in out
        with Path(f"{mname}module.tex").open() as otex:
            assert "\\documentclass" in otex.read()


def test_nolatexdoc(capfd, hello_world_f90, monkeypatch):
    """Ensures that TeX documentation is written out

    CLI :: --no-latex-doc
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --no-latex-doc'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "Documentation is saved to file" not in out

@pytest.mark.slow
def test_latex_doc_gh30268(tmp_path):

    if not util.has_fortran_compiler():
        pytest.skip("No Fortran compiler found")

    fsource = textwrap.dedent("""
        subroutine foo
        end
    """)

    fpath = tmp_path / "test_latex.f90"
    with open(fpath, "w") as f:
        f.write(fsource)

    cmd = [sys.executable, "-m", "numpy.f2py", "-c", str(fpath), "-m", "test_latex", "--latex-doc"]
    subprocess.check_call(cmd, cwd=tmp_path)


def test_shortlatex(capfd, hello_world_f90, monkeypatch):
    """Ensures that truncated documentation is written out

    TODO: Test to ensure this has no effect without --latex-doc
    CLI :: --latex-doc --short-latex
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(
        sys,
        "argv",
        f'f2py -m {mname} {ipath} --latex-doc --short-latex'.split(),
    )

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "Documentation is saved to file" in out
        with Path(f"./{mname}module.tex").open() as otex:
            assert "\\documentclass" not in otex.read()


def test_restdoc(capfd, hello_world_f90, monkeypatch):
    """Ensures that RsT documentation is written out

    CLI :: --rest-doc
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --rest-doc'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "ReST Documentation is saved to file" in out
        with Path(f"./{mname}module.rest").open() as orst:
            assert r".. -*- rest -*-" in orst.read()


def test_norestexdoc(capfd, hello_world_f90, monkeypatch):
    """Ensures that TeX documentation is written out

    CLI :: --no-rest-doc
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --no-rest-doc'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "ReST Documentation is saved to file" not in out


def test_debugcapi(capfd, hello_world_f90, monkeypatch):
    """Ensures that debugging wrappers are written

    CLI :: --debug-capi
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --debug-capi'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        with Path(f"./{mname}module.c").open() as ocmod:
            assert r"#define DEBUGCFUNCS" in ocmod.read()


@pytest.mark.skip(reason="Consistently fails on CI; noisy so skip not xfail.")
def test_debugcapi_bld(hello_world_f90, monkeypatch):
    """Ensures that debugging wrappers work

    CLI :: --debug-capi -c
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} -c --debug-capi'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        cmd_run = shlex.split(f"{sys.executable} -c \"import blah; blah.hi()\"")
        rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
        eout = ' Hello World\n'
        eerr = textwrap.dedent("""\
debug-capi:Python C/API function blah.hi()
debug-capi:float hi=:output,hidden,scalar
debug-capi:hi=0
debug-capi:Fortran subroutine `f2pywraphi(&hi)'
debug-capi:hi=0
debug-capi:Building return value.
debug-capi:Python C/API function blah.hi: successful.
debug-capi:Freeing memory.
        """)
        assert rout.stdout == eout
        assert rout.stderr == eerr


def test_wrapfunc_def(capfd, hello_world_f90, monkeypatch):
    """Ensures that fortran subroutine wrappers for F77 are included by default

    CLI :: --[no]-wrap-functions
    """
    # Implied
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv", f'f2py -m {mname} {ipath}'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
    out, _ = capfd.readouterr()
    assert r"Fortran 77 wrappers are saved to" in out

    # Explicit
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --wrap-functions'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert r"Fortran 77 wrappers are saved to" in out


def test_nowrapfunc(capfd, hello_world_f90, monkeypatch):
    """Ensures that fortran subroutine wrappers for F77 can be disabled

    CLI :: --no-wrap-functions
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(sys, "argv",
                        f'f2py -m {mname} {ipath} --no-wrap-functions'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert r"Fortran 77 wrappers are saved to" not in out


def test_inclheader(capfd, hello_world_f90, monkeypatch):
    """Add to the include directories

    CLI :: -include
    TODO: Document this in the help string
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    monkeypatch.setattr(
        sys,
        "argv",
        f'f2py -m {mname} {ipath} -include<stdbool.h> -include<stdio.h> '.
        split(),
    )

    with util.switchdir(ipath.parent):
        f2pycli()
        with Path(f"./{mname}module.c").open() as ocmod:
            ocmr = ocmod.read()
            assert "#include <stdbool.h>" in ocmr
            assert "#include <stdio.h>" in ocmr

@pytest.mark.skipif((platform.system() != 'Linux'), reason='Compiler required')
@pytest.mark.slow
def test_cli_obj(capfd, hello_world_f90, monkeypatch):
    """Ensures that the extra object can be specified when using meson backend
    """
    ipath = Path(hello_world_f90)
    mname = "blah"
    odir = "tttmp"
    obj = "extra.o"
    monkeypatch.setattr(sys, "argv",
                        f'f2py --backend meson --build-dir {odir} -m {mname} -c {obj} {ipath}'.split())

    with util.switchdir(ipath.parent):
        Path(obj).touch()
        compiler_check_f2pycli()
        with Path(f"{odir}/meson.build").open() as mesonbuild:
            mbld = mesonbuild.read()
            assert "objects:" in mbld
            assert f"'''{obj}'''" in mbld


def test_inclpath():
    """Add to the include directories

    CLI :: --include-paths
    """
    # TODO: populate
    pass


def test_hlink():
    """Add to the include directories

    CLI :: --help-link
    """
    # TODO: populate
    pass


def test_f2cmap(capfd, f2cmap_f90, monkeypatch):
    """Check that Fortran-to-Python KIND specs can be passed

    CLI :: --f2cmap
    """
    ipath = Path(f2cmap_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} --f2cmap mapfile'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "Reading f2cmap from 'mapfile' ..." in out
        assert "Mapping \"real(kind=real32)\" to \"float\"" in out
        assert "Mapping \"real(kind=real64)\" to \"double\"" in out
        assert "Mapping \"integer(kind=int64)\" to \"long_long\"" in out
        assert "Successfully applied user defined f2cmap changes" in out


def test_quiet(capfd, hello_world_f90, monkeypatch):
    """Reduce verbosity

    CLI :: --quiet
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} --quiet'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert len(out) == 0


def test_verbose(capfd, hello_world_f90, monkeypatch):
    """Increase verbosity

    CLI :: --verbose
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} --verbose'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        out, _ = capfd.readouterr()
        assert "analyzeline" in out


def test_version(capfd, monkeypatch):
    """Ensure version

    CLI :: -v
    """
    monkeypatch.setattr(sys, "argv", ["f2py", "-v"])
    # TODO: f2py2e should not call sys.exit() after printing the version
    with pytest.raises(SystemExit):
        f2pycli()
        out, _ = capfd.readouterr()
        import numpy as np
        assert np.__version__ == out.strip()


@pytest.mark.skip(reason="Consistently fails on CI; noisy so skip not xfail.")
def test_npdistop(hello_world_f90, monkeypatch):
    """
    CLI :: -c
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c'.split())

    with util.switchdir(ipath.parent):
        f2pycli()
        cmd_run = shlex.split(f"{sys.executable} -c \"import blah; blah.hi()\"")
        rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
        eout = ' Hello World\n'
        assert rout.stdout == eout


@pytest.mark.skipif((platform.system() != 'Linux') or sys.version_info <= (3, 12),
                    reason='Compiler and Python 3.12 or newer required')
@pytest.mark.slow
def test_no_freethreading_compatible(hello_world_f90, monkeypatch):
    """
    CLI :: --no-freethreading-compatible
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c --no-freethreading-compatible'.split())

    with util.switchdir(ipath.parent):
        compiler_check_f2pycli()
        cmd = f"{sys.executable} -c \"import blah; blah.hi();"
        if NOGIL_BUILD:
            cmd += "import sys; assert sys._is_gil_enabled() is True\""
        else:
            cmd += "\""
        cmd_run = shlex.split(cmd)
        rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
        eout = ' Hello World\n'
        assert rout.stdout == eout
        if NOGIL_BUILD:
            assert "The global interpreter lock (GIL) has been enabled to load module 'blah'" in rout.stderr
        assert rout.returncode == 0


@pytest.mark.skipif((platform.system() != 'Linux') or sys.version_info <= (3, 12),
                    reason='Compiler and Python 3.12 or newer required')
def test_freethreading_compatible(hello_world_f90, monkeypatch):
    """
    CLI :: --freethreading_compatible
    """
    ipath = Path(hello_world_f90)
    monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c --freethreading-compatible'.split())

    with util.switchdir(ipath.parent):
        compiler_check_f2pycli()
        cmd = f"{sys.executable} -c \"import blah; blah.hi();"
        if NOGIL_BUILD:
            cmd += "import sys; assert sys._is_gil_enabled() is False\""
        else:
            cmd += "\""
        cmd_run = shlex.split(cmd)
        rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
        eout = ' Hello World\n'
        assert rout.stdout == eout
        if "LSAN_OPTIONS" not in os.environ:
            assert rout.stderr == ""
        assert rout.returncode == 0


# Numpy distutils flags
# TODO: These should be tested separately

def test_npd_fcompiler():
    """
    CLI :: -c --fcompiler
    """
    # TODO: populate
    pass


def test_npd_compiler():
    """
    CLI :: -c --compiler
    """
    # TODO: populate
    pass


def test_npd_help_fcompiler():
    """
    CLI :: -c --help-fcompiler
    """
    # TODO: populate
    pass


def test_npd_f77exec():
    """
    CLI :: -c --f77exec
    """
    # TODO: populate
    pass


def test_npd_f90exec():
    """
    CLI :: -c --f90exec
    """
    # TODO: populate
    pass


def test_npd_f77flags():
    """
    CLI :: -c --f77flags
    """
    # TODO: populate
    pass


def test_npd_f90flags():
    """
    CLI :: -c --f90flags
    """
    # TODO: populate
    pass


def test_npd_opt():
    """
    CLI :: -c --opt
    """
    # TODO: populate
    pass


def test_npd_arch():
    """
    CLI :: -c --arch
    """
    # TODO: populate
    pass


def test_npd_noopt():
    """
    CLI :: -c --noopt
    """
    # TODO: populate
    pass


def test_npd_noarch():
    """
    CLI :: -c --noarch
    """
    # TODO: populate
    pass


def test_npd_debug():
    """
    CLI :: -c --debug
    """
    # TODO: populate
    pass


def test_npd_link_auto():
    """
    CLI :: -c --link-<resource>
    """
    # TODO: populate
    pass


def test_npd_lib():
    """
    CLI :: -c -L/path/to/lib/ -l<libname>
    """
    # TODO: populate
    pass


def test_npd_define():
    """
    CLI :: -D<define>
    """
    # TODO: populate
    pass


def test_npd_undefine():
    """
    CLI :: -U<name>
    """
    # TODO: populate
    pass


def test_npd_incl():
    """
    CLI :: -I/path/to/include/
    """
    # TODO: populate
    pass


def test_npd_linker():
    """
    CLI :: <filename>.o <filename>.so <filename>.a
    """
    # TODO: populate
    pass
