# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from unittest import TestCase

from conda._vendor.auxlib.collection import frozendict
import pytest

from conda import text_type
from conda.base.context import context
from conda.cli.common import arg2spec, spec_from_line
from conda.common.compat import on_win
from conda.exceptions import CondaValueError, InvalidMatchSpec, InvalidSpec
from conda.models.channel import Channel
from conda.models.dist import Dist
from conda.models.records import PackageRecord
from conda.models.match_spec import ChannelMatch, MatchSpec, _parse_spec_str
from conda.models.version import VersionSpec


blas_value = 'accelerate' if context.subdir == 'osx-64' else 'openblas'


def m(string):
    return text_type(MatchSpec(string))


def DPkg(s, **kwargs):
    d = Dist(s)
    return PackageRecord(
        fn=d.to_filename(),
        name=d.name,
        version=d.version,
        build=d.build_string,
        build_number=int(d.build_string.rsplit('_', 1)[-1]),
        channel=d.channel,
        subdir=context.subdir,
        md5="012345789",
        **kwargs)


class MatchSpecTests(TestCase):

    def test_match_1(self):
        for spec, result in [
            ('numpy 1.7*', True),          ('numpy 1.7.1', True),
            ('numpy 1.7', False),          ('numpy 1.5*', False),
            ('numpy >=1.5', True),         ('numpy >=1.5,<2', True),
            ('numpy >=1.8,<1.9', False),   ('numpy >1.5,<2,!=1.7.1', False),
            ('numpy >1.8,<2|==1.7', False),('numpy >1.8,<2|>=1.7.1', True),
            ('numpy >=1.8|1.7*', True),    ('numpy ==1.7', False),
            ('numpy >=1.5,>1.6', True),    ('numpy ==1.7.1', True),
            ('numpy ==1.7.1.0', True),     ('numpy==1.7.1.0.0', True),
            ('numpy >=1,*.7.*', True),     ('numpy *.7.*,>=1', True),
            ('numpy >=1,*.8.*', False),    ('numpy >=2,*.7.*', False),
            ('numpy 1.6*|1.7*', True),     ('numpy 1.6*|1.8*', False),
            ('numpy 1.6.2|1.7*', True),    ('numpy 1.6.2|1.7.1', True),
            ('numpy 1.6.2|1.7.0', False),  ('numpy 1.7.1 py27_0', True),
            ('numpy 1.7.1 py26_0', False), ('numpy >1.7.1a', True),
            ('python', False),
        ]:
            m = MatchSpec(spec)
            assert m.match(DPkg('numpy-1.7.1-py27_0.tar.bz2')) == result
            assert 'name' in m
            assert m.name == 'python' or 'version' in m

        # both version numbers conforming to PEP 440
        assert not MatchSpec('numpy >=1.0.1').match(DPkg('numpy-1.0.1a-0.tar.bz2'))
        # both version numbers non-conforming to PEP 440
        assert not MatchSpec('numpy >=1.0.1.vc11').match(DPkg('numpy-1.0.1a.vc11-0.tar.bz2'))
        assert MatchSpec('numpy >=1.0.1*.vc11').match(DPkg('numpy-1.0.1a.vc11-0.tar.bz2'))
        # one conforming, other non-conforming to PEP 440
        assert MatchSpec('numpy <1.0.1').match(DPkg('numpy-1.0.1.vc11-0.tar.bz2'))
        assert MatchSpec('numpy <1.0.1').match(DPkg('numpy-1.0.1a.vc11-0.tar.bz2'))
        assert not MatchSpec('numpy >=1.0.1.vc11').match(DPkg('numpy-1.0.1a-0.tar.bz2'))
        assert MatchSpec('numpy >=1.0.1a').match(DPkg('numpy-1.0.1z-0.tar.bz2'))
        assert MatchSpec('numpy >=1.0.1a py27*').match(DPkg('numpy-1.0.1z-py27_1.tar.bz2'))
        assert MatchSpec('blas * openblas_0').match(DPkg('blas-1.0-openblas_0.tar.bz2'))

        assert MatchSpec('blas')._is_simple()
        assert not MatchSpec('blas 1.0')._is_simple()
        assert not MatchSpec('blas 1.0 1')._is_simple()

        m = MatchSpec('blas 1.0', optional=True)
        m2 = MatchSpec(m, optional=False)
        m3 = MatchSpec(m2, target='blas-1.0-0.tar.bz2')
        m4 = MatchSpec(m3, target=None, optional=True)
        assert m.spec == m2.spec and m.optional != m2.optional
        assert m2.spec == m3.spec and m2.optional == m3.optional and m2.target != m3.target
        assert m == m4

        self.assertRaises(ValueError, MatchSpec, (1, 2, 3))

    def test_no_name_match_spec(self):
        ms = MatchSpec(track_features="mkl")
        assert str(ms) == '*[track_features=mkl]'

    def test_to_filename(self):
        m1 = MatchSpec(fn='foo-1.7-52.tar.bz2')
        m2 = MatchSpec(name='foo', version='1.7', build='52')
        m3 = MatchSpec(Dist('defaults::foo-1.7-52'))
        assert m1._to_filename_do_not_use() == 'foo-1.7-52.tar.bz2'
        assert m2._to_filename_do_not_use() == 'foo-1.7-52.tar.bz2'
        assert m3._to_filename_do_not_use() == 'foo-1.7-52.tar.bz2'

        for spec in 'bitarray', 'pycosat 0.6.0', 'numpy 1.6*':
            ms = MatchSpec(spec)
            assert ms._to_filename_do_not_use() is None

    def test_hash(self):
        a = MatchSpec('numpy 1.7*')
        b = MatchSpec('numpy 1.7*')
        c = MatchSpec(name='numpy', version='1.7*')
        # optional should not change the hash
        d = MatchSpec(c, optional=True)
        assert d.optional
        assert not c.optional
        assert a is not b
        assert a is not c
        assert a is not d
        assert a == b
        assert a == c
        assert a != d
        assert hash(a) == hash(b)
        assert hash(a) == hash(c)
        assert hash(a) != hash(d)
        c = MatchSpec('python')
        d = MatchSpec('python 2.7.4')
        e = MatchSpec('python', version='2.7.4')
        f = MatchSpec('* 2.7.4', name='python')
        assert d == e
        assert d == f
        assert a != c
        assert hash(a) != hash(c)
        assert c != d
        assert hash(c) != hash(d)

    # def test_string_mcg1969(self):
    #     a = MatchSpec("foo1 >=1.3 2", optional=True, target="burg")
    #     b = MatchSpec('* [name="foo1", version=">=1.3", build="2"]', optional=True, target="burg")
    #     assert a.optional and a.target == 'burg'
    #     assert a == b
    #     c = MatchSpec("^foo1$ >=1.3 2 ")
    #     d = MatchSpec("* >=1.3 2", name=re.compile(u'^foo1$'))
    #     e = MatchSpec("* >=1.3 2", name='^foo1$')
    #     assert c == d
    #     assert c == e
    #     # build_number is not the same as build!
    #     f = MatchSpec('foo1 >=1.3', build_number=2, optional=True, target='burg')
    #     g = MatchSpec('foo1 >=1.3[build_number=2]', optional=True, target='burg')
    #     assert a != f
    #     assert f == g
    #
    #     assert a._to_string() == "foo1 >=1.3 2"
    #     # assert b._to_string() == ""
    #     assert g._to_string() == "foo1 >=1.3[build_number=2]"

    def test_canonical_string_forms(self):
        assert m("numpy") == "numpy"

        assert m("numpy=1.7") == "numpy=1.7"
        assert m("numpy 1.7*") == "numpy=1.7"
        assert m("numpy 1.7.*") == "numpy=1.7"
        assert m("numpy[version='1.7*']") == "numpy=1.7"
        assert m("numpy[version='1.7.*']") == "numpy=1.7"
        assert m("numpy[version=1.7.*]") == "numpy=1.7"

        assert m("numpy==1.7") == "numpy==1.7"
        assert m("numpy[version='1.7']") == "numpy==1.7"
        assert m("numpy[version=1.7]") == "numpy==1.7"
        assert m("numpy 1.7") == "numpy==1.7"

        assert m("numpy[version='1.7|1.8']") == "numpy[version='1.7|1.8']"
        assert m('numpy[version="1.7,1.8"]') == "numpy[version='1.7,1.8']"
        assert m('numpy >1.7') == "numpy[version='>1.7']"
        assert m('numpy>=1.7') == "numpy[version='>=1.7']"

        assert m("numpy=1.7=py3*_2") == "numpy==1.7[build=py3*_2]"
        assert m("numpy=1.7.*=py3*_2") == "numpy=1.7[build=py3*_2]"

        assert m("https://repo.anaconda.com/pkgs/free::numpy") == "pkgs/free::numpy"
        assert m("numpy[channel=https://repo.anaconda.com/pkgs/free]") == "pkgs/free::numpy"
        assert m("defaults::numpy") == "defaults::numpy"
        assert m("numpy[channel=defaults]") == "defaults::numpy"
        assert m("conda-forge::numpy") == "conda-forge::numpy"
        assert m("numpy[channel=conda-forge]") == "conda-forge::numpy"

        assert m("numpy[channel=defaults,subdir=osx-64]") == "defaults/osx-64::numpy"
        assert m("numpy[channel=https://repo.anaconda.com/pkgs/free/osx-64, subdir=linux-64]") == "pkgs/free/linux-64::numpy"
        assert m("https://repo.anaconda.com/pkgs/free/win-32::numpy") == "pkgs/free/win-32::numpy"
        assert m("numpy[channel=https://repo.anaconda.com/pkgs/free/osx-64]") == "pkgs/free/osx-64::numpy"
        assert m("defaults/win-32::numpy") == "defaults/win-32::numpy"
        assert m("conda-forge/linux-64::numpy") == "conda-forge/linux-64::numpy"
        assert m("numpy[channel=conda-forge,subdir=noarch]") == "conda-forge/noarch::numpy"

        assert m("numpy[subdir=win-32]") == 'numpy[subdir=win-32]'
        assert m("*/win-32::numpy") == 'numpy[subdir=win-32]'
        assert m("*/win-32::numpy[subdir=\"osx-64\"]") == 'numpy[subdir=osx-64]'

        # TODO: should the result in these example pull out subdir?
        assert m("https://repo.anaconda.com/pkgs/free/linux-32::numpy") == "pkgs/free/linux-32::numpy"
        assert m("numpy[channel=https://repo.anaconda.com/pkgs/free/linux-32]") == "pkgs/free/linux-32::numpy"

        assert m("numpy=1.10=py38_0") == "numpy==1.10=py38_0"
        assert m("numpy==1.10=py38_0") == "numpy==1.10=py38_0"
        assert m("numpy[version=1.10 build=py38_0]") == "numpy==1.10=py38_0"

        assert m("numpy!=1.10") == "numpy!=1.10"
        assert m("numpy !=1.10") == "numpy!=1.10"
        assert m("numpy!=1.10 py38_0") == "numpy[version='!=1.10',build=py38_0]"
        assert m("numpy !=1.10 py38_0") == "numpy[version='!=1.10',build=py38_0]"
        assert m("numpy!=1.10=py38_0") == "numpy[version='!=1.10',build=py38_0]"
        assert m("numpy !=1.10=py38_0") == "numpy[version='!=1.10',build=py38_0]"
        assert m("numpy >1.7,!=1.10 py38_0") == "numpy[version='>1.7,!=1.10',build=py38_0]"
        assert m("numpy!=1.10.*") == "numpy!=1.10.*"
        assert m("numpy!=1.10,!=1.11") == "numpy[version='!=1.10,!=1.11']"
        assert m("numpy=1.10.*,!=1.10.2") == "numpy[version='=1.10.*,!=1.10.2']"

        assert m("numpy ~=1.10.1") == "numpy~=1.10.1"
        assert m("numpy~=1.10.1") == "numpy~=1.10.1"
        assert m("numpy ~=1.10.1 py38_0") == "numpy[version='~=1.10.1',build=py38_0]"

        # # a full, exact spec looks like 'defaults/linux-64::numpy==1.8=py26_0'
        # # can we take an old dist str and reliably parse it with MatchSpec?
        # assert m("numpy-1.10-py38_0") == "numpy==1.10=py38_0"
        # assert m("numpy-1.10-py38_0[channel=defaults]") == "defaults::numpy==1.10=py38_0"
        # assert m("*/win-32::numpy-1.10-py38_0[channel=defaults]") == "defaults/win-32::numpy==1.10=py38_0"

    @pytest.mark.skip(reason="key-value features interface has been disabled in conda 4.4")
    def test_key_value_features_canonical_string_forms(self):
        assert m("numpy[build=py3*_2, track_features=mkl]") == "numpy[build=py3*_2,provides_features='blas=mkl']"
        assert m("numpy[build=py3*_2, track_features='mkl debug']") == "numpy[build=py3*_2,provides_features='blas=mkl debug=true']"
        assert m("numpy[track_features='mkl,debug', build=py3*_2]") == "numpy[build=py3*_2,provides_features='blas=mkl debug=true']"
        assert m("numpy[track_features='mkl,debug' build=py3*_2]") == "numpy[build=py3*_2,provides_features='blas=mkl debug=true']"

        assert m('numpy[features="mkl debug" build_number=2]') == "numpy[build_number=2,provides_features='blas=mkl debug=true']"

    def test_legacy_features_canonical_string_forms(self):
        assert m("mkl@") == "*[track_features=mkl]"

        # assert m("@mkl") == "*[features=mkl]"
        assert text_type(MatchSpec(features="mkl")) == "*[features=mkl]"

    def test_tarball_match_specs(self):
        url = "https://conda.anaconda.org/conda-canary/linux-64/conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2"
        assert m(url) == "conda-canary/linux-64::conda==4.3.21.post699+1dab973=py36h4a561cd_0"
        assert m("conda-canary/linux-64::conda==4.3.21.post699+1dab973=py36h4a561cd_0") == "conda-canary/linux-64::conda==4.3.21.post699+1dab973=py36h4a561cd_0"

        url = "https://conda.anaconda.org/conda-canary/conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2"
        assert m(url) == "*[url=%s]" % url

        pref1 = PackageRecord(
            channel=Channel(None),
            name="conda",
            version="4.3.21.post699+1dab973",
            build="py36h4a561cd_0",
            build_number=0,
            fn="conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2",
            url=url,
        )
        pref2 = PackageRecord.from_objects(pref1, md5="1234")
        assert MatchSpec(url=url).match(pref1)
        assert MatchSpec(m(url)).match(pref1)
        assert MatchSpec(m(url)).match(pref1.dump())
        assert not MatchSpec(url=url, md5="1234").match(pref1)
        assert not MatchSpec(url=url, md5="1234").match(pref1.dump())
        assert MatchSpec(url=url, md5="1234").match(pref2)
        assert MatchSpec(url=url, md5="1234").get('md5') == "1234"

        url = "file:///var/folders/cp/7r2s_s593j7_cpdtxxsmct880000gp/T/edfc ñçêáôß/flask-0.10.1-py35_2.tar.bz2"
        assert m(url) == "*[url='%s']" % url
        # url = '*[url="file:///var/folders/cp/7r2s_s593j7_cpdtxxsmct880000gp/T/edfc ñçêáôß/flask-0.10.1-py35_2.tar.bz2"]'

        # TODO: we need this working correctly with both channel and subdir
        # especially for usages around PrefixData.all_subdir_urls() and Solver._prepare()
        # assert MatchSpec('defaults/zos::python').get_exact_value('channel').urls() == ()

    def test_exact_values(self):
        assert MatchSpec("*").get_exact_value('name') is None
        assert MatchSpec("numpy").get_exact_value('name') == 'numpy'

        assert MatchSpec("numpy=1.7").get_exact_value('version') is None
        assert MatchSpec("numpy==1.7").get_exact_value('version') == '1.7'
        assert MatchSpec("numpy[version=1.7]").get_exact_value('version') == '1.7'

        assert MatchSpec("numpy=1.7=py3*_2").get_exact_value('version') == '1.7'
        assert MatchSpec("numpy=1.7=py3*_2").get_exact_value('build') is None
        assert MatchSpec("numpy=1.7=py3*_2").get_exact_value('version') == '1.7'
        assert MatchSpec("numpy=1.7=py3*_2").get_exact_value('build') is None
        assert MatchSpec("numpy=1.7.*=py37_2").get_exact_value('version') is None
        assert MatchSpec("numpy=1.7.*=py37_2").get_exact_value('build') == 'py37_2'

    def test_channel_matching(self):
        assert ChannelMatch('pkgs/free').match('defaults') is False
        assert ChannelMatch('defaults').match('pkgs/free') is True

        assert ChannelMatch("https://repo.anaconda.com/pkgs/free").match('defaults') is False
        assert ChannelMatch("defaults").match("https://repo.anaconda.com/pkgs/free") is True

        assert ChannelMatch("https://conda.anaconda.org/conda-forge").match('conda-forge') is True
        assert ChannelMatch("conda-forge").match("https://conda.anaconda.org/conda-forge") is True

        assert ChannelMatch("https://repo.anaconda.com/pkgs/free").match('conda-forge') is False

        assert str(MatchSpec("pkgs/free::*")) == "pkgs/free::*"
        assert str(MatchSpec("defaults::*")) == "defaults::*"

    def test_matchspec_errors(self):
        with pytest.raises(InvalidSpec):
            MatchSpec('blas [optional')

        with pytest.raises(InvalidSpec):
            MatchSpec('blas [test=]')

        with pytest.raises(InvalidSpec):
            MatchSpec('blas[invalid="1"]')

        if not on_win:
            # skipping on Windows for now.  don't feel like dealing with the windows url path crud
            assert text_type(MatchSpec("/some/file/on/disk/package-1.2.3-2.tar.bz2")) == '*[url=file:///some/file/on/disk/package-1.2.3-2.tar.bz2]'

    def test_dist(self):
        dst = Dist('defaults::foo-1.2.3-4.tar.bz2')
        a = MatchSpec(dst)
        b = MatchSpec(a)
        c = MatchSpec(dst, optional=True, target='burg')
        d = MatchSpec(a, build='5')

        assert a == b
        assert hash(a) == hash(b)
        assert a is b

        assert a != c
        assert hash(a) != hash(c)

        assert a != d
        assert hash(a) != hash(d)

        p = MatchSpec(channel='defaults',name='python',version=VersionSpec('3.5*'))
        assert p.match(Dist(channel='defaults', dist_name='python-3.5.3-1', name='python',
                            version='3.5.3', build_string='1', build_number=1, base_url=None,
                            platform=None))

        assert not p.match(Dist(channel='defaults', dist_name='python-3.6.0-0', name='python',
                                version='3.6.0', build_string='0', build_number=0, base_url=None,
                                platform=None))

        assert p.match(Dist(channel='defaults', dist_name='python-3.5.1-0', name='python',
                            version='3.5.1', build_string='0', build_number=0, base_url=None,
                            platform=None))
        assert p.match(PackageRecord(name='python', version='3.5.1', build='0', build_number=0,
                                     depends=('openssl 1.0.2*', 'readline 6.2*', 'sqlite',
                                               'tk 8.5*', 'xz 5.0.5', 'zlib 1.2*', 'pip'),
                                     channel=Channel(scheme='https', auth=None,
                                                      location='repo.anaconda.com', token=None,
                                                      name='pkgs/free', platform='osx-64',
                                                      package_filename=None),
                                     subdir='osx-64', fn='python-3.5.1-0.tar.bz2',
                                     md5='a813bc0a32691ab3331ac9f37125164c', size=14678857,
                                     priority=0,
                                     url='https://repo.anaconda.com/pkgs/free/osx-64/python-3.5.1-0.tar.bz2'))

    def test_index_record(self):
        dst = Dist('defaults::foo-1.2.3-4.tar.bz2')
        rec = DPkg(dst)
        a = MatchSpec(dst)
        b = MatchSpec(rec)
        assert b.match(rec.dump())
        assert b.match(rec)
        assert a.match(rec)

    def test_strictness(self):
        assert MatchSpec('foo').strictness == 1
        assert MatchSpec('foo 1.2').strictness == 2
        assert MatchSpec('foo 1.2 3').strictness == 3
        assert MatchSpec('foo 1.2 3 [channel=burg]').strictness == 3
        # Seems odd, but this is needed for compatibility
        assert MatchSpec('test* 1.2').strictness == 3
        assert MatchSpec('foo', build_number=2).strictness == 3

    def test_build_number_and_filename(self):
        ms = MatchSpec('zlib 1.2.7 0')
        assert ms.get_exact_value('name') == 'zlib'
        assert ms.get_exact_value('version') == '1.2.7'
        assert ms.get_exact_value('build') == '0'
        assert ms._to_filename_do_not_use() == 'zlib-1.2.7-0.tar.bz2'

    def test_track_features_match(self):
        dst = Dist('defaults::foo-1.2.3-4.tar.bz2')
        a = MatchSpec(features='test')
        assert text_type(a) == "*[features=test]"
        assert not a.match(DPkg(dst))
        assert not a.match(DPkg(dst, track_features=''))

        a = MatchSpec(track_features='test')
        assert a.match(DPkg(dst, track_features='test'))
        assert not a.match(DPkg(dst, track_features='test2'))
        assert not a.match(DPkg(dst, track_features='test me'))
        assert not a.match(DPkg(dst, track_features='you test'))
        assert not a.match(DPkg(dst, track_features='you test me'))
        assert a.get_exact_value('track_features') == frozenset(('test',))

        b = MatchSpec(track_features='mkl')
        assert not b.match(DPkg(dst))
        assert b.match(DPkg(dst, track_features='mkl'))
        assert b.match(DPkg(dst, track_features='mkl'))
        assert not b.match(DPkg(dst, track_features='mkl debug'))
        assert not b.match(DPkg(dst, track_features='debug'))

        c = MatchSpec(track_features='nomkl')
        assert not c.match(DPkg(dst))
        assert not c.match(DPkg(dst, track_features='mkl'))
        assert c.match(DPkg(dst, track_features='nomkl'))
        assert not c.match(DPkg(dst, track_features='nomkl debug'))

        # regression test for #6860
        d = MatchSpec(track_features='')
        assert d.get_exact_value('track_features') == frozenset()
        d = MatchSpec(track_features=' ')
        assert d.get_exact_value('track_features') == frozenset()
        d = MatchSpec(track_features=('', ''))
        assert d.get_exact_value('track_features') == frozenset()
        d = MatchSpec(track_features=('', '', 'test'))
        assert d.get_exact_value('track_features') == frozenset(('test',))

    def test_bracket_matches(self):
        record = {
            'name': 'numpy',
            'version': '1.11.0',
            'build': 'py34_7',
            'build_number': 7,
        }

        assert MatchSpec("numpy<2").match(record)
        assert MatchSpec("numpy[version<2]").match(record)
        assert not MatchSpec("numpy>2").match(record)
        assert not MatchSpec("numpy[version='>2']").match(record)

        assert MatchSpec("numpy[build_number='7']").match(record)
        assert MatchSpec("numpy[build_number='<8']").match(record)
        assert not MatchSpec("numpy[build_number='>7']").match(record)
        assert MatchSpec("numpy[build_number='>=7']").match(record)

        assert MatchSpec("numpy ~=1.10").match(record)
        assert MatchSpec("numpy~=1.10").match(record)

    def test_license_match(self):
        record = {
            'name': 'numpy',
            'version': '1.11.0',
            'build': 'py34_7',
            'build_number': 7,
            'license': 'LGPLv3+',
            'license_family': 'LGPL',
        }
        assert MatchSpec("*[license_family='LGPL']").match(record)
        assert MatchSpec("*[license_family='lgpl']").match(record)
        assert MatchSpec("*[license_family='*GP*']").match(record)
        assert MatchSpec("*[license_family='*gp*']").match(record)
        assert MatchSpec("*[license_family='*GPL*']").match(record)
        assert MatchSpec("*[license_family='*gpl*']").match(record)

        assert MatchSpec("*[license='*gpl*']").match(record)
        assert MatchSpec("*[license='*v3+']").match(record)


