# Copyright 2015 IBM Corp.
#
# SPDX-License-Identifier: Apache-2.0
import os
import tempfile
import textwrap
import uuid
from unittest import mock

import fixtures
import testtools

from bandit.core import config
from bandit.core import utils


class TempFile(fixtures.Fixture):
    def __init__(self, contents=None, suffix=".yaml"):
        super().__init__()
        self.contents = contents
        self.suffix = suffix

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

        with tempfile.NamedTemporaryFile(
            suffix=self.suffix, mode="wt", delete=False
        ) as f:
            if self.contents:
                f.write(self.contents)

        self.addCleanup(os.unlink, f.name)

        self.name = f.name


class TestInit(testtools.TestCase):
    def test_settings(self):
        # Can initialize a BanditConfig.

        example_key = uuid.uuid4().hex
        example_value = self.getUniqueString()
        contents = f"{example_key}: {example_value}"
        f = self.useFixture(TempFile(contents))
        b_config = config.BanditConfig(f.name)

        # After initialization, can get settings.
        self.assertEqual("*.py", b_config.get_setting("plugin_name_pattern"))

        self.assertEqual({example_key: example_value}, b_config.config)
        self.assertEqual(example_value, b_config.get_option(example_key))

    def test_file_does_not_exist(self):
        # When the config file doesn't exist, ConfigFileUnopenable is raised.

        cfg_file = os.path.join(os.getcwd(), "notafile")
        self.assertRaisesRegex(
            utils.ConfigError, cfg_file, config.BanditConfig, cfg_file
        )

    def test_yaml_invalid(self):
        # When the config yaml file isn't valid, sys.exit(2) is called.

        # The following is invalid because it starts a sequence and doesn't
        # end it.
        invalid_yaml = "- [ something"
        f = self.useFixture(TempFile(invalid_yaml))
        self.assertRaisesRegex(
            utils.ConfigError, f.name, config.BanditConfig, f.name
        )


class TestGetOption(testtools.TestCase):
    def setUp(self):
        super().setUp()

        self.example_key = uuid.uuid4().hex
        self.example_subkey = uuid.uuid4().hex
        self.example_subvalue = uuid.uuid4().hex
        sample_yaml = textwrap.dedent(
            f"""
            {self.example_key}:
                {self.example_subkey}: {self.example_subvalue}
            """
        )

        f = self.useFixture(TempFile(sample_yaml))

        self.b_config = config.BanditConfig(f.name)

    def test_levels(self):
        # get_option with .-separated string.

        sample_option_name = f"{self.example_key}.{self.example_subkey}"
        self.assertEqual(
            self.example_subvalue, self.b_config.get_option(sample_option_name)
        )

    def test_levels_not_exist(self):
        # get_option when option name doesn't exist returns None.

        sample_option_name = f"{uuid.uuid4().hex}.{uuid.uuid4().hex}"
        self.assertIsNone(self.b_config.get_option(sample_option_name))


class TestGetSetting(testtools.TestCase):
    def setUp(self):
        super().setUp()
        test_yaml = "key: value"
        f = self.useFixture(TempFile(test_yaml))
        self.b_config = config.BanditConfig(f.name)

    def test_not_exist(self):
        # get_setting() when the name doesn't exist returns None

        sample_setting_name = uuid.uuid4().hex
        self.assertIsNone(self.b_config.get_setting(sample_setting_name))


