# -*- coding: utf-8 -*-
import unittest
import json
import sys
import textwrap
from os.path import join
from subprocess import Popen
from subprocess import PIPE
from distutils.errors import DistutilsSetupError
from distutils import dist as distutils_dist
from setuptools.dist import Distribution

import pkg_resources

from calmjs.module import ModuleRegistry
from calmjs import dist as calmjs_dist
from calmjs.cli import locale
from calmjs.utils import pretty_logging
from calmjs.testing.mocks import Mock_egg_info
from calmjs.testing.mocks import MockProvider
from calmjs.testing.mocks import StringIO
from calmjs.testing.utils import make_dummy_dist
from calmjs.testing.utils import mkdtemp
from calmjs.testing.utils import stub_stdouts


class DistTestCase(unittest.TestCase):
    """
    calmjs.dist module test case.
    """

    def setUp(self):
        self.dist = Distribution()
        self.optname = 'default_json'
        self.pkgname = calmjs_dist.DEFAULT_JSON

    def test_is_json_compat_bad_type(self):
        with self.assertRaises(ValueError) as e:
            calmjs_dist.is_json_compat(NotImplemented)

        msg = str(e.exception)
        self.assertIn('must be a JSON serializable object', msg)
        self.assertIn('NotImplemented', msg)

    def test_is_json_compat_bad_type_in_dict(self):
        with self.assertRaises(ValueError) as e:
            calmjs_dist.is_json_compat({
                'devDependencies': {
                    'left-pad': NotImplemented,
                }
            })

        msg = str(e.exception)
        self.assertIn('must be a JSON serializable object', msg)
        self.assertIn('NotImplemented', msg)

    def test_is_json_compat_bad_type_not_dict(self):
        with self.assertRaises(ValueError) as e:
            calmjs_dist.is_json_compat(1)

        self.assertEqual(
            str(e.exception),
            'must be specified as a JSON serializable dict '
            'or a JSON deserializable string'
        )

        with self.assertRaises(ValueError) as e:
            calmjs_dist.is_json_compat('"hello world"')

        self.assertEqual(
            str(e.exception),
            'must be specified as a JSON serializable dict '
            'or a JSON deserializable string'
        )

    def test_is_json_compat_bad_encode(self):
        with self.assertRaises(ValueError) as e:
            calmjs_dist.is_json_compat(
                '{'
                '    "devDependencies": {'
                '        "left-pad": "~1.1.1",'  # trailing comma
                '    },'
                '}'
            )

        self.assertTrue(str(e.exception).startswith('JSON decoding error:'))

    def test_is_json_compat_good_str(self):
        result = calmjs_dist.is_json_compat(
            '{'
            '    "devDependencies": {'
            '        "left-pad": "~1.1.1"'
            '    }'
            '}'
        )
        self.assertTrue(result)

    def test_is_json_compat_good_dict(self):
        result = calmjs_dist.is_json_compat(
            # trailing commas are fine in python dicts.
            {
                "devDependencies": {
                    "left-pad": "~1.1.1",
                },
            },
        )
        self.assertTrue(result)

    def test_is_json_compat_good_dict_with_none(self):
        # Possible to specify a null requirement to remove things.
        result = calmjs_dist.is_json_compat(
            {
                "devDependencies": {
                    "left-pad": None
                },
            },
        )
        self.assertTrue(result)

    def test_validate_json_field_good(self):
        # don't need to validate against None as "the validation
        # function will only be called if the setup() call sets it to a"
        # non-None value", as per setuptools documentation.

        self.assertTrue(calmjs_dist.validate_json_field(
            self.dist, self.optname, {}))

    def test_validate_json_field_bad(self):
        with self.assertRaises(DistutilsSetupError) as e:
            calmjs_dist.validate_json_field(
                self.dist, self.optname, "{},")

        self.assertTrue(str(e.exception).startswith(
            "'default_json' JSON decoding error:"
        ))

    def test_validate_line_list_good(self):
        self.assertTrue(calmjs_dist.validate_line_list(
            self.dist, self.optname, ['this', 'value']))
        self.assertTrue(calmjs_dist.validate_line_list(
            self.dist, self.optname, 'this\nvalue'))
        self.assertTrue(calmjs_dist.validate_line_list(
            self.dist, self.optname, ['this']))
        self.assertTrue(calmjs_dist.validate_line_list(
            self.dist, self.optname, 'this'))
        self.assertTrue(calmjs_dist.validate_line_list(
            self.dist, self.optname, ('this', 'value')))

    def test_validate_line_list_bad(self):
        with self.assertRaises(DistutilsSetupError) as e:
            calmjs_dist.validate_line_list(self.dist, 'items', [
                'in valid', 'value'])
        self.assertTrue(str(e.exception).startswith(
            "'items' must be a list of valid identifiers"))

        with self.assertRaises(DistutilsSetupError) as e:
            calmjs_dist.validate_line_list(self.dist, 'items', [
                'this', object()])

        with self.assertRaises(DistutilsSetupError) as e:
            calmjs_dist.validate_line_list(self.dist, 'items', [
                'this', object()])

    def test_write_json_file(self):
        self.dist.default_json = '{}'
        ei = Mock_egg_info(self.dist)
        ei.initialize_options()
        calmjs_dist.write_json_file(
            'default_json', ei, self.pkgname, self.pkgname)
        self.assertEqual(ei.called[self.pkgname], '{}')

    def test_write_json_file_dict(self):
        self.dist.default_json = {}
        ei = Mock_egg_info(self.dist)
        ei.initialize_options()
        calmjs_dist.write_json_file(
            'default_json', ei, self.pkgname, self.pkgname)
        self.assertEqual(ei.called[self.pkgname], '{}')

    def test_write_json_file_delete(self):
        self.dist.default_json = None  # this triggers the delete
        ei = Mock_egg_info(self.dist)
        ei.initialize_options()
        calmjs_dist.write_json_file(
            'default_json', ei, self.pkgname, self.pkgname)
        # However since the top level method was stubbed out, just check
        # that it's been called...
        self.assertEqual(ei.called[self.pkgname], None)

    def test_write_line_list(self):
        self.dist.field = ['module', 'tests']
        ei = Mock_egg_info(self.dist)
        ei.initialize_options()
        calmjs_dist.write_line_list('field', ei, self.pkgname, self.pkgname)
        self.assertEqual(ei.called[self.pkgname], 'module\ntests')

    def test_write_line_list_str(self):
        self.dist.field = 'module\ntests'
        ei = Mock_egg_info(self.dist)
        ei.initialize_options()
        calmjs_dist.write_line_list('field', ei, self.pkgname, self.pkgname)
        self.assertEqual(ei.called[self.pkgname], 'module\ntests')

    def test_write_line_list_delete(self):
        self.dist.field = None
        ei = Mock_egg_info(self.dist)
        ei.initialize_options()
        calmjs_dist.write_line_list('field', ei, self.pkgname, self.pkgname)
        self.assertEqual(ei.called[self.pkgname], None)

    def test_find_pkg_dist(self):
        # Only really testing that this returns an actual distribution
        result = calmjs_dist.find_pkg_dist('setuptools')
        # it's the Distribution class from pkg_resources...
        self.assertTrue(isinstance(result, pkg_resources.Distribution))
        self.assertEqual(result.project_name, 'setuptools')

    def test_convert_package_names(self):
        result, error = calmjs_dist.convert_package_names('setuptools calmjs')
        self.assertEqual(result, ['setuptools', 'calmjs'])
        self.assertEqual(error, [])

        result, error = calmjs_dist.convert_package_names('calmjs [dev]')
        self.assertEqual(result, ['calmjs'])
        self.assertEqual(error, ['[dev]'])

        result, error = calmjs_dist.convert_package_names('calmjs[dev]')
        self.assertEqual(result, ['calmjs[dev]'])
        self.assertEqual(error, [])

        result, error = calmjs_dist.convert_package_names(
            ['setuptools'])
        self.assertEqual(result, ['setuptools'])
        self.assertEqual(error, [])

        result, error = calmjs_dist.convert_package_names(
            ['setuptools', '[dev]', 'calmjs [dev]'])
        self.assertEqual(result, ['setuptools', 'calmjs [dev]'])
        self.assertEqual(error, ['[dev]'])

    def test_pkg_names_to_dists(self):
        base = make_dummy_dist(self, (('requires.txt', ''),), 'base', '1.0.0')
        working_set = pkg_resources.WorkingSet()
        working_set.add(base, self._calmjs_testing_tmpdir)
        self.assertEqual(calmjs_dist.pkg_names_to_dists(
            ['nothing', 'base'], working_set=working_set), [base])

    def test_get_pkg_json_integrated_live(self):
        # Try reading a fake package.json from setuptools package
        # directly and see that it will just return nothing while not
        # exploding.
        self.assertIsNone(calmjs_dist.read_egginfo_json(
            'setuptools', filename='_not_package.json'))

    def test_read_dist_egginfo_json(self):
        package_json = {"dependencies": {"left-pad": "~1.1.1"}}

        # We will mock up a Distribution object with some fake metadata.
        mock_provider = MockProvider({
            self.pkgname: json.dumps(package_json),
        })

        mock_dist = pkg_resources.Distribution(
            metadata=mock_provider, project_name='dummydist', version='0.0.0')

        results = calmjs_dist.read_dist_egginfo_json(mock_dist)

        self.assertEqual(results, package_json)

    def test_get_dist_package_decoding_error(self):
        # Quiet stdout from distutils logs
        stub_stdouts(self)

        # trailing comma
        package_json = '{"dependencies": {"left-pad": "~1.1.1"},}'
        # bad data could be created by a competiting package.
        mock_provider = MockProvider({
            self.pkgname: package_json,
        })
        mock_dist = pkg_resources.Distribution(
            metadata=mock_provider, project_name='dummydist', version='0.0.0')

        results = calmjs_dist.read_dist_egginfo_json(mock_dist)

        # Should still not fail.
        self.assertIsNone(results)

    def test_get_dist_package_read_error(self):
        # Quiet stdout from distutils logs
        stub_stdouts(self)

        mock_provider = MockProvider({
            self.pkgname: None,  # None will emulate IO error.
        })
        mock_dist = pkg_resources.Distribution(
            metadata=mock_provider, project_name='dummydist', version='0.0.0')
        results = calmjs_dist.read_dist_egginfo_json(mock_dist)
        # Should still not fail.
        self.assertIsNone(results)

    def test_get_dist_package_fs(self):
        """
        Use the make_dummy_dist testing util to generate a working
        distribution based on upstream library.
        """

        package_json = {"dependencies": {"left-pad": "~1.1.1"}}
        mock_dist = make_dummy_dist(
            self, (
                (self.pkgname, json.dumps(package_json)),
            ), pkgname='dummydist'
        )
        results = calmjs_dist.read_dist_egginfo_json(mock_dist)
        self.assertEqual(results['dependencies']['left-pad'], '~1.1.1')

    def test_read_dist_egginfo_json_alternative_name_args(self):
        package_json = {"dependencies": {"left-pad": "~1.1.1"}}

        # We will mock up a Distribution object with some fake metadata.
        mock_provider = MockProvider({
            'bower.json': json.dumps(package_json),
        })

        mock_dist = pkg_resources.Distribution(
            metadata=mock_provider, project_name='dummydist', version='0.0.0')

        results = calmjs_dist.read_dist_egginfo_json(
            mock_dist, filename='bower.json')

        self.assertEqual(results, package_json)

        working_set = pkg_resources.WorkingSet()
        working_set.add(mock_dist)

        self.assertEqual(package_json, calmjs_dist.read_egginfo_json(
            'dummydist', filename='bower.json', working_set=working_set))

        # Finally do the flattening
        flattened_json = {
            "dependencies": {"left-pad": "~1.1.1"}, "devDependencies": {}}
        self.assertEqual(flattened_json, calmjs_dist.flatten_dist_egginfo_json(
            [mock_dist], filename='bower.json', working_set=working_set))
        self.assertEqual(flattened_json, calmjs_dist.flatten_egginfo_json(
            ['dummydist'], filename='bower.json', working_set=working_set))

    def tests_flatten_egginfo_json_deps(self):
        # Quiet stdout from distutils logs
        stub_stdouts(self)
        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
            ])),
            (self.pkgname, 'This is very NOT a package.json.'),
        ), 'security', '9999')

        framework = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'security',
            ])),
            (self.pkgname, json.dumps({
                'name': 'framework',
                'description': 'some framework',
                'dependencies': {
                    'left-pad': '~1.1.1',
                },
                'devDependencies': {
                    'sinon': '~1.15.0',
                },
            })),
        ), 'framework', '2.4')

        widget = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.1',
            ])),
            (self.pkgname, json.dumps({
                'dependencies': {
                    'jquery': '~2.0.0',
                    'underscore': '~1.7.0',
                },
            })),
        ), 'widget', '1.1')

        forms = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.2',
                'widget>=1.0',
            ])),
            (self.pkgname, json.dumps({
                'dependencies': {
                    'backbone': '~1.3.0',
                    'jquery-ui': '~1.12.0',
                },
            })),
        ), 'forms', '1.6')

        service = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.1',
            ])),
            (self.pkgname, json.dumps({
                'dependencies': {
                    'underscore': '~1.8.0',
                },
                'devDependencies': {
                    'sinon': '~1.17.0',
                },
            })),
        ), 'service', '1.1')

        site = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.1',
                'widget>=1.1',
                'forms>=1.6',
                'service>=1.1',
            ])),
            (self.pkgname, json.dumps({
                'name': 'site',
                'dependencies': {
                    'underscore': '~1.8.0',
                    'jquery': '~1.9.0',
                },
            })),
        ), 'site', '2.0')

        answer = {
            'name': 'site',
            'dependencies': {
                'left-pad': '~1.1.1',
                'jquery': '~1.9.0',
                'backbone': '~1.3.0',
                'jquery-ui': '~1.12.0',
                'underscore': '~1.8.0',
            },
            'devDependencies': {
                'sinon': '~1.17.0',
            },
        }

        # WorkingSet is a frozen representation of the versions and
        # locations of all available package presented through sys.path
        # by default.  Here we just emulate it using our temporary path
        # created by our mock package definitions above.

        working_set = pkg_resources.WorkingSet([self._calmjs_testing_tmpdir])

        # Ensure that this works with a raw requirements object, that
        # should normally be automatically resolved from a name.
        result = calmjs_dist.flatten_dist_egginfo_json(
            [framework, widget, forms, service, site], working_set=working_set)
        self.assertEqual(result, answer)

        # Also a raw requirement (package) string on the other function.
        result = calmjs_dist.flatten_egginfo_json(
            ['site'], working_set=working_set)
        self.assertEqual(result, answer)

    def tests_flatten_egginfo_json_multi_version(self):
        """
        Need to ensure the *correct* version is picked.
        """

        uilib_1_1 = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([])),
            (self.pkgname, json.dumps({
                'dependencies': {'jquery': '~1.0.0'},
            })),
        ), 'uilib', '1.1.0')

        uilib_1_4 = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([])),
            (self.pkgname, json.dumps({
                'dependencies': {'jquery': '~1.4.0'},
            })),
        ), 'uilib', '1.4.0')

        uilib_1_9 = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([])),
            (self.pkgname, json.dumps({
                'dependencies': {'jquery': '~1.9.0'},
            })),
        ), 'uilib', '1.9.0')

        app = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'uilib>=1.0',
            ])),
        ), 'app', '2.0')

        # Instead of passing in the tmpdir like the previous test, this
        # working set will be manually created as the situation here
        # should not happen normally - a raw (dist|site)-packages dir
        # with multiple versions of egg-info available for a single
        # importable path - a situation that results in the uilib's
        # actual version being ambiguous.  Anyway, each of these
        # "versions" should be in their own .egg directory with an
        # "EGG-INFO" subdir underneath, with the top level egg path
        # being added to sys.path either through a site.py or some kind
        # of generated program entry point that does that.  For all
        # intents and purposes if the manual Requirements are added to
        # the WorkingSet like so, the expected values presented in the
        # system can be created to behave as if they really exist.

        working_set = pkg_resources.WorkingSet()
        working_set.add(uilib_1_9, self._calmjs_testing_tmpdir)
        working_set.add(app, self._calmjs_testing_tmpdir)

        answer = {
            'dependencies': {
                'jquery': '~1.9.0',
            },
            'devDependencies': {},
        }
        result = calmjs_dist.flatten_egginfo_json(
            ['app'], working_set=working_set)
        self.assertEqual(result, answer)

        # Now emulate an older version, with a different working set.

        working_set = pkg_resources.WorkingSet()
        working_set.add(uilib_1_4, self._calmjs_testing_tmpdir)
        # this shouldn't override the previous.
        working_set.add(uilib_1_1, self._calmjs_testing_tmpdir)
        working_set.add(app, self._calmjs_testing_tmpdir)

        answer = {
            'dependencies': {
                'jquery': '~1.4.0',
            },
            'devDependencies': {},
        }
        result = calmjs_dist.flatten_egginfo_json(
            ['app'], working_set=working_set)
        self.assertEqual(result, answer)

    def tests_flatten_egginfo_json_missing_complete(self):
        """
        A completely missing egg should not just blow up.
        """

        working_set = pkg_resources.WorkingSet()
        self.assertEqual(
            {'dependencies': {}, 'devDependencies': {}},
            calmjs_dist.flatten_egginfo_json(
                'nosuchpkg', working_set=working_set))

    def tests_flatten_egginfo_json_missing_deps(self):
        """
        Missing dependencies should not cause a hard failure.
        """

        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'uilib>=1.0',
            ])),
        ), 'app', '2.0')

        working_set = pkg_resources.WorkingSet([self._calmjs_testing_tmpdir])

        # Python dependency acquisition failures should fail hard.
        with self.assertRaises(pkg_resources.DistributionNotFound):
            calmjs_dist.flatten_egginfo_json(['app'], working_set=working_set)

    def tests_flatten_egginfo_json_nulled(self):
        """
        Need to ensure the *correct* version is picked.
        """

        lib = make_dummy_dist(self, (  # noqa: F841
            ('requires.txt', '\n'.join([])),
            (self.pkgname, json.dumps({
                'dependencies': {
                    'jquery': '~3.0.0',
                    'left-pad': '1.1.1',
                },
            })),
        ), 'lib', '1.0.0')

        app = make_dummy_dist(self, (  # noqa: F841
            ('requires.txt', '\n'.join([
                'lib>=1.0.0',
            ])),
            (self.pkgname, json.dumps({
                'dependencies': {
                    'jquery': '~3.0.0',
                    'left-pad': None,
                },
            })),
        ), 'app', '2.0')

        working_set = pkg_resources.WorkingSet([self._calmjs_testing_tmpdir])

        answer = {
            'dependencies': {
                'jquery': '~3.0.0',
                # left-pad will be absent as app removed via None.
            },
            'devDependencies': {},
        }
        result = calmjs_dist.flatten_egginfo_json(
            ['app'], working_set=working_set)
        self.assertEqual(result, answer)

    def test_package_name_to_dists(self):
        lib1 = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([])),
        ), 'lib1', '1.0.0')
        lib2 = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([])),
        ), 'lib2', '1.0.0')
        lib3 = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'lib1>=1.0.0',
                'lib2>=1.0.0',
            ])),
        ), 'lib3', '1.0.0')

        app = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'lib3>=1.0.0',
            ])),
        ), 'app', '2.0')

        extra = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
            ])),
        ), 'extra', '2.0')

        working_set = pkg_resources.WorkingSet()
        working_set.add(lib1, self._calmjs_testing_tmpdir)
        working_set.add(lib2, self._calmjs_testing_tmpdir)
        working_set.add(lib3, self._calmjs_testing_tmpdir)
        working_set.add(app, self._calmjs_testing_tmpdir)
        working_set.add(extra, self._calmjs_testing_tmpdir)

        # finding individual packages
        self.assertEqual(['app'], [
            d.project_name
            for d in calmjs_dist.pkg_names_to_dists(
                ['app'], working_set=working_set)])

        self.assertEqual(['lib3'], [
            d.project_name
            for d in calmjs_dist.pkg_names_to_dists(
                ['lib3'], working_set=working_set)])

        # finding everything
        self.assertEqual(['lib1', 'lib2', 'lib3', 'app'], [
            d.project_name
            for d in calmjs_dist.find_packages_requirements_dists(
                ['app'], working_set=working_set)])

        self.assertEqual(['lib1', 'lib2', 'lib3', 'extra'], [
            d.project_name
            for d in calmjs_dist.find_packages_requirements_dists(
                ['lib3', 'extra'], working_set=working_set)])

        # only find the parents
        self.assertEqual(['lib1', 'lib2', 'lib3'], [
            d.project_name
            for d in calmjs_dist.find_packages_parents_requirements_dists(
                ['app'], working_set=working_set)])

        self.assertEqual(['lib1', 'lib2'], [
            d.project_name
            for d in calmjs_dist.find_packages_parents_requirements_dists(
                ['app', 'lib3'], working_set=working_set)])

        self.assertEqual(['lib1', 'lib2'], [
            d.project_name
            for d in calmjs_dist.find_packages_parents_requirements_dists(
                ['lib3', 'app'], working_set=working_set)])

        self.assertEqual(['lib1', 'lib2'], [
            d.project_name
            for d in calmjs_dist.find_packages_parents_requirements_dists(
                ['lib3'], working_set=working_set)])

        self.assertEqual([], [
            d.project_name
            for d in calmjs_dist.find_packages_parents_requirements_dists(
                ['lib1'], working_set=working_set)])

        self.assertEqual([], [
            d.project_name
            for d in calmjs_dist.find_packages_parents_requirements_dists(
                ['lib2', 'lib1'], working_set=working_set)])

    # While it really is for node/npm, the declaration is almost generic
    # enough that the particular method should be used here.
    def test_node_modules_registry_flattening(self):
        lib = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([])),
            (self.pkgname, json.dumps({
                'dependencies': {
                    'jquery': '~1.8.3',
                    'underscore': '1.8.3',
                },
            })),
            ('extras_calmjs.json', json.dumps({
                'node_modules': {
                    'jquery': 'jquery/dist/jquery.js',
                    'underscore': 'underscore/underscore-min.js',
                },
                'something_else': {'parent': 'lib'},
            })),
        ), 'lib', '1.0.0')

        app = make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'lib>=1.0.0',
            ])),
            (self.pkgname, json.dumps({
                'dependencies': {
                    'jquery': '~3.0.0',
                },
            })),
            ('extras_calmjs.json', json.dumps({
                'node_modules': {
                    'jquery': 'jquery/dist/jquery.min.js',
                },
                'something_else': {'child': 'named'},
            })),
        ), 'app', '2.0')

        working_set = pkg_resources.WorkingSet()
        working_set.add(lib, self._calmjs_testing_tmpdir)
        working_set.add(app, self._calmjs_testing_tmpdir)

        single = calmjs_dist.get_extras_calmjs(
            ['app'], working_set=working_set)
        self.assertEqual(single['node_modules'], {
            'jquery': 'jquery/dist/jquery.min.js',
        })

        results = calmjs_dist.flatten_extras_calmjs(
            ['app'], working_set=working_set)
        self.assertEqual(results['node_modules'], {
            'jquery': 'jquery/dist/jquery.min.js',
            'underscore': 'underscore/underscore-min.js',
        })
        # child takes precedences as this was not specified to be merged
        self.assertEqual(results['something_else'], {'child': 'named'})

        results = calmjs_dist.flatten_parents_extras_calmjs(
            ['app'], working_set=working_set)
        self.assertEqual(results['node_modules'], {
            'jquery': 'jquery/dist/jquery.js',
            'underscore': 'underscore/underscore-min.js',
        })
        self.assertEqual(results['something_else'], {'parent': 'lib'})

    def test_module_registry_dependencies_failure_no_reg(self):
        self.assertEqual(calmjs_dist.flatten_module_registry_dependencies(
            ['calmjs'], registry_name='calmjs.no_reg',), {})

        self.assertEqual(
            calmjs_dist.flatten_parents_module_registry_dependencies(
                ['calmjs'], registry_name='calmjs.no_reg',), {})

        self.assertEqual(calmjs_dist.get_module_registry_dependencies(
            ['calmjs'], registry_name='calmjs.no_reg',), {})

    def test_module_registry_dependencies_success(self):
        from calmjs.registry import _inst

        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
            ])),
        ), 'security', '9999')

        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'security',
            ])),
        ), 'framework', '2.4')

        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.1',
            ])),
        ), 'widget', '1.1')

        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.2',
                'widget>=1.0',
            ])),
        ), 'forms', '1.6')

        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.1',
            ])),
        ), 'service', '1.1')

        make_dummy_dist(self, (
            ('requires.txt', '\n'.join([
                'framework>=2.1',
                'widget>=1.1',
                'forms>=1.6',
                'service>=1.1',
            ])),
        ), 'site', '2.0')

        working_set = pkg_resources.WorkingSet([self._calmjs_testing_tmpdir])

        dummy_regid = 'calmjs.module.dummy.test'

        # ensure the dummy record we adding will be cleaned up.
        def cleanup():
            _inst.records.pop(dummy_regid, None)
        self.addCleanup(cleanup)

        # set up/register a dummy registry with dummy records.
        dummy_reg = _inst.records[dummy_regid] = ModuleRegistry(dummy_regid)
        dummy_reg.records = {
            'site': {
                'site/config': '/home/src/site/config.js',
            },
            'widget': {
                'widget/ui': '/home/src/widget/ui.js',
                'widget/widget': '/home/src/widget/widget.js',
            },
            'forms': {
                'forms/ui': '/home/src/forms/ui.js',
            },
            'service': {
                'service/lib': '/home/src/forms/lib.js',
            },
        }
        dummy_reg.package_module_map = {
            'site': ['site'],
            'widget': ['widget'],
            'forms': ['forms'],
            'service': ['service'],
        }

        site = calmjs_dist.flatten_module_registry_dependencies(
            ['site'], registry_name=dummy_regid, working_set=working_set)
        self.assertEqual(site, {
            'site/config': '/home/src/site/config.js',
            'widget/ui': '/home/src/widget/ui.js',
            'widget/widget': '/home/src/widget/widget.js',
            'service/lib': '/home/src/forms/lib.js',
            'forms/ui': '/home/src/forms/ui.js',
        })

        self.assertEqual(
            calmjs_dist.flatten_parents_module_registry_dependencies(
                ['site'], registry_name=dummy_regid, working_set=working_set
            ), {
                'widget/ui': '/home/src/widget/ui.js',
                'widget/widget': '/home/src/widget/widget.js',
                'service/lib': '/home/src/forms/lib.js',
                'forms/ui': '/home/src/forms/ui.js',
            }
        )

        service = calmjs_dist.flatten_module_registry_dependencies(
            ['service'], registry_name=dummy_regid, working_set=working_set)
        self.assertEqual(service, {
            'service/lib': '/home/src/forms/lib.js',
        })

        forms = calmjs_dist.flatten_module_registry_dependencies(
            ['forms'], registry_name=dummy_regid, working_set=working_set)
        self.assertEqual(forms, {
            'forms/ui': '/home/src/forms/ui.js',
            'widget/ui': '/home/src/widget/ui.js',
            'widget/widget': '/home/src/widget/widget.js',
        })

        # merger
        merged = calmjs_dist.flatten_module_registry_dependencies(
            ['forms', 'service'], registry_name=dummy_regid,
            working_set=working_set)
        self.assertEqual(merged, {
            'forms/ui': '/home/src/forms/ui.js',
            'widget/ui': '/home/src/widget/ui.js',
            'widget/widget': '/home/src/widget/widget.js',
            'service/lib': '/home/src/forms/lib.js',
        })

        self.assertEqual(
            calmjs_dist.flatten_parents_module_registry_dependencies(
                ['forms', 'service', 'app'], registry_name=dummy_regid,
                working_set=working_set
            ), {
                'widget/ui': '/home/src/widget/ui.js',
                'widget/widget': '/home/src/widget/widget.js',
            }
        )

        # no declared exports/registry entries in security.
        security = calmjs_dist.flatten_module_registry_dependencies(
            ['security'], registry_name=dummy_regid, working_set=working_set)
        self.assertEqual(security, {})

        # package not even in working set
        missing_pkg = calmjs_dist.flatten_module_registry_dependencies(
            ['missing_pkg'], registry_name=dummy_regid,
            working_set=working_set)
        self.assertEqual(missing_pkg, {})

        # singlular methods
        self.assertEqual(calmjs_dist.get_module_registry_dependencies(
            ['site'], registry_name=dummy_regid, working_set=working_set), {
            'site/config': '/home/src/site/config.js'})

        self.assertEqual(calmjs_dist.get_module_registry_dependencies(
            ['security'],
            registry_name=dummy_regid, working_set=working_set), {})

        self.assertEqual(calmjs_dist.get_module_registry_dependencies(
            ['missing'],
            registry_name=dummy_regid, working_set=working_set), {})

    def test_read_dist_line_list(self):
        # We will mock up a Distribution object with some fake metadata.
        mock_provider = MockProvider({
            'list.txt': 'reg1\nreg2',
        })
        mock_dist = pkg_resources.Distribution(
            metadata=mock_provider, project_name='dummydist', version='0.0.0')
        results = calmjs_dist.read_dist_line_list(mock_dist, 'list.txt')
        self.assertEqual(results, ['reg1', 'reg2'])

    def test_read_dist_line_io_error(self):
        # We will mock up a Distribution object with some fake metadata.
        stub_stdouts(self)
        mock_provider = MockProvider({
            'list.txt': None  # the MockProvider emulates IOError
        })
        mock_dist = pkg_resources.Distribution(
            metadata=mock_provider, project_name='dummydist', version='0.0.0')
        results = calmjs_dist.read_dist_line_list(mock_dist, 'list.txt')
        self.assertEqual(results, [])

    def test_module_module_registry_names_no_reg(self):
        working_set = pkg_resources.WorkingSet()
        self.assertEqual(calmjs_dist.flatten_module_registry_names(
            ['nothing'], working_set=working_set), [])
        self.assertEqual(calmjs_dist.get_module_registry_names(
            ['nothing'], working_set=working_set), [])

    def test_module_module_registry_names_success(self):
        base = make_dummy_dist(self, (
            ('requires.txt', ''),
        ), 'base', '1.0.0')

        lib = make_dummy_dist(self, (
            ('requires.txt', 'base>=1.0.0'),
            (calmjs_dist.CALMJS_MODULE_REGISTRY_TXT,
                '\n'.join(['reg1', 'reg2'])),
        ), 'lib', '1.0.0')

        app = make_dummy_dist(self, (
            ('requires.txt', 'lib>=1.0.0'),
            (calmjs_dist.CALMJS_MODULE_REGISTRY_TXT,
                '\n'.join(['reg2', 'reg3'])),
        ), 'app', '2.0')

        working_set = pkg_resources.WorkingSet()
        working_set.add(base, self._calmjs_testing_tmpdir)
        working_set.add(lib, self._calmjs_testing_tmpdir)
        working_set.add(app, self._calmjs_testing_tmpdir)

        self.assertEqual(calmjs_dist.get_module_registry_names(
            ['base'], working_set=working_set), [])
        self.assertEqual(calmjs_dist.get_module_registry_names(
            ['lib'], working_set=working_set), ['reg1', 'reg2'])
        self.assertEqual(calmjs_dist.get_module_registry_names(
            ['app'], working_set=working_set), ['reg2', 'reg3'])
        self.assertEqual(calmjs_dist.get_module_registry_names(
            ['nothing'], working_set=working_set), [])

        self.assertEqual(calmjs_dist.flatten_module_registry_names(
            ['base'], working_set=working_set), [])
        self.assertEqual(calmjs_dist.flatten_module_registry_names(
            ['lib'], working_set=working_set), ['reg1', 'reg2'])
        self.assertEqual(calmjs_dist.flatten_module_registry_names(
            ['app'], working_set=working_set), ['reg1', 'reg2', 'reg3'])
        self.assertEqual(calmjs_dist.flatten_module_registry_names(
            ['nothing'], working_set=working_set), [])


