import os
import ssl
import sys
import tempfile
import textwrap
from pathlib import Path
from typing import Callable, List

import pytest

from tests.conftest import CertFactory, MockServer, ScriptFactory
from tests.lib import PipTestEnvironment, TestData
from tests.lib.server import (
    authorization_response,
    file_response,
    make_mock_server,
    package_page,
    server_running,
)
from tests.lib.venv import VirtualEnvironment

TEST_PYPI_INITOOLS = "https://test.pypi.org/simple/initools/"


def test_options_from_env_vars(script: PipTestEnvironment) -> None:
    """
    Test if ConfigOptionParser reads env vars (e.g. not using PyPI here)

    """
    script.environ["PIP_NO_INDEX"] = "1"
    result = script.pip("install", "-vvv", "INITools", expect_error=True)
    assert "Ignoring indexes:" in result.stdout, str(result)
    msg = "DistributionNotFound: No matching distribution found for INITools"
    # Case insensitive as the new resolver canonicalizes the project name
    assert msg.lower() in result.stdout.lower(), str(result)


def test_command_line_options_override_env_vars(
    script: PipTestEnvironment, virtualenv: VirtualEnvironment
) -> None:
    """
    Test that command line options override environmental variables.

    """
    script.environ["PIP_INDEX_URL"] = "https://example.com/simple/"
    result = script.pip("install", "-vvv", "INITools", expect_error=True)
    assert "Getting page https://example.com/simple/initools" in result.stdout
    virtualenv.clear()
    result = script.pip(
        "install",
        "-vvv",
        "--index-url",
        "https://download.zope.org/ppix",
        "INITools",
        expect_error=True,
    )
    assert "example.com" not in result.stdout
    assert "Getting page https://download.zope.org/ppix" in result.stdout


@pytest.mark.network
def test_env_vars_override_config_file(
    script: PipTestEnvironment, virtualenv: VirtualEnvironment
) -> None:
    """
    Test that environmental variables override settings in config files.
    """
    config_file = script.scratch_path / "test-pip.cfg"
    # set this to make pip load it
    script.environ["PIP_CONFIG_FILE"] = str(config_file)
    # It's important that we test this particular config value ('no-index')
    # because there is/was a bug which only shows up in cases in which
    # 'config-item' and 'config_item' hash to the same value modulo the size
    # of the config dictionary.
    config_file.write_text(
        textwrap.dedent(
            """\
        [global]
        no-index = 1
        """
        )
    )
    result = script.pip("install", "-vvv", "INITools", expect_error=True)
    msg = "DistributionNotFound: No matching distribution found for INITools"
    # Case insensitive as the new resolver canonicalizes the project name
    assert msg.lower() in result.stdout.lower(), str(result)
    script.environ["PIP_NO_INDEX"] = "0"
    virtualenv.clear()
    result = script.pip("install", "-vvv", "INITools")
    assert "Successfully installed INITools" in result.stdout


@pytest.mark.network
def test_command_line_append_flags(
    script: PipTestEnvironment, virtualenv: VirtualEnvironment, data: TestData
) -> None:
    """
    Test command line flags that append to defaults set by environmental
    variables.

    """
    script.environ["PIP_FIND_LINKS"] = TEST_PYPI_INITOOLS
    result = script.pip(
        "install",
        "-vvv",
        "INITools",
        "--trusted-host",
        "test.pypi.org",
    )
    assert (
        "Fetching project page and analyzing links: https://test.pypi.org"
        in result.stdout
    ), str(result)
    virtualenv.clear()
    result = script.pip(
        "install",
        "-vvv",
        "--find-links",
        data.find_links,
        "INITools",
        "--trusted-host",
        "test.pypi.org",
    )
    assert (
        "Fetching project page and analyzing links: https://test.pypi.org"
        in result.stdout
    )
    assert (
        f"Skipping link: not a file: {data.find_links}" in result.stdout
    ), f"stdout: {result.stdout}"


@pytest.mark.network
def test_command_line_appends_correctly(
    script: PipTestEnvironment, data: TestData
) -> None:
    """
    Test multiple appending options set by environmental variables.

    """
    script.environ["PIP_FIND_LINKS"] = f"{TEST_PYPI_INITOOLS} {data.find_links}"
    result = script.pip(
        "install",
        "-vvv",
        "INITools",
        "--trusted-host",
        "test.pypi.org",
    )

    assert (
        "Fetching project page and analyzing links: https://test.pypi.org"
        in result.stdout
    ), result.stdout
    assert (
        f"Skipping link: not a file: {data.find_links}" in result.stdout
    ), f"stdout: {result.stdout}"


