import subprocess
import sys
import textwrap

import pytest
from typer.testing import CliRunner

from pip_deepfreeze.__main__ import app
from pip_deepfreeze.pip import (
    Installer,
    InstallerFlavor,
    PipInstaller,
    UvpipInstaller,
    pip_freeze,
    pip_list,
)
from pip_deepfreeze.sync import sync


def test_sync(virtualenv_python, testpkgs, tmp_path):
    (tmp_path / "constraints.txt").write_text(
        textwrap.dedent(
            f"""\
            --pre
            --no-index
            --find-links {testpkgs}
            pkga<1
            """
        )
    )
    (tmp_path / "setup.py").write_text(
        textwrap.dedent(
            """\
            from setuptools import setup

            setup(name="theproject", install_requires=["pkgb"])
            """
        )
    )
    (tmp_path / "setup.cfg").write_text("[metadata]\nname = theproject\n")  # for perf
    subprocess.check_call(
        [sys.executable, "-m", "pip_deepfreeze", "--python", virtualenv_python, "sync"],
        cwd=tmp_path,
    )
    assert (tmp_path / "requirements.txt").read_text() == textwrap.dedent(
        f"""\
        # frozen requirements generated by pip-deepfreeze
        --pre
        --no-index
        --find-links {testpkgs}
        pkga==0.0.0
        pkgb==0.0.0
        """
    )


def test_sync_normalization(virtualenv_python, testpkgs, tmp_path):
    (tmp_path / "constraints.txt").write_text(
        textwrap.dedent(
            f"""\
            --no-index
            --find-links {testpkgs}
            """
        )
    )
    (tmp_path / "pyproject.toml").write_text(
        textwrap.dedent(
            """\
            [project]
            name = "theproject"
            version = "1.0"
            dependencies = ["pkg-F.G_H"]
            """
        )
    )
    subprocess.check_call(
        [sys.executable, "-m", "pip_deepfreeze", "--python", virtualenv_python, "sync"],
        cwd=tmp_path,
    )
    assert (tmp_path / "requirements.txt").read_text() == textwrap.dedent(
        f"""\
        # frozen requirements generated by pip-deepfreeze
        --no-index
        --find-links {testpkgs}
        pkg-f-g-h==0.0.0
        """
    )


def test_sync_no_in_req(virtualenv_python, tmp_path):
    (tmp_path / "setup.py").write_text(
        textwrap.dedent(
            """\
            from setuptools import setup

            setup(name="theproject")
            """
        )
    )
    (tmp_path / "setup.cfg").write_text("[metadata]\nname = theproject\n")  # for perf
    subprocess.check_call(
        [sys.executable, "-m", "pip_deepfreeze", "--python", virtualenv_python, "sync"],
        cwd=tmp_path,
    )
    assert (tmp_path / "requirements.txt").read_text() == textwrap.dedent(
        """\
        # frozen requirements generated by pip-deepfreeze
        """
    )


def test_python_not_found():
    runner = CliRunner()
    result = runner.invoke(app, ["--python", "this-is-not-a-python", "sync"])
    assert result.exit_code != 0
    assert "Python interpreter not found (this-is-not-a-python)" in result.output


@pytest.fixture
def editable_foobar_path(tmp_path):
    setup_py = tmp_path / "setup.py"
    setup_py.write_text(
        textwrap.dedent(
            """
            from setuptools import setup

            setup(name="foobar", version="0.0.1")
            """
        )
    )
    (tmp_path / "setup.cfg").write_text("[metadata]\nname = foobar\n")  # for perf
    return tmp_path


@pytest.mark.parametrize(
    "installer_flavor", [InstallerFlavor.pip, InstallerFlavor.uvpip]
)
def test_editable_default_install(
    installer_flavor: InstallerFlavor, virtualenv_python, editable_foobar_path
):
    subprocess.check_call(
        [
            sys.executable,
            "-m",
            "pip_deepfreeze",
            "--python",
            virtualenv_python,
            "sync",
            "--installer",
            installer_flavor.value,
        ],
        cwd=editable_foobar_path,
    )
    # installed editable by default
    assert "-e " in "\n".join(
        pip_freeze(
            Installer.create(installer_flavor, virtualenv_python), virtualenv_python
        )
    )


@pytest.mark.parametrize("installer", [PipInstaller(), UvpipInstaller()])
def test_sync_project_root(
    installer: Installer, virtualenv_python, editable_foobar_path
):
    sync(
        installer,
        virtualenv_python,
        upgrade_all=False,
        to_upgrade=[],
        extras=[],
        uninstall_unneeded=False,
        project_root=editable_foobar_path,
    )
    assert (editable_foobar_path / "requirements.txt").exists()


