# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

import itertools
import operator

import pytest

from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
from packaging.version import Version, parse

from .test_version import VERSIONS

LEGACY_SPECIFIERS = [
    "==2.1.0.3",
    "!=2.2.0.5",
    "<=5",
    ">=7.9a1",
    "<1.0.dev1",
    ">2.0.post1",
]

SPECIFIERS = [
    "~=2.0",
    "==2.1.*",
    "==2.1.0.3",
    "!=2.2.*",
    "!=2.2.0.5",
    "<=5",
    ">=7.9a1",
    "<1.0.dev1",
    ">2.0.post1",
]


class TestSpecifier:
    @pytest.mark.parametrize("specifier", SPECIFIERS)
    def test_specifiers_valid(self, specifier):
        Specifier(specifier)

    @pytest.mark.parametrize(
        "specifier",
        [
            # Operator-less specifier
            "2.0",
            # Invalid operator
            "=>2.0",
            # Version-less specifier
            "==",
            # Local segment on operators which don't support them
            "~=1.0+5",
            ">=1.0+deadbeef",
            "<=1.0+abc123",
            ">1.0+watwat",
            "<1.0+1.0",
            # Prefix matching on operators which don't support them
            "~=1.0.*",
            ">=1.0.*",
            "<=1.0.*",
            ">1.0.*",
            "<1.0.*",
            # Combination of local and prefix matching on operators which do
            # support one or the other
            "==1.0.*+5",
            "!=1.0.*+deadbeef",
            # Prefix matching cannot be used with a pre-release, post-release,
            # dev or local version
            "==2.0a1.*",
            "!=2.0a1.*",
            "==2.0.post1.*",
            "!=2.0.post1.*",
            "==2.0.dev1.*",
            "!=2.0.dev1.*",
            "==1.0+5.*",
            "!=1.0+deadbeef.*",
            # Prefix matching must appear at the end
            "==1.0.*.5",
            # Compatible operator requires 2 digits in the release operator
            "~=1",
            # Cannot use a prefix matching after a .devN version
            "==1.0.dev1.*",
            "!=1.0.dev1.*",
        ],
    )
    def test_specifiers_invalid(self, specifier):
        with pytest.raises(InvalidSpecifier):
            Specifier(specifier)

    @pytest.mark.parametrize(
        "version",
        [
            # Various development release incarnations
            "1.0dev",
            "1.0.dev",
            "1.0dev1",
            "1.0-dev",
            "1.0-dev1",
            "1.0DEV",
            "1.0.DEV",
            "1.0DEV1",
            "1.0.DEV1",
            "1.0-DEV",
            "1.0-DEV1",
            # Various alpha incarnations
            "1.0a",
            "1.0.a",
            "1.0.a1",
            "1.0-a",
            "1.0-a1",
            "1.0alpha",
            "1.0.alpha",
            "1.0.alpha1",
            "1.0-alpha",
            "1.0-alpha1",
            "1.0A",
            "1.0.A",
            "1.0.A1",
            "1.0-A",
            "1.0-A1",
            "1.0ALPHA",
            "1.0.ALPHA",
            "1.0.ALPHA1",
            "1.0-ALPHA",
            "1.0-ALPHA1",
            # Various beta incarnations
            "1.0b",
            "1.0.b",
            "1.0.b1",
            "1.0-b",
            "1.0-b1",
            "1.0beta",
            "1.0.beta",
            "1.0.beta1",
            "1.0-beta",
            "1.0-beta1",
            "1.0B",
            "1.0.B",
            "1.0.B1",
            "1.0-B",
            "1.0-B1",
            "1.0BETA",
            "1.0.BETA",
            "1.0.BETA1",
            "1.0-BETA",
            "1.0-BETA1",
            # Various release candidate incarnations
            "1.0c",
            "1.0.c",
            "1.0.c1",
            "1.0-c",
            "1.0-c1",
            "1.0rc",
            "1.0.rc",
            "1.0.rc1",
            "1.0-rc",
            "1.0-rc1",
            "1.0C",
            "1.0.C",
            "1.0.C1",
            "1.0-C",
            "1.0-C1",
            "1.0RC",
            "1.0.RC",
            "1.0.RC1",
            "1.0-RC",
            "1.0-RC1",
            # Various post release incarnations
            "1.0post",
            "1.0.post",
            "1.0post1",
            "1.0-post",
            "1.0-post1",
            "1.0POST",
            "1.0.POST",
            "1.0POST1",
            "1.0.POST1",
            "1.0-POST",
            "1.0-POST1",
            "1.0-5",
            # Local version case insensitivity
            "1.0+AbC"
            # Integer Normalization
            "1.01",
            "1.0a05",
            "1.0b07",
            "1.0c056",
            "1.0rc09",
            "1.0.post000",
            "1.1.dev09000",
            "00!1.2",
            "0100!0.0",
            # Various other normalizations
            "v1.0",
            "  \r \f \v v1.0\t\n",
        ],
    )
    def test_specifiers_normalized(self, version):
        if "+" not in version:
            ops = ["~=", "==", "!=", "<=", ">=", "<", ">"]
        else:
            ops = ["==", "!="]

        for op in ops:
            Specifier(op + version)

    @pytest.mark.parametrize(
        ("specifier", "expected"),
        [
            # Single item specifiers should just be reflexive
            ("!=2.0", "!=2.0"),
            ("<2.0", "<2.0"),
            ("<=2.0", "<=2.0"),
            ("==2.0", "==2.0"),
            (">2.0", ">2.0"),
            (">=2.0", ">=2.0"),
            ("~=2.0", "~=2.0"),
            # Spaces should be removed
            ("< 2", "<2"),
        ],
    )
    def test_specifiers_str_and_repr(self, specifier, expected):
        spec = Specifier(specifier)

        assert str(spec) == expected
        assert repr(spec) == f"<Specifier({expected!r})>"

    @pytest.mark.parametrize("specifier", SPECIFIERS)
    def test_specifiers_hash(self, specifier):
        assert hash(Specifier(specifier)) == hash(Specifier(specifier))

    @pytest.mark.parametrize(
        ("left", "right", "op"),
        itertools.chain.from_iterable(
            # Verify that the equal (==) operator works correctly
            [[(x, x, operator.eq) for x in SPECIFIERS]]
            +
            # Verify that the not equal (!=) operator works correctly
            [
                [(x, y, operator.ne) for j, y in enumerate(SPECIFIERS) if i != j]
                for i, x in enumerate(SPECIFIERS)
            ]
        ),
    )
    def test_comparison_true(self, left, right, op):
        assert op(Specifier(left), Specifier(right))
        assert op(left, Specifier(right))
        assert op(Specifier(left), right)

    @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")])
    def test_comparison_canonicalizes(self, left, right):
        assert Specifier(left) == Specifier(right)
        assert left == Specifier(right)
        assert Specifier(left) == right

    @pytest.mark.parametrize(
        ("left", "right", "op"),
        itertools.chain.from_iterable(
            # Verify that the equal (==) operator works correctly
            [[(x, x, operator.ne) for x in SPECIFIERS]]
            +
            # Verify that the not equal (!=) operator works correctly
            [
                [(x, y, operator.eq) for j, y in enumerate(SPECIFIERS) if i != j]
                for i, x in enumerate(SPECIFIERS)
            ]
        ),
    )
    def test_comparison_false(self, left, right, op):
        assert not op(Specifier(left), Specifier(right))
        assert not op(left, Specifier(right))
        assert not op(Specifier(left), right)

    def test_comparison_non_specifier(self):
        assert Specifier("==1.0") != 12
        assert not Specifier("==1.0") == 12
        assert Specifier("==1.0") != "12"
        assert not Specifier("==1.0") == "12"

    @pytest.mark.parametrize(
        ("version", "spec", "expected"),
        [
            (v, s, True)
            for v, s in [
                # Test the equality operation
                ("2.0", "==2"),
                ("2.0", "==2.0"),
                ("2.0", "==2.0.0"),
                ("2.0+deadbeef", "==2"),
                ("2.0+deadbeef", "==2.0"),
                ("2.0+deadbeef", "==2.0.0"),
                ("2.0+deadbeef", "==2+deadbeef"),
                ("2.0+deadbeef", "==2.0+deadbeef"),
                ("2.0+deadbeef", "==2.0.0+deadbeef"),
                ("2.0+deadbeef.0", "==2.0.0+deadbeef.00"),
                # Test the equality operation with a prefix
                ("2.dev1", "==2.*"),
                ("2a1", "==2.*"),
                ("2a1.post1", "==2.*"),
                ("2b1", "==2.*"),
                ("2b1.dev1", "==2.*"),
                ("2c1", "==2.*"),
                ("2c1.post1.dev1", "==2.*"),
                ("2c1.post1.dev1", "==2.0.*"),
                ("2rc1", "==2.*"),
                ("2rc1", "==2.0.*"),
                ("2", "==2.*"),
                ("2", "==2.0.*"),
                ("2", "==0!2.*"),
                ("0!2", "==2.*"),
                ("2.0", "==2.*"),
                ("2.0.0", "==2.*"),
                ("2.1+local.version", "==2.1.*"),
                # Test the in-equality operation
                ("2.1", "!=2"),
                ("2.1", "!=2.0"),
                ("2.0.1", "!=2"),
                ("2.0.1", "!=2.0"),
                ("2.0.1", "!=2.0.0"),
                ("2.0", "!=2.0+deadbeef"),
                # Test the in-equality operation with a prefix
                ("2.0", "!=3.*"),
                ("2.1", "!=2.0.*"),
                # Test the greater than equal operation
                ("2.0", ">=2"),
                ("2.0", ">=2.0"),
                ("2.0", ">=2.0.0"),
                ("2.0.post1", ">=2"),
                ("2.0.post1.dev1", ">=2"),
                ("3", ">=2"),
                ("3.0.0a8", ">=3.0.0a7"),
                # Test the less than equal operation
                ("2.0", "<=2"),
                ("2.0", "<=2.0"),
                ("2.0", "<=2.0.0"),
                ("2.0.dev1", "<=2"),
                ("2.0a1", "<=2"),
                ("2.0a1.dev1", "<=2"),
                ("2.0b1", "<=2"),
                ("2.0b1.post1", "<=2"),
                ("2.0c1", "<=2"),
                ("2.0c1.post1.dev1", "<=2"),
                ("2.0rc1", "<=2"),
                ("1", "<=2"),
                ("3.0.0a7", "<=3.0.0a8"),
                # Test the greater than operation
                ("3", ">2"),
                ("2.1", ">2.0"),
                ("2.0.1", ">2"),
                ("2.1.post1", ">2"),
                ("2.1+local.version", ">2"),
                ("3.0.0a8", ">3.0.0a7"),
                # Test the less than operation
                ("1", "<2"),
                ("2.0", "<2.1"),
                ("2.0.dev0", "<2.1"),
                ("3.0.0a7", "<3.0.0a8"),
                # Test the compatibility operation
                ("1", "~=1.0"),
                ("1.0.1", "~=1.0"),
                ("1.1", "~=1.0"),
                ("1.9999999", "~=1.0"),
                ("1.1", "~=1.0a1"),
                ("2022.01.01", "~=2022.01.01"),
                # Test that epochs are handled sanely
                ("2!1.0", "~=2!1.0"),
                ("2!1.0", "==2!1.*"),
                ("2!1.0", "==2!1.0"),
                ("2!1.0", "!=1.0"),
                ("2!1.0.0", "==2!1.0.0.0.*"),
                ("2!1.0.0", "==2!1.0.*"),
                ("2!1.0.0", "==2!1.*"),
                ("1.0", "!=2!1.0"),
                ("1.0", "<=2!0.1"),
                ("2!1.0", ">=2.0"),
                ("1.0", "<2!0.1"),
                ("2!1.0", ">2.0"),
                # Test some normalization rules
                ("2.0.5", ">2.0dev"),
            ]
        ]
        + [
            (v, s, False)
            for v, s in [
                # Test the equality operation
                ("2.1", "==2"),
                ("2.1", "==2.0"),
                ("2.1", "==2.0.0"),
                ("2.0", "==2.0+deadbeef"),
                # Test the equality operation with a prefix
                ("2.0", "==3.*"),
                ("2.1", "==2.0.*"),
                # Test the in-equality operation
                ("2.0", "!=2"),
                ("2.0", "!=2.0"),
                ("2.0", "!=2.0.0"),
                ("2.0+deadbeef", "!=2"),
                ("2.0+deadbeef", "!=2.0"),
                ("2.0+deadbeef", "!=2.0.0"),
                ("2.0+deadbeef", "!=2+deadbeef"),
                ("2.0+deadbeef", "!=2.0+deadbeef"),
                ("2.0+deadbeef", "!=2.0.0+deadbeef"),
                ("2.0+deadbeef.0", "!=2.0.0+deadbeef.00"),
                # Test the in-equality operation with a prefix
                ("2.dev1", "!=2.*"),
                ("2a1", "!=2.*"),
                ("2a1.post1", "!=2.*"),
                ("2b1", "!=2.*"),
                ("2b1.dev1", "!=2.*"),
                ("2c1", "!=2.*"),
                ("2c1.post1.dev1", "!=2.*"),
                ("2c1.post1.dev1", "!=2.0.*"),
                ("2rc1", "!=2.*"),
                ("2rc1", "!=2.0.*"),
                ("2", "!=2.*"),
                ("2", "!=2.0.*"),
                ("2.0", "!=2.*"),
                ("2.0.0", "!=2.*"),
                # Test the greater than equal operation
                ("2.0.dev1", ">=2"),
                ("2.0a1", ">=2"),
                ("2.0a1.dev1", ">=2"),
                ("2.0b1", ">=2"),
                ("2.0b1.post1", ">=2"),
                ("2.0c1", ">=2"),
                ("2.0c1.post1.dev1", ">=2"),
                ("2.0rc1", ">=2"),
                ("1", ">=2"),
                # Test the less than equal operation
                ("2.0.post1", "<=2"),
                ("2.0.post1.dev1", "<=2"),
                ("3", "<=2"),
                # Test the greater than operation
                ("1", ">2"),
                ("2.0.dev1", ">2"),
                ("2.0a1", ">2"),
                ("2.0a1.post1", ">2"),
                ("2.0b1", ">2"),
                ("2.0b1.dev1", ">2"),
                ("2.0c1", ">2"),
                ("2.0c1.post1.dev1", ">2"),
                ("2.0rc1", ">2"),
                ("2.0", ">2"),
                ("2.0.post1", ">2"),
                ("2.0.post1.dev1", ">2"),
                ("2.0+local.version", ">2"),
                # Test the less than operation
                ("2.0.dev1", "<2"),
                ("2.0a1", "<2"),
                ("2.0a1.post1", "<2"),
                ("2.0b1", "<2"),
                ("2.0b2.dev1", "<2"),
                ("2.0c1", "<2"),
                ("2.0c1.post1.dev1", "<2"),
                ("2.0rc1", "<2"),
                ("2.0", "<2"),
                ("2.post1", "<2"),
                ("2.post1.dev1", "<2"),
                ("3", "<2"),
                # Test the compatibility operation
                ("2.0", "~=1.0"),
                ("1.1.0", "~=1.0.0"),
                ("1.1.post1", "~=1.0.0"),
                # Test that epochs are handled sanely
                ("1.0", "~=2!1.0"),
                ("2!1.0", "~=1.0"),
                ("2!1.0", "==1.0"),
                ("1.0", "==2!1.0"),
                ("2!1.0", "==1.0.0.*"),
                ("1.0", "==2!1.0.0.*"),
                ("2!1.0", "==1.*"),
                ("1.0", "==2!1.*"),
                ("2!1.0", "!=2!1.0"),
            ]
        ],
    )
    def test_specifiers(self, version, spec, expected):
        spec = Specifier(spec, prereleases=True)

        if expected:
            # Test that the plain string form works
            assert version in spec
            assert spec.contains(version)

            # Test that the version instance form works
            assert Version(version) in spec
            assert spec.contains(Version(version))
        else:
            # Test that the plain string form works
            assert version not in spec
            assert not spec.contains(version)

            # Test that the version instance form works
            assert Version(version) not in spec
            assert not spec.contains(Version(version))

    @pytest.mark.parametrize(
        ("version", "spec", "expected"),
        [
            ("1.0.0", "===1.0", False),
            ("1.0.dev0", "===1.0", False),
            # Test identity comparison by itself
            ("1.0", "===1.0", True),
            ("1.0.dev0", "===1.0.dev0", True),
        ],
    )
    def test_specifiers_identity(self, version, spec, expected):
        spec = Specifier(spec)

        if expected:
            # Identity comparisons only support the plain string form
            assert version in spec
        else:
            # Identity comparisons only support the plain string form
            assert version not in spec

    @pytest.mark.parametrize(
        ("specifier", "expected"),
        [
            ("==1.0", False),
            (">=1.0", False),
            ("<=1.0", False),
            ("~=1.0", False),
            ("<1.0", False),
            (">1.0", False),
            ("<1.0.dev1", True),
            (">1.0.dev1", True),
            ("!=1.0.dev1", False),
            ("==1.0.*", False),
            ("==1.0.dev1", True),
            (">=1.0.dev1", True),
            ("<=1.0.dev1", True),
            ("~=1.0.dev1", True),
        ],
    )
    def test_specifier_prereleases_detection(self, specifier, expected):
        assert Specifier(specifier).prereleases == expected

    @pytest.mark.parametrize(
        ("specifier", "version", "expected"),
        [
            (">=1.0", "2.0.dev1", False),
            (">=2.0.dev1", "2.0a1", True),
            ("==2.0.*", "2.0a1.dev1", False),
            ("<=2.0", "1.0.dev1", False),
            ("<=2.0.dev1", "1.0a1", True),
        ],
    )
    def test_specifiers_prereleases(self, specifier, version, expected):
        spec = Specifier(specifier)

        if expected:
            assert version in spec
            spec.prereleases = False
            assert version not in spec
        else:
            assert version not in spec
            spec.prereleases = True
            assert version in spec

    @pytest.mark.parametrize(
        ("specifier", "prereleases", "input", "expected"),
        [
            (">=1.0", None, ["2.0a1"], ["2.0a1"]),
            (">=1.0.dev1", None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
            (">=1.0.dev1", False, ["1.0", "2.0a1"], ["1.0"]),
            ("!=2.0a1", None, ["1.0a2", "1.0", "2.0a1"], ["1.0"]),
            ("==2.0a1", None, ["2.0a1"], ["2.0a1"]),
            (">2.0a1", None, ["2.0a1", "3.0a2", "3.0"], ["3.0a2", "3.0"]),
            ("<2.0a1", None, ["1.0a2", "1.0", "2.0a1"], ["1.0a2", "1.0"]),
            ("~=2.0a1", None, ["1.0", "2.0a1", "3.0a2", "3.0"], ["2.0a1"]),
        ],
    )
    def test_specifier_filter(self, specifier, prereleases, input, expected):
        spec = Specifier(specifier)

        kwargs = {"prereleases": prereleases} if prereleases is not None else {}

        assert list(spec.filter(input, **kwargs)) == expected

    @pytest.mark.parametrize(
        ("spec", "op"),
        [
            ("~=2.0", "~="),
            ("==2.1.*", "=="),
            ("==2.1.0.3", "=="),
            ("!=2.2.*", "!="),
            ("!=2.2.0.5", "!="),
            ("<=5", "<="),
            (">=7.9a1", ">="),
            ("<1.0.dev1", "<"),
            (">2.0.post1", ">"),
            # === is an escape hatch in PEP 440
            ("===lolwat", "==="),
        ],
    )
    def test_specifier_operator_property(self, spec, op):
        assert Specifier(spec).operator == op

    @pytest.mark.parametrize(
        ("spec", "version"),
        [
            ("~=2.0", "2.0"),
            ("==2.1.*", "2.1.*"),
            ("==2.1.0.3", "2.1.0.3"),
            ("!=2.2.*", "2.2.*"),
            ("!=2.2.0.5", "2.2.0.5"),
            ("<=5", "5"),
            (">=7.9a1", "7.9a1"),
            ("<1.0.dev1", "1.0.dev1"),
            (">2.0.post1", "2.0.post1"),
            # === is an escape hatch in PEP 440
            ("===lolwat", "lolwat"),
        ],
    )
    def test_specifier_version_property(self, spec, version):
        assert Specifier(spec).version == version

    @pytest.mark.parametrize(
        ("spec", "expected_length"),
        [("", 0), ("==2.0", 1), (">=2.0", 1), (">=2.0,<3", 2), (">=2.0,<3,==2.4", 3)],
    )
    def test_length(self, spec, expected_length):
        spec = SpecifierSet(spec)
        assert len(spec) == expected_length

    @pytest.mark.parametrize(
        ("spec", "expected_items"),
        [
            ("", []),
            ("==2.0", ["==2.0"]),
            (">=2.0", [">=2.0"]),
            (">=2.0,<3", [">=2.0", "<3"]),
            (">=2.0,<3,==2.4", [">=2.0", "<3", "==2.4"]),
        ],
    )
    def test_iteration(self, spec, expected_items):
        spec = SpecifierSet(spec)
        items = {str(item) for item in spec}
        assert items == set(expected_items)

    def test_specifier_equal_for_compatible_operator(self):
        assert Specifier("~=1.18.0") != Specifier("~=1.18")

    def test_specifier_hash_for_compatible_operator(self):
        assert hash(Specifier("~=1.18.0")) != hash(Specifier("~=1.18"))


class TestSpecifierSet:
    @pytest.mark.parametrize("version", VERSIONS)
    def test_empty_specifier(self, version):
        spec = SpecifierSet(prereleases=True)

        assert version in spec
        assert spec.contains(version)
        assert parse(version) in spec
        assert spec.contains(parse(version))

    def test_create_from_specifiers(self):
        spec_strs = [">=1.0", "!=1.1", "!=1.2", "<2.0"]
        specs = [Specifier(s) for s in spec_strs]
        spec = SpecifierSet(iter(specs))
        assert set(spec) == set(specs)

    def test_specifier_prereleases_explicit(self):
        spec = SpecifierSet()
        assert not spec.prereleases
        assert "1.0.dev1" not in spec
        assert not spec.contains("1.0.dev1")
        spec.prereleases = True
        assert spec.prereleases
        assert "1.0.dev1" in spec
        assert spec.contains("1.0.dev1")

        spec = SpecifierSet(prereleases=True)
        assert spec.prereleases
        assert "1.0.dev1" in spec
        assert spec.contains("1.0.dev1")
        spec.prereleases = False
        assert not spec.prereleases
        assert "1.0.dev1" not in spec
        assert not spec.contains("1.0.dev1")

        spec = SpecifierSet(prereleases=True)
        assert spec.prereleases
        assert "1.0.dev1" in spec
        assert spec.contains("1.0.dev1")
        spec.prereleases = None
        assert not spec.prereleases
        assert "1.0.dev1" not in spec
        assert not spec.contains("1.0.dev1")

    def test_specifier_contains_prereleases(self):
        spec = SpecifierSet()
        assert spec.prereleases is None
        assert not spec.contains("1.0.dev1")
        assert spec.contains("1.0.dev1", prereleases=True)

        spec = SpecifierSet(prereleases=True)
        assert spec.prereleases
        assert spec.contains("1.0.dev1")
        assert not spec.contains("1.0.dev1", prereleases=False)

    def test_specifier_contains_installed_prereleases(self):
        spec = SpecifierSet("~=1.0")
        assert not spec.contains("1.0.0.dev1", installed=True)
        assert spec.contains("1.0.0.dev1", prereleases=True, installed=True)

        spec = SpecifierSet("~=1.0", prereleases=True)
        assert spec.contains("1.0.0.dev1", installed=True)
        assert not spec.contains("1.0.0.dev1", prereleases=False, installed=False)

    @pytest.mark.parametrize(
        ("specifier", "specifier_prereleases", "prereleases", "input", "expected"),
        [
            # General test of the filter method
            ("", None, None, ["1.0", "2.0a1"], ["1.0"]),
            (">=1.0.dev1", None, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
            ("", None, None, ["1.0a1"], ["1.0a1"]),
            ("", None, None, ["1.0", Version("2.0")], ["1.0", Version("2.0")]),
            # Test overriding with the prereleases parameter on filter
            ("", None, False, ["1.0a1"], []),
            (">=1.0.dev1", None, False, ["1.0", "2.0a1"], ["1.0"]),
            ("", None, True, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
            # Test overriding with the overall specifier
            ("", True, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
            ("", False, None, ["1.0", "2.0a1"], ["1.0"]),
            (">=1.0.dev1", True, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
            (">=1.0.dev1", False, None, ["1.0", "2.0a1"], ["1.0"]),
            ("", True, None, ["1.0a1"], ["1.0a1"]),
            ("", False, None, ["1.0a1"], []),
        ],
    )
    def test_specifier_filter(
        self, specifier_prereleases, specifier, prereleases, input, expected
    ):
        if specifier_prereleases is None:
            spec = SpecifierSet(specifier)
        else:
            spec = SpecifierSet(specifier, prereleases=specifier_prereleases)

        kwargs = {"prereleases": prereleases} if prereleases is not None else {}

        assert list(spec.filter(input, **kwargs)) == expected

    @pytest.mark.parametrize(
        ("specifier", "expected"),
        [
            # Single item specifiers should just be reflexive
            ("!=2.0", "!=2.0"),
            ("<2.0", "<2.0"),
            ("<=2.0", "<=2.0"),
            ("==2.0", "==2.0"),
            (">2.0", ">2.0"),
            (">=2.0", ">=2.0"),
            ("~=2.0", "~=2.0"),
            # Spaces should be removed
            ("< 2", "<2"),
            # Multiple item specifiers should work
            ("!=2.0,>1.0", "!=2.0,>1.0"),
            ("!=2.0 ,>1.0", "!=2.0,>1.0"),
        ],
    )
    def test_specifiers_str_and_repr(self, specifier, expected):
        spec = SpecifierSet(specifier)

        assert str(spec) == expected
        assert repr(spec) == f"<SpecifierSet({expected!r})>"

    @pytest.mark.parametrize("specifier", SPECIFIERS + LEGACY_SPECIFIERS)
    def test_specifiers_hash(self, specifier):
        assert hash(SpecifierSet(specifier)) == hash(SpecifierSet(specifier))

    @pytest.mark.parametrize(
        ("left", "right", "expected"), [(">2.0", "<5.0", ">2.0,<5.0")]
    )
    def test_specifiers_combine(self, left, right, expected):
        result = SpecifierSet(left) & SpecifierSet(right)
        assert result == SpecifierSet(expected)

        result = SpecifierSet(left) & right
        assert result == SpecifierSet(expected)

        result = SpecifierSet(left, prereleases=True) & SpecifierSet(right)
        assert result == SpecifierSet(expected)
        assert result.prereleases

        result = SpecifierSet(left, prereleases=False) & SpecifierSet(right)
        assert result == SpecifierSet(expected)
        assert not result.prereleases

        result = SpecifierSet(left) & SpecifierSet(right, prereleases=True)
        assert result == SpecifierSet(expected)
        assert result.prereleases

        result = SpecifierSet(left) & SpecifierSet(right, prereleases=False)
        assert result == SpecifierSet(expected)
        assert not result.prereleases

        result = SpecifierSet(left, prereleases=True) & SpecifierSet(
            right, prereleases=True
        )
        assert result == SpecifierSet(expected)
        assert result.prereleases

        result = SpecifierSet(left, prereleases=False) & SpecifierSet(
            right, prereleases=False
        )
        assert result == SpecifierSet(expected)
        assert not result.prereleases

        with pytest.raises(ValueError):
            result = SpecifierSet(left, prereleases=True) & SpecifierSet(
                right, prereleases=False
            )

        with pytest.raises(ValueError):
            result = SpecifierSet(left, prereleases=False) & SpecifierSet(
                right, prereleases=True
            )

    def test_specifiers_combine_not_implemented(self):
        with pytest.raises(TypeError):
            SpecifierSet() & 12

    @pytest.mark.parametrize(
        ("left", "right", "op"),
        itertools.chain.from_iterable(
            # Verify that the equal (==) operator works correctly
            [[(x, x, operator.eq) for x in SPECIFIERS]]
            +
            # Verify that the not equal (!=) operator works correctly
            [
                [(x, y, operator.ne) for j, y in enumerate(SPECIFIERS) if i != j]
                for i, x in enumerate(SPECIFIERS)
            ]
        ),
    )
    def test_comparison_true(self, left, right, op):
        assert op(SpecifierSet(left), SpecifierSet(right))
        assert op(SpecifierSet(left), Specifier(right))
        assert op(Specifier(left), SpecifierSet(right))
        assert op(left, SpecifierSet(right))
        assert op(SpecifierSet(left), right)

    @pytest.mark.parametrize(
        ("left", "right", "op"),
        itertools.chain.from_iterable(
            # Verify that the equal (==) operator works correctly
            [[(x, x, operator.ne) for x in SPECIFIERS]]
            +
            # Verify that the not equal (!=) operator works correctly
            [
                [(x, y, operator.eq) for j, y in enumerate(SPECIFIERS) if i != j]
                for i, x in enumerate(SPECIFIERS)
            ]
        ),
    )
    def test_comparison_false(self, left, right, op):
        assert not op(SpecifierSet(left), SpecifierSet(right))
        assert not op(SpecifierSet(left), Specifier(right))
        assert not op(Specifier(left), SpecifierSet(right))
        assert not op(left, SpecifierSet(right))
        assert not op(SpecifierSet(left), right)

    @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")])
    def test_comparison_canonicalizes(self, left, right):
        assert SpecifierSet(left) == SpecifierSet(right)
        assert left == SpecifierSet(right)
        assert SpecifierSet(left) == right

    def test_comparison_non_specifier(self):
        assert SpecifierSet("==1.0") != 12
        assert not SpecifierSet("==1.0") == 12

    @pytest.mark.parametrize(
        ("version", "specifier", "expected"),
        [
            ("1.0.0+local", "==1.0.0", True),
            ("1.0.0+local", "!=1.0.0", False),
            ("1.0.0+local", "<=1.0.0", True),
            ("1.0.0+local", ">=1.0.0", True),
            ("1.0.0+local", "<1.0.0", False),
            ("1.0.0+local", ">1.0.0", False),
        ],
    )
    def test_comparison_ignores_local(self, version, specifier, expected):
        assert (Version(version) in SpecifierSet(specifier)) == expected

    def test_contains_with_compatible_operator(self):
        combination = SpecifierSet("~=1.18.0") & SpecifierSet("~=1.18")
        assert "1.19.5" not in combination
        assert "1.18.0" in combination