class TestConfigCompat(testtools.TestCase):
    sample = textwrap.dedent(
        """
        profiles:
            test_1:
                include:
                    - any_other_function_with_shell_equals_true
                    - assert_used
                exclude:

            test_2:
                include:
                    - blacklist_calls

            test_3:
                include:
                    - blacklist_imports

            test_4:
                exclude:
                    - assert_used

            test_5:
                exclude:
                    - blacklist_calls
                    - blacklist_imports

            test_6:
                include:
                    - blacklist_calls

                exclude:
                    - blacklist_imports

        blacklist_calls:
            bad_name_sets:
                - pickle:
                    qualnames: [pickle.loads]
                    message: "{func} library appears to be in use."

        blacklist_imports:
            bad_import_sets:
                - telnet:
                    imports: [telnetlib]
                    level: HIGH
                    message: "{module} is considered insecure."
        """
    )
    suffix = ".yaml"

    def setUp(self):
        super().setUp()
        f = self.useFixture(TempFile(self.sample, suffix=self.suffix))
        self.config = config.BanditConfig(f.name)

    def test_converted_include(self):
        profiles = self.config.get_option("profiles")
        test = profiles["test_1"]
        data = {
            "blacklist": {},
            "exclude": set(),
            "include": {"B101", "B604"},
        }

        self.assertEqual(data, test)

    def test_converted_exclude(self):
        profiles = self.config.get_option("profiles")
        test = profiles["test_4"]

        self.assertEqual({"B101"}, test["exclude"])

    def test_converted_blacklist_call_data(self):
        profiles = self.config.get_option("profiles")
        test = profiles["test_2"]
        data = {
            "Call": [
                {
                    "qualnames": ["telnetlib"],
                    "level": "HIGH",
                    "message": "{name} is considered insecure.",
                    "name": "telnet",
                }
            ]
        }

        self.assertEqual(data, test["blacklist"])

    def test_converted_blacklist_import_data(self):
        profiles = self.config.get_option("profiles")
        test = profiles["test_3"]
        data = [
            {
                "message": "{name} library appears to be in use.",
                "name": "pickle",
                "qualnames": ["pickle.loads"],
            }
        ]

        self.assertEqual(data, test["blacklist"]["Call"])
        self.assertEqual(data, test["blacklist"]["Import"])
        self.assertEqual(data, test["blacklist"]["ImportFrom"])

    def test_converted_blacklist_call_test(self):
        profiles = self.config.get_option("profiles")
        test = profiles["test_2"]

        self.assertEqual({"B001"}, test["include"])

    def test_converted_blacklist_import_test(self):
        profiles = self.config.get_option("profiles")
        test = profiles["test_3"]

        self.assertEqual({"B001"}, test["include"])

    def test_converted_exclude_blacklist(self):
        profiles = self.config.get_option("profiles")
        test = profiles["test_5"]

        self.assertEqual({"B001"}, test["exclude"])

    def test_deprecation_message(self):
        msg = (
            "Config file '%s' contains deprecated legacy config data. "
            "Please consider upgrading to the new config format. The tool "
            "'bandit-config-generator' can help you with this. Support for "
            "legacy configs will be removed in a future bandit version."
        )

        with mock.patch("bandit.core.config.LOG.warning") as m:
            self.config._config = {"profiles": {}}
            self.config.validate("")
            self.assertEqual((msg, ""), m.call_args_list[0][0])

    def test_blacklist_error(self):
        msg = (
            " : Config file has an include or exclude reference to legacy "
            "test '%s' but no configuration data for it. Configuration "
            "data is required for this test. Please consider switching to "
            "the new config file format, the tool "
            "'bandit-config-generator' can help you with this."
        )

        for name in [
            "blacklist_call",
            "blacklist_imports",
            "blacklist_imports_func",
        ]:
            self.config._config = {"profiles": {"test": {"include": [name]}}}
            try:
                self.config.validate("")
            except utils.ConfigError as e:
                self.assertEqual(msg % name, e.message)

    def test_bad_yaml(self):
        f = self.useFixture(TempFile("[]"))
        try:
            self.config = config.BanditConfig(f.name)
        except utils.ConfigError as e:
            self.assertIn("Error parsing file.", e.message)


class TestTomlConfig(TestConfigCompat):
    sample = textwrap.dedent(
        """
        [tool.bandit.profiles.test_1]
        include = [
            "any_other_function_with_shell_equals_true",
            "assert_used",
        ]

        [tool.bandit.profiles.test_2]
        include = ["blacklist_calls"]

        [tool.bandit.profiles.test_3]
        include = ["blacklist_imports"]

        [tool.bandit.profiles.test_4]
        exclude = ["assert_used"]

        [tool.bandit.profiles.test_5]
        exclude = ["blacklist_calls", "blacklist_imports"]

        [tool.bandit.profiles.test_6]
        include = ["blacklist_calls"]
        exclude = ["blacklist_imports"]

        [[tool.bandit.blacklist_calls.bad_name_sets]]
            [tool.bandit.blacklist_calls.bad_name_sets.pickle]
            qualnames = ["pickle.loads"]
            message = "{func} library appears to be in use."

        [[tool.bandit.blacklist_imports.bad_import_sets]]
            [tool.bandit.blacklist_imports.bad_import_sets.telnet]
            imports = ["telnetlib"]
            level = "HIGH"
            message = "{module} is considered insecure."
        """
    )
    suffix = ".toml"
