# 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.
from __future__ import absolute_import, division, print_function

import collections
import itertools
import os
import platform
import sys

import pretend
import pytest

from packaging.markers import (
    InvalidMarker,
    Marker,
    Node,
    UndefinedComparison,
    UndefinedEnvironmentName,
    default_environment,
    format_full_version,
)

VARIABLES = [
    "extra",
    "implementation_name",
    "implementation_version",
    "os_name",
    "platform_machine",
    "platform_release",
    "platform_system",
    "platform_version",
    "python_full_version",
    "python_version",
    "platform_python_implementation",
    "sys_platform",
]

PEP_345_VARIABLES = [
    "os.name",
    "sys.platform",
    "platform.version",
    "platform.machine",
    "platform.python_implementation",
]

SETUPTOOLS_VARIABLES = ["python_implementation"]

OPERATORS = ["===", "==", ">=", "<=", "!=", "~=", ">", "<", "in", "not in"]

VALUES = [
    "1.0",
    "5.6a0",
    "dog",
    "freebsd",
    "literally any string can go here",
    "things @#4 dsfd (((",
]


class TestNode:
    @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
    def test_accepts_value(self, value):
        assert Node(value).value == value

    @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
    def test_str(self, value):
        assert str(Node(value)) == str(value)

    @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
    def test_repr(self, value):
        assert repr(Node(value)) == "<Node({0!r})>".format(str(value))

    def test_base_class(self):
        with pytest.raises(NotImplementedError):
            Node("cover all the code").serialize()


class TestOperatorEvaluation:
    def test_prefers_pep440(self):
        assert Marker('"2.7.9" < "foo"').evaluate(dict(foo="2.7.10"))

    def test_falls_back_to_python(self):
        assert Marker('"b" > "a"').evaluate(dict(a="a"))

    def test_fails_when_undefined(self):
        with pytest.raises(UndefinedComparison):
            Marker("'2.7.0' ~= os_name").evaluate()


FakeVersionInfo = collections.namedtuple(
    "FakeVersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]
)