class ArtifactIntegrationTestCase(unittest.TestCase):

    def test_calmjs_artifact_declarations(self):
        from calmjs.registry import _inst

        # the actual implementations this is supporting
        from calmjs.artifact import build_calmjs_artifacts
        from calmjs.artifact import ArtifactRegistry

        working_dir = mkdtemp(self)
        make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.registry]',
                'calmjs.artifacts = calmjs.artifact:ArtifactsRegistry',
            ])),
        ), 'calmjs', '1.0', working_dir=working_dir)

        make_dummy_dist(self, (
            ('entry_points.txt', '\n'.join([
                '[calmjs.artifacts]',
                'example.js = example:builder',
            ])),
        ), 'some.package', '1.0', working_dir=working_dir)

        mock_ws = pkg_resources.WorkingSet([working_dir])
        registry_id = 'calmjs.artifacts'
        registry = ArtifactRegistry(registry_id, _working_set=mock_ws)
        # cleanup the about to be injected version.
        self.addCleanup(_inst.records.pop, registry_id, None)
        _inst.records['calmjs.artifacts'] = registry

        # construct a command for the declaration check.
        cmd = build_calmjs_artifacts(dist=distutils_dist.Distribution(
            attrs={'name': 'some.package'}))
        self.assertTrue(calmjs_dist.has_calmjs_artifact_declarations(cmd))

        cmd = build_calmjs_artifacts(dist=distutils_dist.Distribution(
            attrs={'name': 'missing.package'}))
        self.assertFalse(calmjs_dist.has_calmjs_artifact_declarations(cmd))

        cmd = build_calmjs_artifacts(dist=distutils_dist.Distribution(
            attrs={'name': 'calmjs'}))
        self.assertFalse(calmjs_dist.has_calmjs_artifact_declarations(cmd))

    def test_build_calmjs_artifacts_standard(self):
        dist = distutils_dist.Distribution()
        build_cmd = dist.get_command_obj('build')
        original_subcmds = list(build_cmd.sub_commands)
        calmjs_dist.build_calmjs_artifacts(dist, 'build_artifact', False)
        self.assertEqual(original_subcmds, build_cmd.sub_commands)

        # keys are named after the build step.
        calmjs_dist.build_calmjs_artifacts(dist, 'build_artifact', True)
        self.assertEqual(
            ('build_artifact', calmjs_dist.has_calmjs_artifact_declarations),
            build_cmd.sub_commands[-1],
        )

        calmjs_dist.build_calmjs_artifacts(dist, 'calmjs_artifact', True)
        self.assertEqual(
            ('calmjs_artifact', calmjs_dist.has_calmjs_artifact_declarations),
            build_cmd.sub_commands[-1],
        )

    def test_build_calmjs_artifacts_failure(self):
        def fakecmd(*a, **kw):
            return object

        dist = distutils_dist.Distribution(attrs={
            'cmdclass': {'build': fakecmd},
        })

        with pretty_logging(stream=StringIO()) as stream:
            calmjs_dist.build_calmjs_artifacts(dist, 'build_again', True)

        self.assertIn(
            "'build' command in Distribution is not an instance of "
            "'distutils.command.build:build'", stream.getvalue(),
        )


