#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
# Copyright 2015 Nebula, Inc.
#
# SPDX-License-Identifier: Apache-2.0
import ast
import os
import shutil
import sys
import tempfile

import testtools

from bandit.core import utils as b_utils


def _touch(path):
    """Create an empty file at ``path``."""
    open(path, "w").close()


class UtilTests(testtools.TestCase):
    """This set of tests exercises bandit.core.util functions."""

    def setUp(self):
        super().setUp()
        self._setup_get_module_qualname_from_path()

    def _setup_get_module_qualname_from_path(self):
        """Setup a fake directory for testing get_module_qualname_from_path().

        Create temporary directory and then create fake .py files
        within directory structure.  We setup test cases for
        a typical module, a path misssing a middle __init__.py,
        no __init__.py anywhere in path, symlinking .py files.
        """

        self.tempdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, self.tempdir)
        self.reltempdir = os.path.relpath(self.tempdir)

        # good/a/b/c/test_typical.py
        os.makedirs(os.path.join(self.tempdir, "good", "a", "b", "c"), 0o755)
        _touch(os.path.join(self.tempdir, "good", "__init__.py"))
        _touch(os.path.join(self.tempdir, "good", "a", "__init__.py"))
        _touch(os.path.join(self.tempdir, "good", "a", "b", "__init__.py"))
        _touch(
            os.path.join(self.tempdir, "good", "a", "b", "c", "__init__.py")
        )
        _touch(
            os.path.join(
                self.tempdir, "good", "a", "b", "c", "test_typical.py"
            )
        )

        # missingmid/a/b/c/test_missingmid.py
        os.makedirs(
            os.path.join(self.tempdir, "missingmid", "a", "b", "c"), 0o755
        )
        _touch(os.path.join(self.tempdir, "missingmid", "__init__.py"))
        # no missingmid/a/__init__.py
        _touch(
            os.path.join(self.tempdir, "missingmid", "a", "b", "__init__.py")
        )
        _touch(
            os.path.join(
                self.tempdir, "missingmid", "a", "b", "c", "__init__.py"
            )
        )
        _touch(
            os.path.join(
                self.tempdir, "missingmid", "a", "b", "c", "test_missingmid.py"
            )
        )

        # missingend/a/b/c/test_missingend.py
        os.makedirs(
            os.path.join(self.tempdir, "missingend", "a", "b", "c"), 0o755
        )
        _touch(os.path.join(self.tempdir, "missingend", "__init__.py"))
        _touch(
            os.path.join(self.tempdir, "missingend", "a", "b", "__init__.py")
        )
        # no missingend/a/b/c/__init__.py
        _touch(
            os.path.join(
                self.tempdir, "missingend", "a", "b", "c", "test_missingend.py"
            )
        )

        # syms/a/bsym/c/test_typical.py
        os.makedirs(os.path.join(self.tempdir, "syms", "a"), 0o755)
        _touch(os.path.join(self.tempdir, "syms", "__init__.py"))
        _touch(os.path.join(self.tempdir, "syms", "a", "__init__.py"))
        os.symlink(
            os.path.join(self.tempdir, "good", "a", "b"),
            os.path.join(self.tempdir, "syms", "a", "bsym"),
        )

    def test_get_module_qualname_from_path_abs_typical(self):
        """Test get_module_qualname_from_path with typical absolute paths."""

        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.tempdir, "good", "a", "b", "c", "test_typical.py"
            )
        )
        self.assertEqual("good.a.b.c.test_typical", name)

    def test_get_module_qualname_from_path_with_dot(self):
        """Test get_module_qualname_from_path with a "." ."""

        name = b_utils.get_module_qualname_from_path(
            os.path.join(".", "__init__.py")
        )

        self.assertEqual("__init__", name)

    def test_get_module_qualname_from_path_abs_missingmid(self):
        # Test get_module_qualname_from_path with missing module
        # __init__.py

        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.tempdir, "missingmid", "a", "b", "c", "test_missingmid.py"
            )
        )
        self.assertEqual("b.c.test_missingmid", name)

    def test_get_module_qualname_from_path_abs_missingend(self):
        # Test get_module_qualname_from_path with no __init__.py
        # last dir'''

        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.tempdir, "missingend", "a", "b", "c", "test_missingend.py"
            )
        )
        self.assertEqual("test_missingend", name)

    def test_get_module_qualname_from_path_abs_syms(self):
        """Test get_module_qualname_from_path with symlink in path."""

        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.tempdir, "syms", "a", "bsym", "c", "test_typical.py"
            )
        )
        self.assertEqual("syms.a.bsym.c.test_typical", name)

    def test_get_module_qualname_from_path_rel_typical(self):
        """Test get_module_qualname_from_path with typical relative paths."""

        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.reltempdir, "good", "a", "b", "c", "test_typical.py"
            )
        )
        self.assertEqual("good.a.b.c.test_typical", name)

    def test_get_module_qualname_from_path_rel_missingmid(self):
        # Test get_module_qualname_from_path with module __init__.py
        # missing and relative paths

        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.reltempdir,
                "missingmid",
                "a",
                "b",
                "c",
                "test_missingmid.py",
            )
        )
        self.assertEqual("b.c.test_missingmid", name)

    def test_get_module_qualname_from_path_rel_missingend(self):
        # Test get_module_qualname_from_path with __init__.py missing from
        # last dir and using relative paths

        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.reltempdir,
                "missingend",
                "a",
                "b",
                "c",
                "test_missingend.py",
            )
        )
        self.assertEqual("test_missingend", name)

    def test_get_module_qualname_from_path_rel_syms(self):
        """Test get_module_qualname_from_path with symbolic relative paths."""
        name = b_utils.get_module_qualname_from_path(
            os.path.join(
                self.reltempdir, "syms", "a", "bsym", "c", "test_typical.py"
            )
        )
        self.assertEqual("syms.a.bsym.c.test_typical", name)

    def test_get_module_qualname_from_path_sys(self):
        """Test get_module_qualname_from_path with system module paths."""

        name = b_utils.get_module_qualname_from_path(os.__file__)
        self.assertEqual("os", name)

        # This will fail because of magic for os.path. Not sure how to fix.
        # name = b_utils.get_module_qualname_from_path(os.path.__file__)
        # self.assertEqual(name, 'os.path')

    def test_get_module_qualname_from_path_invalid_path(self):
        """Test get_module_qualname_from_path with invalid path."""

        name = b_utils.get_module_qualname_from_path("/a/b/c/d/e.py")
        self.assertEqual("e", name)

    def test_get_module_qualname_from_path_dir(self):
        """Test get_module_qualname_from_path with dir path."""

        self.assertRaises(
            b_utils.InvalidModulePath,
            b_utils.get_module_qualname_from_path,
            "/tmp/",
        )

    def test_namespace_path_join(self):
        p = b_utils.namespace_path_join("base1.base2", "name")
        self.assertEqual("base1.base2.name", p)

    def test_namespace_path_split(self):
        (head, tail) = b_utils.namespace_path_split("base1.base2.name")
        self.assertEqual("base1.base2", head)
        self.assertEqual("name", tail)

    def test_get_call_name1(self):
        """Gets a qualified call name."""
        tree = ast.parse("a.b.c.d(x,y)").body[0].value
        name = b_utils.get_call_name(tree, {})
        self.assertEqual("a.b.c.d", name)

    def test_get_call_name2(self):
        """Gets qualified call name and resolves aliases."""
        tree = ast.parse("a.b.c.d(x,y)").body[0].value

        name = b_utils.get_call_name(tree, {"a": "alias.x.y"})
        self.assertEqual("alias.x.y.b.c.d", name)

        name = b_utils.get_call_name(tree, {"a.b": "alias.x.y"})
        self.assertEqual("alias.x.y.c.d", name)

        name = b_utils.get_call_name(tree, {"a.b.c.d": "alias.x.y"})
        self.assertEqual("alias.x.y", name)

    def test_get_call_name3(self):
        """Getting name for a complex call."""
        tree = ast.parse("a.list[0](x,y)").body[0].value
        name = b_utils._get_attr_qual_name(tree, {})
        self.assertEqual("", name)
        # TODO(ljfisher) At best we might be able to get:
        # self.assertEqual(name, 'a.list[0]')

    def test_linerange(self):
        with open("./examples/jinja2_templating.py") as test_file:
            tree = ast.parse(test_file.read())
        # Check linerange returns corrent number of lines
        line = tree.body[8]
        lrange = b_utils.linerange(line)

        # line 9 should be three lines long
        self.assertEqual(3, len(lrange))

        # the range should be the correct line numbers
        self.assertEqual([11, 12, 13], list(lrange))

    def test_path_for_function(self):
        path = b_utils.get_path_for_function(b_utils.get_path_for_function)
        self.assertEqual(path, b_utils.__file__)

    def test_path_for_function_no_file(self):
        self.assertIsNone(b_utils.get_path_for_function(sys.settrace))

    def test_path_for_function_no_module(self):
        self.assertIsNone(b_utils.get_path_for_function(1))

    def test_escaped_representation_simple(self):
        res = b_utils.escaped_bytes_representation(b"ascii")
        self.assertEqual(res, b"ascii")

    def test_escaped_representation_valid_not_printable(self):
        res = b_utils.escaped_bytes_representation(b"\\u0000")
        self.assertEqual(res, b"\\x00")

    def test_escaped_representation_invalid(self):
        res = b_utils.escaped_bytes_representation(b"\\uffff")
        self.assertEqual(res, b"\\uffff")

    def test_escaped_representation_mixed(self):
        res = b_utils.escaped_bytes_representation(b"ascii\\u0000\\uffff")
        self.assertEqual(res, b"ascii\\x00\\uffff")

    def test_deepgetattr(self):
        a = type("", (), {})
        a.b = type("", (), {})
        a.b.c = type("", (), {})
        a.b.c.d = "deep value"
        a.b.c.d2 = "deep value 2"
        a.b.c.e = "a.b.c"
        self.assertEqual("deep value", b_utils.deepgetattr(a.b.c, "d"))
        self.assertEqual("deep value 2", b_utils.deepgetattr(a.b.c, "d2"))
        self.assertEqual("a.b.c", b_utils.deepgetattr(a.b.c, "e"))
        self.assertEqual("deep value", b_utils.deepgetattr(a, "b.c.d"))
        self.assertEqual("deep value 2", b_utils.deepgetattr(a, "b.c.d2"))
        self.assertRaises(AttributeError, b_utils.deepgetattr, a.b, "z")

    def test_parse_ini_file(self):
        tests = [
            {
                "content": "[bandit]\nexclude=/abc,/def",
                "expected": {"exclude": "/abc,/def"},
            },
            {"content": "[Blabla]\nsomething=something", "expected": None},
        ]

        with tempfile.NamedTemporaryFile("r+") as t:
            for test in tests:
                with open(t.name, "w") as f:
                    f.write(test["content"])

                self.assertEqual(
                    b_utils.parse_ini_file(t.name), test["expected"]
                )

    def test_check_ast_node_good(self):
        node = b_utils.check_ast_node("Call")
        self.assertEqual("Call", node)

    def test_check_ast_node_bad_node(self):
        self.assertRaises(TypeError, b_utils.check_ast_node, "Derp")

    def test_check_ast_node_bad_type(self):
        self.assertRaises(TypeError, b_utils.check_ast_node, "walk")
