"""Tests for distutils.dist."""
import os
import io
import sys
import warnings
import textwrap
import functools
import unittest.mock as mock

import pytest
import jaraco.path

from distutils.dist import Distribution, fix_help_options
from distutils.cmd import Command

from test.support import captured_stdout, captured_stderr
from .py38compat import TESTFN
from distutils.tests import support
from distutils import log


pydistutils_cfg = '.' * (os.name == 'posix') + 'pydistutils.cfg'


class test_dist(Command):
    """Sample distutils extension command."""

    user_options = [
        ("sample-option=", "S", "help text"),
    ]

    def initialize_options(self):
        self.sample_option = None


class TestDistribution(Distribution):
    """Distribution subclasses that avoids the default search for
    configuration files.

    The ._config_files attribute must be set before
    .parse_config_files() is called.
    """

    def find_config_files(self):
        return self._config_files


@pytest.fixture
def clear_argv():
    del sys.argv[1:]


@support.combine_markers
@pytest.mark.usefixtures('save_env')
@pytest.mark.usefixtures('save_argv')
class TestDistributionBehavior(
    support.LoggingSilencer,
    support.TempdirManager,
):
    def create_distribution(self, configfiles=()):
        d = TestDistribution()
        d._config_files = configfiles
        d.parse_config_files()
        d.parse_command_line()
        return d

    def test_command_packages_unspecified(self, clear_argv):
        sys.argv.append("build")
        d = self.create_distribution()
        assert d.get_command_packages() == ["distutils.command"]

    def test_command_packages_cmdline(self, clear_argv):
        from distutils.tests.test_dist import test_dist

        sys.argv.extend(
            [
                "--command-packages",
                "foo.bar,distutils.tests",
                "test_dist",
                "-Ssometext",
            ]
        )
        d = self.create_distribution()
        # let's actually try to load our test command:
        assert d.get_command_packages() == [
            "distutils.command",
            "foo.bar",
            "distutils.tests",
        ]
        cmd = d.get_command_obj("test_dist")
        assert isinstance(cmd, test_dist)
        assert cmd.sample_option == "sometext"

    @pytest.mark.skipif(
        'distutils' not in Distribution.parse_config_files.__module__,
        reason='Cannot test when virtualenv has monkey-patched Distribution',
    )
    def test_venv_install_options(self, request):
        sys.argv.append("install")
        request.addfinalizer(functools.partial(os.unlink, TESTFN))

        fakepath = '/somedir'

        jaraco.path.build(
            {
                TESTFN: f"""
                    [install]
                    install-base = {fakepath}
                    install-platbase = {fakepath}
                    install-lib = {fakepath}
                    install-platlib = {fakepath}
                    install-purelib = {fakepath}
                    install-headers = {fakepath}
                    install-scripts = {fakepath}
                    install-data = {fakepath}
                    prefix = {fakepath}
                    exec-prefix = {fakepath}
                    home = {fakepath}
                    user = {fakepath}
                    root = {fakepath}
                    """,
            }
        )

        # Base case: Not in a Virtual Environment
        with mock.patch.multiple(sys, prefix='/a', base_prefix='/a'):
            d = self.create_distribution([TESTFN])

        option_tuple = (TESTFN, fakepath)

        result_dict = {
            'install_base': option_tuple,
            'install_platbase': option_tuple,
            'install_lib': option_tuple,
            'install_platlib': option_tuple,
            'install_purelib': option_tuple,
            'install_headers': option_tuple,
            'install_scripts': option_tuple,
            'install_data': option_tuple,
            'prefix': option_tuple,
            'exec_prefix': option_tuple,
            'home': option_tuple,
            'user': option_tuple,
            'root': option_tuple,
        }

        assert sorted(d.command_options.get('install').keys()) == sorted(
            result_dict.keys()
        )

        for (key, value) in d.command_options.get('install').items():
            assert value == result_dict[key]

        # Test case: In a Virtual Environment
        with mock.patch.multiple(sys, prefix='/a', base_prefix='/b'):
            d = self.create_distribution([TESTFN])

        for key in result_dict.keys():
            assert key not in d.command_options.get('install', {})

    def test_command_packages_configfile(self, request, clear_argv):
        sys.argv.append("build")
        request.addfinalizer(functools.partial(os.unlink, TESTFN))
        jaraco.path.build(
            {
                TESTFN: """
                    [global]
                    command_packages = foo.bar, splat
                    """,
            }
        )

        d = self.create_distribution([TESTFN])
        assert d.get_command_packages() == ["distutils.command", "foo.bar", "splat"]

        # ensure command line overrides config:
        sys.argv[1:] = ["--command-packages", "spork", "build"]
        d = self.create_distribution([TESTFN])
        assert d.get_command_packages() == ["distutils.command", "spork"]

        # Setting --command-packages to '' should cause the default to
        # be used even if a config file specified something else:
        sys.argv[1:] = ["--command-packages", "", "build"]
        d = self.create_distribution([TESTFN])
        assert d.get_command_packages() == ["distutils.command"]

    def test_empty_options(self, request):
        # an empty options dictionary should not stay in the
        # list of attributes

        # catching warnings
        warns = []

        def _warn(msg):
            warns.append(msg)

        request.addfinalizer(
            functools.partial(setattr, warnings, 'warn', warnings.warn)
        )
        warnings.warn = _warn
        dist = Distribution(
            attrs={
                'author': 'xxx',
                'name': 'xxx',
                'version': 'xxx',
                'url': 'xxxx',
                'options': {},
            }
        )

        assert len(warns) == 0
        assert 'options' not in dir(dist)

    def test_finalize_options(self):
        attrs = {'keywords': 'one,two', 'platforms': 'one,two'}

        dist = Distribution(attrs=attrs)
        dist.finalize_options()

        # finalize_option splits platforms and keywords
        assert dist.metadata.platforms == ['one', 'two']
        assert dist.metadata.keywords == ['one', 'two']

        attrs = {'keywords': 'foo bar', 'platforms': 'foo bar'}
        dist = Distribution(attrs=attrs)
        dist.finalize_options()
        assert dist.metadata.platforms == ['foo bar']
        assert dist.metadata.keywords == ['foo bar']

    def test_get_command_packages(self):
        dist = Distribution()
        assert dist.command_packages is None
        cmds = dist.get_command_packages()
        assert cmds == ['distutils.command']
        assert dist.command_packages == ['distutils.command']

        dist.command_packages = 'one,two'
        cmds = dist.get_command_packages()
        assert cmds == ['distutils.command', 'one', 'two']

    def test_announce(self):
        # make sure the level is known
        dist = Distribution()
        args = ('ok',)
        kwargs = {'level': 'ok2'}
        with pytest.raises(ValueError):
            dist.announce(args, kwargs)

    def test_find_config_files_disable(self, temp_home):
        # Ticket #1180: Allow user to disable their home config file.
        jaraco.path.build({pydistutils_cfg: '[distutils]\n'}, temp_home)

        d = Distribution()
        all_files = d.find_config_files()

        d = Distribution(attrs={'script_args': ['--no-user-cfg']})
        files = d.find_config_files()

        # make sure --no-user-cfg disables the user cfg file
        assert len(all_files) - 1 == len(files)


