import os
import unittest
from collections import OrderedDict

from pkg_resources import parse_requirements

from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import memoize

try:
    from unittest import mock
    builtins_open = 'builtins.open'
except ImportError:
    import mock
    builtins_open = '__builtin__.open'


def mock_open_with_name(read_data="", name="file.name"):
    """Mock open call with a specified `name` attribute."""
    m = mock.mock_open(read_data=read_data)
    m.return_value.name = name
    return m


def mock_open_multiple(files=OrderedDict()):
    """Create a mock open object for multiple files."""
    m = mock.mock_open()
    m.side_effect = [
        mock_open_with_name(read_data=content, name=name).return_value
        for name, content in files.items()
    ]
    return m


class RequirementsTestCase(unittest.TestCase):

    def setUp(self):
        memoize.mem = {}

    def test_resolve_requirement(self):
        self.assertEqual(
            Flake8Checker.resolve_requirement("foo >= 1.0.0"),
            ["foo >= 1.0.0"],
        )

    def test_resolve_requirement_with_option(self):
        self.assertEqual(
            Flake8Checker.resolve_requirement("foo-bar.v1==1.0 --hash=md5:."),
            ["foo-bar.v1==1.0"],
        )

    def test_resolve_requirement_standalone_option(self):
        self.assertEqual(
            Flake8Checker.resolve_requirement("--extra-index-url"),
            [],
        )

    def test_resolve_requirement_with_file_beyond_max_depth(self):
        with self.assertRaises(RuntimeError):
            Flake8Checker.resolve_requirement("-r requirements.txt")

    def test_resolve_requirement_with_file_empty(self):
        with mock.patch(builtins_open, mock.mock_open()) as m:
            self.assertEqual(
                Flake8Checker.resolve_requirement("-r requirements.txt", 1),
                [],
            )
            m.assert_called_once_with("requirements.txt")

    def test_resolve_requirement_with_file_content(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "foo >= 1.0.0\nbar <= 1.0.0\n"),
        )))):
            self.assertEqual(
                Flake8Checker.resolve_requirement("-r requirements.txt", 1),
                ["foo >= 1.0.0", "bar <= 1.0.0"],
            )

    def test_resolve_requirement_with_file_content_line_continuation(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "foo[bar] \\\n>= 1.0.0\n"),
        )))):
            self.assertEqual(
                Flake8Checker.resolve_requirement("-r requirements.txt", 1),
                ["foo[bar] >= 1.0.0"],
            )

    def test_resolve_requirement_with_file_content_line_continuation_2(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "foo \\\n>= 1.0.0 \\\n# comment \\\nbar \\"),
        )))):
            self.assertEqual(
                Flake8Checker.resolve_requirement("-r requirements.txt", 1),
                ["foo >= 1.0.0", "bar"],
            )

    def test_resolve_requirement_with_file_recursion_beyond_max_depth(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "-r requirements.txt\n"),
        )))):
            with self.assertRaises(RuntimeError):
                Flake8Checker.resolve_requirement("-r requirements.txt", 1),

    def test_resolve_requirement_with_file_recursion(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "--requirement inner.txt\nbar <= 1.0.0\n"),
            ("inner.txt", "# inner\nbaz\n\nqux\n"),
        )))):
            self.assertEqual(
                Flake8Checker.resolve_requirement("-r requirements.txt", 2),
                ["baz", "qux", "bar <= 1.0.0"],
            )

    def test_resolve_requirement_with_relative_include(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "-r requirements/production.txt"),
            ("requirements/production.txt", "-r node/one.txt\nfoo"),
            ("requirements/node/one.txt", "-r common.txt\n-r /abs/path.txt"),
            ("requirements/node/common.txt", "bar"),
            ("/abs/path.txt", "bis"),
        )))) as m:
            self.assertEqual(
                Flake8Checker.resolve_requirement("-r requirements.txt", 5),
                ["bar", "bis", "foo"],
            )
            m.assert_has_calls([
                mock.call("requirements.txt"),
                mock.call("requirements/production.txt"),
                mock.call("requirements/node/one.txt"),
                mock.call("requirements/node/common.txt"),
                mock.call("/abs/path.txt"),
            ])

    def test_init_with_no_requirements(self):
        with mock.patch(builtins_open, mock.mock_open()) as m:
            m.side_effect = IOError("No such file or directory"),
            checker = Flake8Checker(None, None)
            self.assertEqual(checker.get_requirements_txt(), ())

    def test_init_with_user_requirements(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements/base.txt", "foo >= 1.0.0\n-r inner.txt\n"),
            ("requirements/inner.txt", "bar\n"),
        )))) as m:
            try:
                Flake8Checker.requirements_file = "requirements/base.txt"
                checker = Flake8Checker(None, None)
                self.assertEqual(
                    checker.get_requirements_txt(),
                    tuple(parse_requirements([
                        "foo >= 1.0.0",
                        "bar",
                    ])),
                )
                m.assert_has_calls([
                    mock.call("requirements/base.txt"),
                    mock.call("requirements/inner.txt"),
                ])
            finally:
                Flake8Checker.requirements_file = None

    def test_init_with_simple_requirements(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "foo >= 1.0.0\nbar <= 1.0.0\n"),
        )))):
            checker = Flake8Checker(None, None)
            self.assertEqual(
                checker.get_requirements_txt(),
                tuple(parse_requirements([
                    "foo >= 1.0.0",
                    "bar <= 1.0.0",
                ])),
            )

    def test_init_with_recursive_requirements_beyond_max_depth(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "foo >= 1.0.0\n-r inner.txt\nbar <= 1.0.0\n"),
            ("inner.txt", "# inner\nbaz\n\nqux\n"),
        )))):
            with self.assertRaises(RuntimeError):
                try:
                    Flake8Checker.requirements_max_depth = 0
                    checker = Flake8Checker(None, None)
                    checker.get_requirements_txt()
                finally:
                    Flake8Checker.requirements_max_depth = 1

    def test_init_with_recursive_requirements(self):
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", "foo >= 1.0.0\n-r inner.txt\nbar <= 1.0.0\n"),
            ("inner.txt", "# inner\nbaz\n\nqux\n"),
        )))):
            checker = Flake8Checker(None, None)
            self.assertEqual(
                checker.get_requirements_txt(),
                tuple(parse_requirements([
                    "foo >= 1.0.0",
                    "baz",
                    "qux",
                    "bar <= 1.0.0",
                ])),
            )

    def test_init_misc(self):
        curdir = os.path.abspath(os.path.dirname(__file__))
        with open(os.path.join(curdir, "test_requirements.txt")) as f:
            requirements_content = f.read()
        with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
            ("requirements.txt", requirements_content),
        )))):
            checker = Flake8Checker(None, None)
            self.assertEqual(
                checker.get_requirements_txt(),
                tuple(parse_requirements([
                    "nose",
                    "apache == 0.6.9",
                    "coverage[graph,test] ~= 3.1",
                    "graph <2.0, >=1.2 ; python_version < '3.8'",
                    "foo-project >= 1.2",
                    "bar-project == 8.8",
                    "configuration",
                    "blackBox == 1.4.4",
                    "exPackage_paint == 1.4.8.dev1984+49a8814",
                    "package-one",
                    "package-two",
                    "whiteBox",
                ])),
            )