class TestArg2Spec(TestCase):

    def test_simple(self):
        assert arg2spec('python') == 'python'
        assert arg2spec('python=2.6') == 'python=2.6'
        assert arg2spec('python=2.6*') == 'python=2.6'
        assert arg2spec('ipython=0.13.2') == 'ipython=0.13.2'
        assert arg2spec('ipython=0.13.0') == 'ipython=0.13.0'
        assert arg2spec('ipython==0.13.0') == 'ipython==0.13.0'
        assert arg2spec('foo=1.3.0=3') == 'foo==1.3.0=3'

    def test_pip_style(self):
        assert arg2spec('foo>=1.3') == "foo[version='>=1.3']"
        assert arg2spec('zope.int>=1.3,<3.0') == "zope.int[version='>=1.3,<3.0']"
        assert arg2spec('numpy >=1.9') == "numpy[version='>=1.9']"

    def test_invalid_arg2spec(self):
        with pytest.raises(CondaValueError):
            arg2spec('!xyz 1.3')


class TestSpecFromLine(TestCase):

    def cb_form(self, spec_str):
        return MatchSpec(spec_str).conda_build_form()

    def test_invalid(self):
        assert spec_from_line('=') is None
        assert spec_from_line('foo 1.0') is None

    def test_comment(self):
        assert spec_from_line('foo # comment') == 'foo' == self.cb_form('foo # comment')
        assert spec_from_line('foo ## comment') == 'foo' == self.cb_form('foo ## comment')

    def test_conda_style(self):
        assert spec_from_line('foo') == 'foo' == self.cb_form('foo')
        assert spec_from_line('foo=1.0=2') == 'foo 1.0 2' == self.cb_form('foo=1.0=2')

        # assert spec_from_line('foo=1.0*') == 'foo 1.0.*' == self.cb_form('foo=1.0*')
        # assert spec_from_line('foo=1.0|1.2') == 'foo 1.0|1.2' == self.cb_form('foo=1.0|1.2')
        # assert spec_from_line('foo=1.0') == 'foo 1.0' == self.cb_form('foo=1.0')

    def test_pip_style(self):
        assert spec_from_line('foo>=1.0') == 'foo >=1.0' == self.cb_form('foo>=1.0')
        assert spec_from_line('foo >=1.0') == 'foo >=1.0' == self.cb_form('foo >=1.0')
        assert spec_from_line('FOO-Bar >=1.0') == 'foo-bar >=1.0' == self.cb_form('FOO-Bar >=1.0')
        assert spec_from_line('foo >= 1.0') == 'foo >=1.0' == self.cb_form('foo >= 1.0')
        assert spec_from_line('foo > 1.0') == 'foo >1.0' == self.cb_form('foo > 1.0')
        assert spec_from_line('foo != 1.0') == 'foo !=1.0' == self.cb_form('foo != 1.0')
        assert spec_from_line('foo <1.0') == 'foo <1.0' == self.cb_form('foo <1.0')
        assert spec_from_line('foo >=1.0 , < 2.0') == 'foo >=1.0,<2.0' == self.cb_form('foo >=1.0 , < 2.0')


