# -*- coding:utf-8 -*-
#
# 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(UtilTests, self).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')