def test_sync_editable_dep(virtualenv_python, tmp_path):
    pyproject_toml = tmp_path / "pyproject.toml"
    pyproject_toml.write_text(
        textwrap.dedent(
            """
            [project]
            name = "foobar"
            version = "0.1"
            dependencies = ["pip-test-package"]
            """
        )
    )
    constraints = tmp_path / "constraints.txt"
    constraints.write_text(
        "-e git+https://github.com/PyPA/pip-test-package#egg=pip-test-package\n"
    )
    subprocess.check_call(
        [sys.executable, "-m", "pip_deepfreeze", "--python", virtualenv_python, "sync"],
        cwd=tmp_path,
    )
    requirements_path = tmp_path / "requirements.txt"
    assert requirements_path.is_file()
    assert "-e " in requirements_path.read_text(), requirements_path.read_text()
    # test that a second run keeps editableness
    subprocess.check_call(
        [sys.executable, "-m", "pip_deepfreeze", "--python", virtualenv_python, "sync"],
        cwd=tmp_path,
    )
    assert "-e " in requirements_path.read_text(), requirements_path.read_text()


@pytest.mark.parametrize("installer", [PipInstaller(), UvpipInstaller()])
def test_sync_uninstall(installer: Installer, virtualenv_python, tmp_path, testpkgs):
    setup_py = tmp_path / "setup.py"
    setup_py.write_text(
        textwrap.dedent(
            """
            from setuptools import setup

            setup(name="foobar", version="0.0.1", install_requires=["pkga"])
            """
        )
    )
    (tmp_path / "setup.cfg").write_text("[metadata]\nname = foobar\n")  # for perf
    in_reqs = tmp_path / "constraints.txt"
    in_reqs.write_text(f"--no-index\n-f {testpkgs}")
    sync(
        installer,
        virtualenv_python,
        upgrade_all=False,
        to_upgrade=[],
        extras=[],
        uninstall_unneeded=False,
        project_root=tmp_path,
    )
    assert "pkga==" in "\n".join(pip_freeze(installer, virtualenv_python))
    # remove dependency on pkga
    setup_py.write_text(
        textwrap.dedent(
            """
            from setuptools import setup

            setup(name="foobar", version="0.0.1", install_requires=[])
            """
        )
    )
    # sync with uninstall=False, pkga remains
    sync(
        installer,
        virtualenv_python,
        upgrade_all=False,
        to_upgrade=[],
        extras=[],
        uninstall_unneeded=False,
        project_root=tmp_path,
    )
    assert "pkga==" in "\n".join(pip_freeze(installer, virtualenv_python))
    # sync with uninstall=True, pkga removed
    sync(
        installer,
        virtualenv_python,
        upgrade_all=False,
        to_upgrade=[],
        extras=[],
        uninstall_unneeded=True,
        project_root=tmp_path,
    )
    assert "pkga==" not in "\n".join(pip_freeze(installer, virtualenv_python))


@pytest.mark.xfail(reason="https://github.com/sbidoul/pip-deepfreeze/issues/24")
@pytest.mark.parametrize("installer", [PipInstaller(), UvpipInstaller()])
def test_sync_update_new_dep(
    installer: Installer, virtualenv_python, testpkgs, tmp_path
):
    """Test that a preinstalled dependency is updated when project is not installed
    before sync.

    This case may also happen when adding intermediate dependencies
    """
    subprocess.check_call(
        [
            *installer.install_cmd(virtualenv_python),
            "--no-index",
            "-f",
            testpkgs,
            "pkgc==0.0.1",
        ]
    )
    assert "pkgc==0.0.1" in "\n".join(pip_freeze(virtualenv_python))
    (tmp_path / "setup.py").write_text(
        textwrap.dedent(
            """\
            from setuptools import setup
            setup(name="theproject", install_requires=["pkgc"])
            """
        )
    )
    (tmp_path / "setup.cfg").write_text("[metadata]\nname = theproject\n")  # for perf
    (tmp_path / "constraints.txt").write_text(
        textwrap.dedent(
            f"""\
            --no-index
            -f {testpkgs}
            """
        )
    )
    sync(
        installer,
        virtualenv_python,
        upgrade_all=False,
        to_upgrade=["pkgc"],
        extras=[],
        uninstall_unneeded=False,
        project_root=tmp_path,
    )
    assert "pkgc==0.0.3" in "\n".join(pip_freeze(virtualenv_python))