class SpecStrParsingTests(TestCase):

    def test_parse_spec_str_tarball_url(self):
        url = "https://repo.anaconda.com/pkgs/free/linux-64/_license-1.1-py27_1.tar.bz2"
        assert _parse_spec_str(url) == {
            "channel": "defaults",
            "subdir": "linux-64",
            "name": "_license",
            "version": "1.1",
            "build": "py27_1",
            "fn": "_license-1.1-py27_1.tar.bz2",
            "url": url,
        }

        url = "https://conda.anaconda.org/conda-canary/linux-64/conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2"
        assert _parse_spec_str(url) == {
            "channel": "conda-canary",
            "subdir": "linux-64",
            "name": "conda",
            "version": "4.3.21.post699+1dab973",
            "build": "py36h4a561cd_0",
            "fn": "conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2",
            "url": url,
        }

    # def test_parse_spec_str_legacy_dist_format(self):
    #     assert _parse_spec_str("numpy-1.8-py26_0") == {
    #         "name": "numpy",
    #         "version": "1.8",
    #         "build": "py26_0",
    #     }
    #     assert _parse_spec_str("numpy-1.8-py26_0[channel=defaults]") == {
    #         "channel": "defaults",
    #         "name": "numpy",
    #         "version": "1.8",
    #         "build": "py26_0",
    #     }
    #     assert _parse_spec_str("*/win-32::numpy-1.8-py26_0[channel=defaults]") == {
    #         "channel": "defaults",
    #         "subdir": "win-32",
    #         "name": "numpy",
    #         "version": "1.8",
    #         "build": "py26_0",
    #     }

    def test_parse_spec_str_no_brackets(self):
        assert _parse_spec_str("numpy") == {
            "_original_spec_str": "numpy",
            "name": "numpy",
        }
        assert _parse_spec_str("defaults::numpy") == {
            "_original_spec_str": "defaults::numpy",
            "channel": "defaults",
            "name": "numpy",
        }
        assert _parse_spec_str("https://repo.anaconda.com/pkgs/free::numpy") == {
            "_original_spec_str": "https://repo.anaconda.com/pkgs/free::numpy",
            "channel": "pkgs/free",
            "name": "numpy",
        }
        assert _parse_spec_str("defaults::numpy=1.8") == {
            "_original_spec_str": "defaults::numpy=1.8",
            "channel": "defaults",
            "name": "numpy",
            "version": "1.8*",
        }
        assert _parse_spec_str("defaults::numpy =1.8") == {
            "_original_spec_str": "defaults::numpy =1.8",
            "channel": "defaults",
            "name": "numpy",
            "version": "1.8*",
        }
        assert _parse_spec_str("defaults::numpy=1.8=py27_0") == {
            "_original_spec_str": "defaults::numpy=1.8=py27_0",
            "channel": "defaults",
            "name": "numpy",
            "version": "1.8",
            "build": "py27_0",
        }
        assert _parse_spec_str("defaults::numpy 1.8 py27_0") == {
            "_original_spec_str": "defaults::numpy 1.8 py27_0",
            "channel": "defaults",
            "name": "numpy",
            "version": "1.8",
            "build": "py27_0",
        }

    def test_parse_spec_str_with_brackets(self):
        assert _parse_spec_str("defaults::numpy[channel=anaconda]") == {
            "_original_spec_str": "defaults::numpy[channel=anaconda]",
            "channel": "anaconda",
            "name": "numpy",
        }
        assert _parse_spec_str("defaults::numpy 1.8 py27_0[channel=anaconda]") == {
            "_original_spec_str": "defaults::numpy 1.8 py27_0[channel=anaconda]",
            "channel": "anaconda",
            "name": "numpy",
            "version": "1.8",
            "build": "py27_0",
        }
        assert _parse_spec_str("defaults::numpy=1.8=py27_0 [channel=anaconda,version=1.9, build=3]") == {
            "_original_spec_str": "defaults::numpy=1.8=py27_0 [channel=anaconda,version=1.9, build=3]",
            "channel": "anaconda",
            "name": "numpy",
            "version": "1.9",
            "build": "3",
        }
        assert _parse_spec_str('defaults::numpy=1.8=py27_0 [channel=\'anaconda\',version=">=1.8,<2|1.9", build=\'3\']') == {
            "_original_spec_str": 'defaults::numpy=1.8=py27_0 [channel=\'anaconda\',version=">=1.8,<2|1.9", build=\'3\']',
            "channel": "anaconda",
            "name": "numpy",
            "version": ">=1.8,<2|1.9",
            "build": "3",
        }

    def test_star_name(self):
        assert _parse_spec_str("* 2.7.4") == {
            "_original_spec_str": "* 2.7.4",
            "name": "*",
            "version": "2.7.4",
        }
        assert _parse_spec_str("* >=1.3 2") == {
            "_original_spec_str": "* >=1.3 2",
            "name": "*",
            "version": ">=1.3",
            "build": "2",
        }

    def test_parse_equal_equal(self):
        assert _parse_spec_str("numpy==1.7") == {
            "_original_spec_str": "numpy==1.7",
            "name": "numpy",
            "version": "1.7",
        }
        assert _parse_spec_str("numpy ==1.7") == {
            "_original_spec_str": "numpy ==1.7",
            "name": "numpy",
            "version": "1.7",
        }
        assert _parse_spec_str("numpy=1.7") == {
            "_original_spec_str": "numpy=1.7",
            "name": "numpy",
            "version": "1.7*",
        }
        assert _parse_spec_str("numpy =1.7") == {
            "_original_spec_str": "numpy =1.7",
            "name": "numpy",
            "version": "1.7*",
        }
        assert _parse_spec_str("numpy !=1.7") == {
            "_original_spec_str": "numpy !=1.7",
            "name": "numpy",
            "version": "!=1.7",
        }

    def test_parse_hard(self):
        assert _parse_spec_str("numpy~=1.7.1") == {
            "_original_spec_str": "numpy~=1.7.1",
            "name": "numpy",
            "version": "~=1.7.1",
        }
        assert _parse_spec_str("numpy>1.8,<2|==1.7") == {
            "_original_spec_str": "numpy>1.8,<2|==1.7",
            "name": "numpy",
            "version": ">1.8,<2|==1.7",
        }
        assert _parse_spec_str("numpy >1.8,<2|==1.7,!=1.9,~=1.7.1 py34_0") == {
            "_original_spec_str": "numpy >1.8,<2|==1.7,!=1.9,~=1.7.1 py34_0",
            "name": "numpy",
            "version": ">1.8,<2|==1.7,!=1.9,~=1.7.1",
            "build": "py34_0",
        }
        assert _parse_spec_str("*>1.8,<2|==1.7") == {
            "_original_spec_str": "*>1.8,<2|==1.7",
            "name": "*",
            "version": ">1.8,<2|==1.7",
        }
        assert _parse_spec_str("* >1.8,<2|==1.7") == {
            "_original_spec_str": "* >1.8,<2|==1.7",
            "name": "*",
            "version": ">1.8,<2|==1.7",
        }

        assert _parse_spec_str("* 1 *") == {
            "_original_spec_str": "* 1 *",
            "name": "*",
            "version": "1",
            "build": "*",
        }
        assert _parse_spec_str("* * openblas_0") == {
            "_original_spec_str": "* * openblas_0",
            "name": "*",
            "version": "*",
            "build": "openblas_0",
        }
        assert _parse_spec_str("* * *") == {
            "_original_spec_str": "* * *",
            "name": "*",
            "version": "*",
            "build": "*",
        }
        assert _parse_spec_str("* *") == {
            "_original_spec_str": "* *",
            "name": "*",
            "version": "*",
        }

    def test_parse_errors(self):
        with pytest.raises(InvalidMatchSpec):
            _parse_spec_str('!xyz 1.3')

    def test_parse_channel_subdir(self):
        assert _parse_spec_str("conda-forge::foo>=1.0") == {
            "_original_spec_str": "conda-forge::foo>=1.0",
            "channel": "conda-forge",
            "name": "foo",
            "version": ">=1.0",
        }

        assert _parse_spec_str("conda-forge/linux-32::foo>=1.0") == {
            "_original_spec_str": "conda-forge/linux-32::foo>=1.0",
            "channel": "conda-forge",
            "subdir": "linux-32",
            "name": "foo",
            "version": ">=1.0",
        }

        assert _parse_spec_str("*/linux-32::foo>=1.0") == {
            "_original_spec_str": "*/linux-32::foo>=1.0",
            "channel": "*",
            "subdir": "linux-32",
            "name": "foo",
            "version": ">=1.0",
        }

    def test_parse_parens(self):
        assert _parse_spec_str("conda-forge::foo[build=3](target=blarg,optional)") == {
            "_original_spec_str": "conda-forge::foo[build=3](target=blarg,optional)",
            "channel": "conda-forge",
            "name": "foo",
            "build": "3",
            # "target": "blarg",  # suppressing these for now
            # "optional": True,
        }

    def test_parse_build_number_brackets(self):
        assert _parse_spec_str("python[build_number=3]") == {
            "_original_spec_str": "python[build_number=3]",
            "name": "python",
            "build_number": '3',
        }
        assert _parse_spec_str("python[build_number='>3']") == {
            "_original_spec_str": "python[build_number='>3']",
            "name": "python",
            "build_number": '>3',
        }
        assert _parse_spec_str("python[build_number='>=3']") == {
            "_original_spec_str": "python[build_number='>=3']",
            "name": "python",
            "build_number": '>=3',
        }

        assert _parse_spec_str("python[build_number='<3']") == {
            "_original_spec_str": "python[build_number='<3']",
            "name": "python",
            "build_number": '<3',
        }
        assert _parse_spec_str("python[build_number='<=3']") == {
            "_original_spec_str": "python[build_number='<=3']",
            "name": "python",
            "build_number": '<=3',
        }

        # # these don't work right now, should they?
        # assert _parse_spec_str("python[build_number<3]") == {
        #     "name": "python",
        #     "build_number": '<3',
        # }
        # assert _parse_spec_str("python[build_number<=3]") == {
        #     "name": "python",
        #     "build_number": '<=3',
        # }

        # # these don't work right now, should they?
        # assert _parse_spec_str("python[build_number>3]") == {
        #     "name": "python",
        #     "build_number": '>3',
        # }
        # assert _parse_spec_str("python[build_number>=3]") == {
        #     "name": "python",
        #     "build_number": '>=3',
        # }