@pytest.mark.usefixtures('save_env')
@pytest.mark.usefixtures('save_argv')
class TestMetadata(support.TempdirManager):
    def format_metadata(self, dist):
        sio = io.StringIO()
        dist.metadata.write_pkg_file(sio)
        return sio.getvalue()

    def test_simple_metadata(self):
        attrs = {"name": "package", "version": "1.0"}
        dist = Distribution(attrs)
        meta = self.format_metadata(dist)
        assert "Metadata-Version: 1.0" in meta
        assert "provides:" not in meta.lower()
        assert "requires:" not in meta.lower()
        assert "obsoletes:" not in meta.lower()

    def test_provides(self):
        attrs = {
            "name": "package",
            "version": "1.0",
            "provides": ["package", "package.sub"],
        }
        dist = Distribution(attrs)
        assert dist.metadata.get_provides() == ["package", "package.sub"]
        assert dist.get_provides() == ["package", "package.sub"]
        meta = self.format_metadata(dist)
        assert "Metadata-Version: 1.1" in meta
        assert "requires:" not in meta.lower()
        assert "obsoletes:" not in meta.lower()

    def test_provides_illegal(self):
        with pytest.raises(ValueError):
            Distribution(
                {"name": "package", "version": "1.0", "provides": ["my.pkg (splat)"]},
            )

    def test_requires(self):
        attrs = {
            "name": "package",
            "version": "1.0",
            "requires": ["other", "another (==1.0)"],
        }
        dist = Distribution(attrs)
        assert dist.metadata.get_requires() == ["other", "another (==1.0)"]
        assert dist.get_requires() == ["other", "another (==1.0)"]
        meta = self.format_metadata(dist)
        assert "Metadata-Version: 1.1" in meta
        assert "provides:" not in meta.lower()
        assert "Requires: other" in meta
        assert "Requires: another (==1.0)" in meta
        assert "obsoletes:" not in meta.lower()

    def test_requires_illegal(self):
        with pytest.raises(ValueError):
            Distribution(
                {"name": "package", "version": "1.0", "requires": ["my.pkg (splat)"]},
            )

    def test_requires_to_list(self):
        attrs = {"name": "package", "requires": iter(["other"])}
        dist = Distribution(attrs)
        assert isinstance(dist.metadata.requires, list)

    def test_obsoletes(self):
        attrs = {
            "name": "package",
            "version": "1.0",
            "obsoletes": ["other", "another (<1.0)"],
        }
        dist = Distribution(attrs)
        assert dist.metadata.get_obsoletes() == ["other", "another (<1.0)"]
        assert dist.get_obsoletes() == ["other", "another (<1.0)"]
        meta = self.format_metadata(dist)
        assert "Metadata-Version: 1.1" in meta
        assert "provides:" not in meta.lower()
        assert "requires:" not in meta.lower()
        assert "Obsoletes: other" in meta
        assert "Obsoletes: another (<1.0)" in meta

    def test_obsoletes_illegal(self):
        with pytest.raises(ValueError):
            Distribution(
                {"name": "package", "version": "1.0", "obsoletes": ["my.pkg (splat)"]},
            )

    def test_obsoletes_to_list(self):
        attrs = {"name": "package", "obsoletes": iter(["other"])}
        dist = Distribution(attrs)
        assert isinstance(dist.metadata.obsoletes, list)

    def test_classifier(self):
        attrs = {
            'name': 'Boa',
            'version': '3.0',
            'classifiers': ['Programming Language :: Python :: 3'],
        }
        dist = Distribution(attrs)
        assert dist.get_classifiers() == ['Programming Language :: Python :: 3']
        meta = self.format_metadata(dist)
        assert 'Metadata-Version: 1.1' in meta

    def test_classifier_invalid_type(self):
        attrs = {
            'name': 'Boa',
            'version': '3.0',
            'classifiers': ('Programming Language :: Python :: 3',),
        }
        with captured_stderr() as error:
            d = Distribution(attrs)
        # should have warning about passing a non-list
        assert 'should be a list' in error.getvalue()
        # should be converted to a list
        assert isinstance(d.metadata.classifiers, list)
        assert d.metadata.classifiers == list(attrs['classifiers'])

    def test_keywords(self):
        attrs = {
            'name': 'Monty',
            'version': '1.0',
            'keywords': ['spam', 'eggs', 'life of brian'],
        }
        dist = Distribution(attrs)
        assert dist.get_keywords() == ['spam', 'eggs', 'life of brian']

    def test_keywords_invalid_type(self):
        attrs = {
            'name': 'Monty',
            'version': '1.0',
            'keywords': ('spam', 'eggs', 'life of brian'),
        }
        with captured_stderr() as error:
            d = Distribution(attrs)
        # should have warning about passing a non-list
        assert 'should be a list' in error.getvalue()
        # should be converted to a list
        assert isinstance(d.metadata.keywords, list)
        assert d.metadata.keywords == list(attrs['keywords'])

    def test_platforms(self):
        attrs = {
            'name': 'Monty',
            'version': '1.0',
            'platforms': ['GNU/Linux', 'Some Evil Platform'],
        }
        dist = Distribution(attrs)
        assert dist.get_platforms() == ['GNU/Linux', 'Some Evil Platform']

    def test_platforms_invalid_types(self):
        attrs = {
            'name': 'Monty',
            'version': '1.0',
            'platforms': ('GNU/Linux', 'Some Evil Platform'),
        }
        with captured_stderr() as error:
            d = Distribution(attrs)
        # should have warning about passing a non-list
        assert 'should be a list' in error.getvalue()
        # should be converted to a list
        assert isinstance(d.metadata.platforms, list)
        assert d.metadata.platforms == list(attrs['platforms'])

    def test_download_url(self):
        attrs = {
            'name': 'Boa',
            'version': '3.0',
            'download_url': 'http://example.org/boa',
        }
        dist = Distribution(attrs)
        meta = self.format_metadata(dist)
        assert 'Metadata-Version: 1.1' in meta

    def test_long_description(self):
        long_desc = textwrap.dedent(
            """\
        example::
              We start here
            and continue here
          and end here."""
        )
        attrs = {"name": "package", "version": "1.0", "long_description": long_desc}

        dist = Distribution(attrs)
        meta = self.format_metadata(dist)
        meta = meta.replace('\n' + 8 * ' ', '\n')
        assert long_desc in meta

    def test_custom_pydistutils(self, temp_home):
        """
        pydistutils.cfg is found
        """
        jaraco.path.build({pydistutils_cfg: ''}, temp_home)
        config_path = temp_home / pydistutils_cfg

        assert str(config_path) in Distribution().find_config_files()

    def test_extra_pydistutils(self, monkeypatch, tmp_path):
        jaraco.path.build({'overrides.cfg': ''}, tmp_path)
        filename = tmp_path / 'overrides.cfg'
        monkeypatch.setenv('DIST_EXTRA_CONFIG', str(filename))
        assert str(filename) in Distribution().find_config_files()

    def test_fix_help_options(self):
        help_tuples = [('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
        fancy_options = fix_help_options(help_tuples)
        assert fancy_options[0] == ('a', 'b', 'c')
        assert fancy_options[1] == (1, 2, 3)

    def test_show_help(self, request):
        # smoke test, just makes sure some help is displayed
        reset_log = functools.partial(log.set_threshold, log._global_log.threshold)
        request.addfinalizer(reset_log)
        dist = Distribution()
        sys.argv = []
        dist.help = 1
        dist.script_name = 'setup.py'
        with captured_stdout() as s:
            dist.parse_command_line()

        output = [line for line in s.getvalue().split('\n') if line.strip() != '']
        assert output

    def test_read_metadata(self):
        attrs = {
            "name": "package",
            "version": "1.0",
            "long_description": "desc",
            "description": "xxx",
            "download_url": "http://example.com",
            "keywords": ['one', 'two'],
            "requires": ['foo'],
        }

        dist = Distribution(attrs)
        metadata = dist.metadata

        # write it then reloads it
        PKG_INFO = io.StringIO()
        metadata.write_pkg_file(PKG_INFO)
        PKG_INFO.seek(0)
        metadata.read_pkg_file(PKG_INFO)

        assert metadata.name == "package"
        assert metadata.version == "1.0"
        assert metadata.description == "xxx"
        assert metadata.download_url == 'http://example.com'
        assert metadata.keywords == ['one', 'two']
        assert metadata.platforms is None
        assert metadata.obsoletes is None
        assert metadata.requires == ['foo']