def test_config_file_override_stack(
    script: PipTestEnvironment,
    virtualenv: VirtualEnvironment,
    mock_server: MockServer,
    shared_data: TestData,
) -> None:
    """
    Test config files (global, overriding a global config with a
    local, overriding all with a command line flag).
    """
    mock_server.set_responses(
        [
            package_page({}),
            package_page({}),
            package_page({"INITools-0.2.tar.gz": "/files/INITools-0.2.tar.gz"}),
            file_response(shared_data.packages.joinpath("INITools-0.2.tar.gz")),
        ]
    )
    mock_server.start()
    base_address = f"http://{mock_server.host}:{mock_server.port}"

    config_file = script.scratch_path / "test-pip.cfg"

    # set this to make pip load it
    script.environ["PIP_CONFIG_FILE"] = str(config_file)

    config_file.write_text(
        textwrap.dedent(
            """\
        [global]
        index-url = {}/simple1
        """.format(
                base_address
            )
        )
    )
    script.pip("install", "-vvv", "INITools", expect_error=True)
    virtualenv.clear()

    config_file.write_text(
        textwrap.dedent(
            """\
        [global]
        index-url = {address}/simple1
        [install]
        index-url = {address}/simple2
        """.format(
                address=base_address
            )
        )
    )
    script.pip("install", "-vvv", "INITools", expect_error=True)
    script.pip(
        "install",
        "-vvv",
        "--index-url",
        f"{base_address}/simple3",
        "INITools",
    )

    mock_server.stop()
    requests = mock_server.get_requests()
    assert len(requests) == 4
    assert requests[0]["PATH_INFO"] == "/simple1/initools/"
    assert requests[1]["PATH_INFO"] == "/simple2/initools/"
    assert requests[2]["PATH_INFO"] == "/simple3/initools/"
    assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz"


def test_options_from_venv_config(
    script: PipTestEnvironment, virtualenv: VirtualEnvironment
) -> None:
    """
    Test if ConfigOptionParser reads a virtualenv-local config file

    """
    from pip._internal.configuration import CONFIG_BASENAME

    conf = "[global]\nno-index = true"
    ini = virtualenv.location / CONFIG_BASENAME
    with open(ini, "w") as f:
        f.write(conf)
    result = script.pip("install", "-vvv", "INITools", expect_error=True)
    assert "Ignoring indexes:" in result.stdout, str(result)
    msg = "DistributionNotFound: No matching distribution found for INITools"
    # Case insensitive as the new resolver canonicalizes the project name
    assert msg.lower() in result.stdout.lower(), str(result)


def test_install_no_binary_via_config_disables_cached_wheels(
    script: PipTestEnvironment, data: TestData
) -> None:
    config_file = tempfile.NamedTemporaryFile(mode="wt", delete=False)
    try:
        script.environ["PIP_CONFIG_FILE"] = config_file.name
        config_file.write(
            textwrap.dedent(
                """\
            [global]
            no-binary = :all:
            """
            )
        )
        config_file.close()
        res = script.pip(
            "install", "--no-index", "-f", data.find_links, "upper", expect_stderr=True
        )
    finally:
        os.unlink(config_file.name)
    assert "Successfully installed upper-2.0" in str(res), str(res)
    # upper is built and not obtained from cache
    assert "Building wheel for upper" in str(res), str(res)


@pytest.mark.skipif(
    sys.platform == "linux" and sys.version_info < (3, 8),
    reason="Custom SSL certification not running well in CI",
)
def test_prompt_for_authentication(
    script: PipTestEnvironment, data: TestData, cert_factory: CertFactory
) -> None:
    """Test behaviour while installing from a index url
    requiring authentication
    """
    cert_path = cert_factory()
    ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
    ctx.load_cert_chain(cert_path, cert_path)
    ctx.load_verify_locations(cafile=cert_path)
    ctx.verify_mode = ssl.CERT_REQUIRED

    server = make_mock_server(ssl_context=ctx)
    server.mock.side_effect = [
        package_page(
            {
                "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
            }
        ),
        authorization_response(data.packages / "simple-3.0.tar.gz"),
    ]

    url = f"https://{server.host}:{server.port}/simple"

    with server_running(server):
        result = script.pip(
            "install",
            "--index-url",
            url,
            "--cert",
            cert_path,
            "--client-cert",
            cert_path,
            "simple",
            expect_error=True,
        )

    assert f"User for {server.host}:{server.port}" in result.stdout, str(result)


@pytest.mark.skipif(
    sys.platform == "linux" and sys.version_info < (3, 8),
    reason="Custom SSL certification not running well in CI",
)
def test_do_not_prompt_for_authentication(
    script: PipTestEnvironment, data: TestData, cert_factory: CertFactory
) -> None:
    """Test behaviour if --no-input option is given while installing
    from a index url requiring authentication
    """
    cert_path = cert_factory()
    ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
    ctx.load_cert_chain(cert_path, cert_path)
    ctx.load_verify_locations(cafile=cert_path)
    ctx.verify_mode = ssl.CERT_REQUIRED

    server = make_mock_server(ssl_context=ctx)

    server.mock.side_effect = [
        package_page(
            {
                "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
            }
        ),
        authorization_response(data.packages / "simple-3.0.tar.gz"),
    ]

    url = f"https://{server.host}:{server.port}/simple"

    with server_running(server):
        result = script.pip(
            "install",
            "--index-url",
            url,
            "--cert",
            cert_path,
            "--client-cert",
            cert_path,
            "--no-input",
            "simple",
            expect_error=True,
        )

    assert "ERROR: HTTP error 401" in result.stderr