class TestDefaultEnvironment:
    @pytest.mark.skipif(
        hasattr(sys, "implementation"), reason="sys.implementation does exist"
    )
    def test_matches_expected_no_sys_implementation(self):
        environment = default_environment()

        assert environment == {
            "implementation_name": "",
            "implementation_version": "0",
            "os_name": os.name,
            "platform_machine": platform.machine(),
            "platform_release": platform.release(),
            "platform_system": platform.system(),
            "platform_version": platform.version(),
            "python_full_version": platform.python_version(),
            "platform_python_implementation": platform.python_implementation(),
            "python_version": ".".join(platform.python_version_tuple()[:2]),
            "sys_platform": sys.platform,
        }

    @pytest.mark.skipif(
        not hasattr(sys, "implementation"), reason="sys.implementation does not exist"
    )
    def test_matches_expected_deleted_sys_implementation(self, monkeypatch):
        monkeypatch.delattr(sys, "implementation")

        environment = default_environment()

        assert environment == {
            "implementation_name": "",
            "implementation_version": "0",
            "os_name": os.name,
            "platform_machine": platform.machine(),
            "platform_release": platform.release(),
            "platform_system": platform.system(),
            "platform_version": platform.version(),
            "python_full_version": platform.python_version(),
            "platform_python_implementation": platform.python_implementation(),
            "python_version": ".".join(platform.python_version_tuple()[:2]),
            "sys_platform": sys.platform,
        }

    @pytest.mark.skipif(
        not hasattr(sys, "implementation"), reason="sys.implementation does not exist"
    )
    def test_matches_expected(self):
        environment = default_environment()

        iver = "{0.major}.{0.minor}.{0.micro}".format(sys.implementation.version)
        if sys.implementation.version.releaselevel != "final":
            iver = "{0}{1[0]}{2}".format(
                iver,
                sys.implementation.version.releaselevel,
                sys.implementation.version.serial,
            )

        assert environment == {
            "implementation_name": sys.implementation.name,
            "implementation_version": iver,
            "os_name": os.name,
            "platform_machine": platform.machine(),
            "platform_release": platform.release(),
            "platform_system": platform.system(),
            "platform_version": platform.version(),
            "python_full_version": platform.python_version(),
            "platform_python_implementation": platform.python_implementation(),
            "python_version": ".".join(platform.python_version_tuple()[:2]),
            "sys_platform": sys.platform,
        }

    @pytest.mark.skipif(
        hasattr(sys, "implementation"), reason="sys.implementation does exist"
    )
    def test_monkeypatch_sys_implementation(self, monkeypatch):
        monkeypatch.setattr(
            sys,
            "implementation",
            pretend.stub(version=FakeVersionInfo(3, 4, 2, "final", 0), name="linux"),
            raising=False,
        )

        environment = default_environment()
        assert environment == {
            "implementation_name": "linux",
            "implementation_version": "3.4.2",
            "os_name": os.name,
            "platform_machine": platform.machine(),
            "platform_release": platform.release(),
            "platform_system": platform.system(),
            "platform_version": platform.version(),
            "python_full_version": platform.python_version(),
            "platform_python_implementation": platform.python_implementation(),
            "python_version": ".".join(platform.python_version_tuple()[:2]),
            "sys_platform": sys.platform,
        }

    def test_multidigit_minor_version(self, monkeypatch):
        version_info = (3, 10, 0, "final", 0)
        monkeypatch.setattr(sys, "version_info", version_info, raising=False)

        monkeypatch.setattr(platform, "python_version", lambda: "3.10.0", raising=False)
        monkeypatch.setattr(
            platform, "python_version_tuple", lambda: ("3", "10", "0"), raising=False
        )

        environment = default_environment()
        assert environment["python_version"] == "3.10"

    def tests_when_releaselevel_final(self):
        v = FakeVersionInfo(3, 4, 2, "final", 0)
        assert format_full_version(v) == "3.4.2"

    def tests_when_releaselevel_not_final(self):
        v = FakeVersionInfo(3, 4, 2, "beta", 4)
        assert format_full_version(v) == "3.4.2b4"