class DistIntegrationTestCase(unittest.TestCase):
    """
    Testing integration of dist with the rest of calmjs and setuptools.
    """

    def setUp(self):
        """
        Set up the dummy test files.
        """

        self.pkg_root = mkdtemp(self)
        setup_py = join(self.pkg_root, 'setup.py')
        dummy_pkg = join(self.pkg_root, 'dummy_pkg.py')

        contents = (
            (setup_py, '''
                from setuptools import setup
                setup(
                    py_modules=['dummy_pkg'],
                    name='dummy_pkg',
                    package_json={
                        'dependencies': {
                            'jquery': '~3.0.0',
                        },
                    },
                    extras_calmjs={
                        'node_modules': {
                            'jquery': 'jquery/dist/jquery.js',
                        },
                    },
                    calmjs_module_registry=['reg1', 'reg2'],
                    zip_safe=False,
                )
            '''),
            (dummy_pkg, '''
            foo = 'bar'
            '''),
        )

        for fn, content in contents:
            with open(fn, 'w') as fd:
                fd.write(textwrap.dedent(content).lstrip())

    def test_setup_egg_info(self):
        """
        Emulate the execution of ``python setup.py egg_info``.

        Ensure everything is covered.
        """

        # naturally, run it like we mean it.
        p = Popen(
            [sys.executable, 'setup.py', 'egg_info'], stdout=PIPE, stderr=PIPE,
            cwd=self.pkg_root,
        )
        stdout, stderr = p.communicate()
        stdout = stdout.decode(locale)
        self.assertIn('writing package_json', stdout)
        self.assertIn('writing extras_calmjs', stdout)
        self.assertIn('writing calmjs_module_registry', stdout)

        egg_root = join(self.pkg_root, 'dummy_pkg.egg-info')

        with open(join(egg_root, 'package.json')) as fd:
            self.assertEqual(json.load(fd), {
                'dependencies': {
                    'jquery': '~3.0.0',
                },
            })

        with open(join(egg_root, 'extras_calmjs.json')) as fd:
            self.assertEqual(json.load(fd), {
                'node_modules': {
                    'jquery': 'jquery/dist/jquery.js',
                },
            })

        with open(join(egg_root, 'calmjs_module_registry.txt')) as fd:
            self.assertEqual(fd.read().split(), ['reg1', 'reg2'])

    def test_build_calmjs_artifact(self):
        """
        Emulate the execution of ``python setup.py egg_info``.

        Ensure everything is covered.
        """

        # run the step directly to see that the command is registered,
        # though the actual effects cannot be tested, as the test
        # package is not going to be installed and there are no valid
        # artifact build functions defined.
        p = Popen(
            [sys.executable, 'setup.py', 'build_calmjs_artifacts'],
            stdout=PIPE, stderr=PIPE, cwd=self.pkg_root,
        )
        stdout, stderr = p.communicate()
        stdout = stdout.decode(locale)
        self.assertIn('running build_calmjs_artifacts', stdout)