class MatchSpecMergeTests(TestCase):

    def test_merge_single_name(self):
        specs = (MatchSpec('exact'), MatchSpec('exact 1.2.3 1'), MatchSpec('exact >1.0,<2'))
        merged_specs = MatchSpec.merge(specs)
        print(merged_specs)
        assert len(merged_specs) == 1
        merged_spec = merged_specs[0]
        print(merged_spec)
        assert str(merged_spec) == "exact[version='1.2.3,>1.0,<2',build=1]"
        assert merged_spec.match({
            'name': 'exact',
            'version': '1.2.3',
            'build': '1',
            'build_number': 1,
        })
        assert not merged_spec.match({
            'name': 'exact',
            'version': '1.2.2',
            'build': '1',
            'build_number': 1,
        })

        specs = (MatchSpec('exact 1.2.3 1'), MatchSpec('exact 1.2.3 2'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

        merged_specs = MatchSpec.merge((MatchSpec('exact 1.2.3 1'),))
        assert len(merged_specs) == 1
        assert str(merged_specs[0]) == "exact==1.2.3=1"

    def test_merge_multiple_name(self):
        specs = tuple(MatchSpec(s) for s in (
            'exact', 'exact 1.2.3 1',
            'bounded >=1.0,<2.0', 'bounded >=1.5', 'bounded <=1.8',
            'exact >1.0,<2',
        ))
        merged_specs = MatchSpec.merge(specs)
        print(merged_specs)
        assert len(merged_specs) == 2

        exact_spec = next(s for s in merged_specs if s.name == 'exact')
        bounded_spec = next(s for s in merged_specs if s.name == 'bounded')

        assert str(exact_spec) == "exact[version='1.2.3,>1.0,<2',build=1]"
        assert str(bounded_spec) == "bounded[version='>=1.0,<2.0,>=1.5,<=1.8']"

        assert not bounded_spec.match({
            'name': 'bounded',
            'version': '1',
            'build': '6',
            'build_number': 6,
        })
        assert bounded_spec.match({
            'name': 'bounded',
            'version': '1.5',
            'build': '7',
            'build_number': 7,
        })
        assert not bounded_spec.match({
            'name': 'bounded',
            'version': '2',
            'build': '8',
            'build_number': 8,
        })

    def test_channel_merge(self):
        specs = (MatchSpec('pkgs/main::python'), MatchSpec('defaults::python'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

        specs = (MatchSpec('defaults::python'), MatchSpec('pkgs/main::python'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

        specs = (MatchSpec('defaults::python'), MatchSpec('defaults::python 1.2.3'))
        merged = MatchSpec.merge(specs)
        assert len(merged) == 1
        assert str(merged[0]) == "defaults::python==1.2.3"

        specs = (MatchSpec('pkgs/free::python'), MatchSpec('pkgs/free::python 1.2.3'))
        merged = MatchSpec.merge(specs)
        assert len(merged) == 1
        assert str(merged[0]) == "pkgs/free::python==1.2.3"

    def test_subdir_merge(self):
        specs = (MatchSpec('pkgs/main/linux-64::python'), MatchSpec('pkgs/main/linux-32::python'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

        specs = (MatchSpec('defaults/win-32::python'), MatchSpec('defaults/win-64::python'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

        specs = (MatchSpec('pkgs/free/linux-64::python'), MatchSpec('pkgs/free::python 1.2.3'))
        merged = MatchSpec.merge(specs)
        assert len(merged) == 1
        assert str(merged[0]) == "pkgs/free/linux-64::python==1.2.3"
        assert merged[0] == MatchSpec(channel='pkgs/free', subdir='linux-64', name='python', version='1.2.3')

    def test_build_merge(self):
        specs = (MatchSpec('python[build=py27_1]'), MatchSpec('python=1.2.3=py27_1'), MatchSpec('conda-forge::python<=8'))
        merged = MatchSpec.merge(specs)
        assert len(merged) == 1
        assert str(merged[0]) == "conda-forge::python[version='1.2.3,<=8',build=py27_1]"

        specs = (MatchSpec('python[build=py27_1]'), MatchSpec('python=1.2.3=1'), MatchSpec('conda-forge::python<=8[build=py27_1]'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

    def test_build_number_merge(self):
        specs = (MatchSpec('python[build_number=1]'), MatchSpec('python=1.2.3=py27_7'), MatchSpec('conda-forge::python<=8[build_number=1]'))
        merged = MatchSpec.merge(specs)
        assert len(merged) == 1
        assert str(merged[0]) == "conda-forge::python[version='1.2.3,<=8',build=py27_7,build_number=1]"

        specs = (MatchSpec('python[build_number=2]'), MatchSpec('python=1.2.3=py27_7'), MatchSpec('python<=8[build_number=1]'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

    def test_md5_merge_with_name(self):
        specs = (MatchSpec('python[md5=deadbeef]'), MatchSpec('python=1.2.3'), MatchSpec('conda-forge::python[md5=deadbeef]'))
        merged = MatchSpec.merge(specs)
        assert len(merged) == 1
        assert str(merged[0]) == "conda-forge::python=1.2.3[md5=deadbeef]"

        specs = (MatchSpec('python[md5=FFBADD11]'), MatchSpec('python=1.2.3'), MatchSpec('python[md5=ffbadd11]'))
        with pytest.raises(ValueError):
            MatchSpec.merge(specs)

    def test_md5_merge_wo_name(self):
        specs = (MatchSpec('*[md5=deadbeef]'), MatchSpec('*[md5=FFBADD11]'))
        merged = MatchSpec.merge(specs)
        assert len(merged) == 2
        str_specs = ('*[md5=deadbeef]', '*[md5=FFBADD11]')
        assert str(merged[0]) in str_specs
        assert str(merged[1]) in str_specs
        assert str(merged[0]) != str(merged[1])