@pytest.mark.xfail(reason="https://github.com/sbidoul/pip-deepfreeze/issues/24")
@pytest.mark.parametrize("installer", [PipInstaller(), UvpipInstaller()])
def test_sync_update_all_new_dep(
    installer: Installer, virtualenv_python, testpkgs, tmp_path
):
    """Test that a preinstalled dependency is updated when project is not installed
    before sync.

    This case may also happen when adding intermediate dependencies
    """
    subprocess.check_call(
        [
            *installer.install_cmd(virtualenv_python),
            "--no-index",
            "-f",
            testpkgs,
            "pkgc==0.0.1",
        ]
    )
    assert "pkgc==0.0.1" in "\n".join(pip_freeze(virtualenv_python))
    (tmp_path / "setup.py").write_text(
        textwrap.dedent(
            """\
            from setuptools import setup
            setup(name="theproject", install_requires=["pkgc"])
            """
        )
    )
    (tmp_path / "setup.cfg").write_text("[metadata]\nname = theproject\n")  # for perf
    (tmp_path / "constraints.txt").write_text(
        textwrap.dedent(
            f"""\
            --no-index
            -f {testpkgs}
            """
        )
    )
    sync(
        installer,
        virtualenv_python,
        upgrade_all=True,
        to_upgrade=[],
        extras=[],
        uninstall_unneeded=False,
        project_root=tmp_path,
    )
    assert "pkgc==0.0.3" in "\n".join(pip_freeze(virtualenv_python))


@pytest.mark.parametrize("installer", [PipInstaller(), UvpipInstaller()])
def test_sync_extras(installer: Installer, virtualenv_python, testpkgs, tmp_path):
    (tmp_path / "setup.py").write_text(
        textwrap.dedent(
            """\
            from setuptools import setup
            setup(
                name="theproject",
                install_requires=["pkgb"],
                extras_require={
                    "c": ["pkgc"],
                },
            )
            """
        )
    )
    (tmp_path / "setup.cfg").write_text("[metadata]\nname = theproject\n")  # for perf
    (tmp_path / "constraints.txt").write_text(
        textwrap.dedent(
            f"""\
            --no-index
            -f {testpkgs}
            """
        )
    )
    sync(
        installer,
        virtualenv_python,
        upgrade_all=False,
        to_upgrade=[],
        extras=["c"],
        uninstall_unneeded=False,
        project_root=tmp_path,
    )
    assert {"pkga", "pkgb", "pkgc"}.issubset(pip_list(virtualenv_python))
    requirements_txt = (tmp_path / "requirements.txt").read_text()
    assert "pkga==0.0.0\npkgb==0.0.0\n" in requirements_txt
    assert "pkgc" not in requirements_txt
    requirements_c_txt = (tmp_path / "requirements-c.txt").read_text()
    assert "pkga" not in requirements_c_txt
    assert "pkgb" not in requirements_c_txt
    assert "pkgc==0.0.3\n" in requirements_c_txt
    # now sync again with a different frozen extra dependency
    (tmp_path / "requirements-c.txt").write_text("pkgc==0.0.2")
    sync(
        installer,
        virtualenv_python,
        upgrade_all=False,
        to_upgrade=[],
        extras=["c"],
        uninstall_unneeded=False,
        project_root=tmp_path,
    )
    assert {"pkga", "pkgb", "pkgc"}.issubset(pip_list(virtualenv_python))
    requirements_txt = (tmp_path / "requirements.txt").read_text()
    assert "pkga==0.0.0\npkgb==0.0.0\n" in requirements_txt
    assert "pkgc" not in requirements_txt
    requirements_c_txt = (tmp_path / "requirements-c.txt").read_text()
    assert "pkga" not in requirements_c_txt
    assert "pkgb" not in requirements_c_txt
    assert "pkgc==0.0.2\n" in requirements_c_txt


def test_pre_post_sync_command(virtualenv_python, testpkgs, tmp_path):
    (tmp_path / "constraints.txt").write_text(
        textwrap.dedent(
            f"""\
            --no-index
            --find-links {testpkgs}
            """
        )
    )
    (tmp_path / "pyproject.toml").write_text(
        textwrap.dedent(
            """\
            [project]
            name = "theproject"
            version = "1.0"
            """
        )
    )
    res = subprocess.run(
        [
            sys.executable,
            "-m",
            "pip_deepfreeze",
            "--python",
            virtualenv_python,
            "sync",
            "--pre-sync-command",
            "echo pre-sync-cmd-1",
            "--post-sync-command",
            "echo post-sync-cmd-1",
            "--post-sync-command",
            "echo post-sync-cmd-2",
        ],
        cwd=tmp_path,
        text=True,
        check=True,
        capture_output=True,
    )
    assert res.stdout.startswith("pre-sync-cmd-1\n")
    assert res.stdout.endswith("post-sync-cmd-1\npost-sync-cmd-2\n")
    res = subprocess.run(
        [
            sys.executable,
            "-m",
            "pip_deepfreeze",
            "--python",
            virtualenv_python,
            "sync",
            "--post-sync-command",
            "notacommand",
        ],
        cwd=tmp_path,
        text=True,
        check=False,
        capture_output=True,
    )
    assert res.returncode != 0
    assert "Post-sync command notacommand failed" in res.stderr