@pytest.fixture(params=(True, False), ids=("interactive", "noninteractive"))
def interactive(request: pytest.FixtureRequest) -> bool:
    return request.param


@pytest.fixture(params=(True, False), ids=("auth_needed", "auth_not_needed"))
def auth_needed(request: pytest.FixtureRequest) -> bool:
    return request.param


@pytest.fixture(params=(None, "disabled", "import", "subprocess", "auto"))
def keyring_provider(request: pytest.FixtureRequest) -> str:
    return request.param


@pytest.fixture(params=("disabled", "import", "subprocess"))
def keyring_provider_implementation(request: pytest.FixtureRequest) -> str:
    return request.param


@pytest.fixture()
def flags(
    request: pytest.FixtureRequest,
    interactive: bool,
    auth_needed: bool,
    keyring_provider: str,
    keyring_provider_implementation: str,
) -> List[str]:
    if (
        keyring_provider not in [None, "auto"]
        and keyring_provider_implementation != keyring_provider
    ):
        pytest.skip()

    flags = []
    if keyring_provider is not None:
        flags.append("--keyring-provider")
        flags.append(keyring_provider)
    if not interactive:
        flags.append("--no-input")
    if auth_needed:
        if keyring_provider_implementation == "disabled" or (
            not interactive and keyring_provider in [None, "auto"]
        ):
            request.applymarker(pytest.mark.xfail())
    return flags


@pytest.mark.skipif(
    sys.platform == "linux" and sys.version_info < (3, 8),
    reason="Custom SSL certification not running well in CI",
)
def test_prompt_for_keyring_if_needed(
    data: TestData,
    cert_factory: CertFactory,
    auth_needed: bool,
    flags: List[str],
    keyring_provider: str,
    keyring_provider_implementation: str,
    tmpdir: Path,
    script_factory: ScriptFactory,
    virtualenv_factory: Callable[[Path], VirtualEnvironment],
) -> None:
    """Test behaviour while installing from an index url
    requiring authentication and keyring is possible.
    """
    environ = os.environ.copy()
    workspace = tmpdir.joinpath("workspace")

    if keyring_provider_implementation == "subprocess":
        keyring_virtualenv = virtualenv_factory(workspace.joinpath("keyring"))
        keyring_script = script_factory(
            workspace.joinpath("keyring"), keyring_virtualenv
        )
        keyring_script.pip(
            "install",
            "keyring",
        )

        environ["PATH"] = str(keyring_script.bin_path) + os.pathsep + environ["PATH"]

    virtualenv = virtualenv_factory(workspace.joinpath("venv"))
    script = script_factory(workspace.joinpath("venv"), virtualenv, environ=environ)

    if (
        keyring_provider not in [None, "auto"]
        or keyring_provider_implementation != "subprocess"
    ):
        script.pip(
            "install",
            "keyring",
        )

    if keyring_provider_implementation != "subprocess":
        keyring_script = script

    cert_path = cert_factory()
    ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
    ctx.load_cert_chain(cert_path, cert_path)
    ctx.load_verify_locations(cafile=cert_path)
    ctx.verify_mode = ssl.CERT_REQUIRED

    response = authorization_response if auth_needed else file_response

    server = make_mock_server(ssl_context=ctx)
    server.mock.side_effect = [
        package_page(
            {
                "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
            }
        ),
        response(data.packages / "simple-3.0.tar.gz"),
        response(data.packages / "simple-3.0.tar.gz"),
    ]

    url = f"https://USERNAME@{server.host}:{server.port}/simple"

    keyring_content = textwrap.dedent(
        """\
        import os
        import sys
        import keyring
        from keyring.backend import KeyringBackend
        from keyring.credentials import SimpleCredential

        class TestBackend(KeyringBackend):
            priority = 1

            def get_credential(self, url, username):
                sys.stderr.write("get_credential was called" + os.linesep)
                return SimpleCredential(username="USERNAME", password="PASSWORD")

            def get_password(self, url, username):
                sys.stderr.write("get_password was called" + os.linesep)
                return "PASSWORD"

            def set_password(self, url, username):
                pass
    """
    )
    keyring_path = keyring_script.site_packages_path / "keyring_test.py"
    keyring_path.write_text(keyring_content)

    keyring_content = (
        "import keyring_test;"
        " import keyring;"
        " keyring.set_keyring(keyring_test.TestBackend())" + os.linesep
    )
    keyring_path = keyring_path.with_suffix(".pth")
    keyring_path.write_text(keyring_content)

    with server_running(server):
        result = script.pip(
            "install",
            "--index-url",
            url,
            "--cert",
            cert_path,
            "--client-cert",
            cert_path,
            *flags,
            "simple",
        )

    function_name = (
        "get_credential"
        if keyring_provider_implementation == "import"
        else "get_password"
    )
    if auth_needed:
        assert function_name + " was called" in result.stderr
    else:
        assert function_name + " was called" not in result.stderr