class TestMarker:
    @pytest.mark.parametrize(
        "marker_string",
        [
            "{0} {1} {2!r}".format(*i)
            for i in itertools.product(VARIABLES, OPERATORS, VALUES)
        ]
        + [
            "{2!r} {1} {0}".format(*i)
            for i in itertools.product(VARIABLES, OPERATORS, VALUES)
        ],
    )
    def test_parses_valid(self, marker_string):
        Marker(marker_string)

    @pytest.mark.parametrize(
        "marker_string",
        [
            "this_isnt_a_real_variable >= '1.0'",
            "python_version",
            "(python_version)",
            "python_version >= 1.0 and (python_version)",
        ],
    )
    def test_parses_invalid(self, marker_string):
        with pytest.raises(InvalidMarker):
            Marker(marker_string)

    @pytest.mark.parametrize(
        ("marker_string", "expected"),
        [
            # Test the different quoting rules
            ("python_version == '2.7'", 'python_version == "2.7"'),
            ('python_version == "2.7"', 'python_version == "2.7"'),
            # Test and/or expressions
            (
                'python_version == "2.7" and os_name == "linux"',
                'python_version == "2.7" and os_name == "linux"',
            ),
            (
                'python_version == "2.7" or os_name == "linux"',
                'python_version == "2.7" or os_name == "linux"',
            ),
            (
                'python_version == "2.7" and os_name == "linux" or '
                'sys_platform == "win32"',
                'python_version == "2.7" and os_name == "linux" or '
                'sys_platform == "win32"',
            ),
            # Test nested expressions and grouping with ()
            ('(python_version == "2.7")', 'python_version == "2.7"'),
            (
                '(python_version == "2.7" and sys_platform == "win32")',
                'python_version == "2.7" and sys_platform == "win32"',
            ),
            (
                'python_version == "2.7" and (sys_platform == "win32" or '
                'sys_platform == "linux")',
                'python_version == "2.7" and (sys_platform == "win32" or '
                'sys_platform == "linux")',
            ),
        ],
    )
    def test_str_and_repr(self, marker_string, expected):
        m = Marker(marker_string)
        assert str(m) == expected
        assert repr(m) == "<Marker({0!r})>".format(str(m))

    def test_extra_with_no_extra_in_environment(self):
        # We can't evaluate an extra if no extra is passed into the environment
        m = Marker("extra == 'security'")
        with pytest.raises(UndefinedEnvironmentName):
            m.evaluate()

    @pytest.mark.parametrize(
        ("marker_string", "environment", "expected"),
        [
            ("os_name == '{0}'".format(os.name), None, True),
            ("os_name == 'foo'", {"os_name": "foo"}, True),
            ("os_name == 'foo'", {"os_name": "bar"}, False),
            ("'2.7' in python_version", {"python_version": "2.7.5"}, True),
            ("'2.7' not in python_version", {"python_version": "2.7"}, False),
            (
                "os_name == 'foo' and python_version ~= '2.7.0'",
                {"os_name": "foo", "python_version": "2.7.6"},
                True,
            ),
            (
                "python_version ~= '2.7.0' and (os_name == 'foo' or "
                "os_name == 'bar')",
                {"os_name": "foo", "python_version": "2.7.4"},
                True,
            ),
            (
                "python_version ~= '2.7.0' and (os_name == 'foo' or "
                "os_name == 'bar')",
                {"os_name": "bar", "python_version": "2.7.4"},
                True,
            ),
            (
                "python_version ~= '2.7.0' and (os_name == 'foo' or "
                "os_name == 'bar')",
                {"os_name": "other", "python_version": "2.7.4"},
                False,
            ),
            ("extra == 'security'", {"extra": "quux"}, False),
            ("extra == 'security'", {"extra": "security"}, True),
        ],
    )
    def test_evaluates(self, marker_string, environment, expected):
        args = [] if environment is None else [environment]
        assert Marker(marker_string).evaluate(*args) == expected

    @pytest.mark.parametrize(
        "marker_string",
        [
            "{0} {1} {2!r}".format(*i)
            for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
        ]
        + [
            "{2!r} {1} {0}".format(*i)
            for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
        ],
    )
    def test_parses_pep345_valid(self, marker_string):
        Marker(marker_string)

    @pytest.mark.parametrize(
        ("marker_string", "environment", "expected"),
        [
            ("os.name == '{0}'".format(os.name), None, True),
            ("sys.platform == 'win32'", {"sys_platform": "linux2"}, False),
            ("platform.version in 'Ubuntu'", {"platform_version": "#39"}, False),
            ("platform.machine=='x86_64'", {"platform_machine": "x86_64"}, True),
            (
                "platform.python_implementation=='Jython'",
                {"platform_python_implementation": "CPython"},
                False,
            ),
            (
                "python_version == '2.5' and platform.python_implementation"
                "!= 'Jython'",
                {"python_version": "2.7"},
                False,
            ),
        ],
    )
    def test_evaluate_pep345_markers(self, marker_string, environment, expected):
        args = [] if environment is None else [environment]
        assert Marker(marker_string).evaluate(*args) == expected

    @pytest.mark.parametrize(
        "marker_string",
        [
            "{0} {1} {2!r}".format(*i)
            for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
        ]
        + [
            "{2!r} {1} {0}".format(*i)
            for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
        ],
    )
    def test_parses_setuptools_legacy_valid(self, marker_string):
        Marker(marker_string)

    def test_evaluate_setuptools_legacy_markers(self):
        marker_string = "python_implementation=='Jython'"
        args = [{"platform_python_implementation": "CPython"}]
        assert Marker(marker_string).evaluate(*args) is False
