#  Copyright 2022 The Debusine developers
#  See the AUTHORS file at the top-level directory of this distribution
#
#  This file is part of Debusine. It is subject to the license terms
#  in the LICENSE file found in the top-level directory of this
#  distribution. No part of Debusine, including this file, may be copied,
#  modified, propagated, or distributed except according to the terms
#  contained in the LICENSE file.
"""Common test-helper code."""
import contextlib
import os
import shutil
import tempfile
from configparser import ConfigParser
from unittest.util import safe_repr

import requests

import responses


class BaseTestHelpersMixin:
    """
    Collection of methods to help write unit tests.

    This mixin-class provides different assert statements that can be handy
    when writing unit tests.
    """

    def create_temp_config_directory(self, config: dict):
        """
        Create a temp directory with a config.ini file inside.

        The method also register the automatic removal of said directory.
        """
        temp_directory = tempfile.mkdtemp()
        config_file_name = os.path.join(temp_directory, 'config.ini')
        with open(config_file_name, 'w') as config_file:
            config_writer = ConfigParser()

            for section, values in config.items():
                config_writer[section] = values

            config_writer.write(config_file)

        self.addCleanup(shutil.rmtree, temp_directory)

        return temp_directory

    def assertDictContainsSubset(self, dictionary, subset, msg=None):
        """
        Implement a replacement of deprecated TestCase.assertDictContainsSubset.

        Assert that the keys and values of subset is in dictionary.

        The order of the arguments in TestCase.assertDictContainsSubset
        and this implementation differs.
        """
        self.assertIsInstance(
            dictionary, dict, 'First argument is not a dictionary'
        )
        self.assertIsInstance(
            subset, dict, 'Second argument is not a dictionary'
        )

        if dictionary != dictionary | subset:
            msg = self._formatMessage(
                msg,
                '%s does not contain the subset %s'
                % (safe_repr(dictionary), safe_repr(subset)),
            )

            raise self.failureException(msg)

    def assert_token_key_included_in_all_requests(self, expected_token):
        """Assert that the requests in responses.calls had the Token."""
        for call in responses.calls:
            headers = call.request.headers

            if 'Token' not in headers:
                raise self.failureException(
                    'Token missing in the headers for '
                    'the request %s' % (safe_repr(call.request.url))
                )

            if (actual_token := headers['Token']) != expected_token:
                raise self.failureException(
                    'Unexpected token. In the request: %s Actual: %s '
                    'Expected: %s'
                    % (
                        safe_repr(call.request.url),
                        safe_repr(actual_token),
                        safe_repr(expected_token),
                    )
                )

    @contextlib.contextmanager
    def assertRaisesSystemExit(self, exit_code):
        """Assert that raises SystemExit with the specific exit_code."""
        with self.assertRaisesRegex(
            SystemExit,
            rf'^{exit_code}$',
            msg=f'Did not raise SystemExit with exit_code=^{exit_code}$',
        ):
            yield

    @contextlib.contextmanager
    def assertLogsContains(
        self, message, expected_count=1, **assert_logs_kwargs
    ):
        """
        Raise failureException if message is not in the logs.

        Yields the same context manager as self.assertLogs(). This allows
        further checks in the logs.

        :param message: message to find in the logs
        :param expected_count: expected times that the message
          must be in the logs
        :param assert_logs_kwargs: arguments for self.assertLogs()
        """

        def failure_exception_if_needed(logs, message, expected_count):
            all_logs = '\n'.join(logs.output)

            actual_times = all_logs.count(message)

            if actual_times != expected_count:
                raise self.failureException(
                    'Expected: "%s"\n'
                    'Actual: "%s"\n'
                    'Expected msg found %s times, expected %s times'
                    % (message, all_logs, actual_times, expected_count)
                )

        with self.assertLogs(**assert_logs_kwargs) as logs:
            try:
                yield logs
            except BaseException as exc:
                failure_exception_if_needed(logs, message, expected_count)
                raise exc

        failure_exception_if_needed(logs, message, expected_count)

    def assertResponseProblem(
        self,
        response,
        title,
        detail_pattern=None,
        status_code=requests.codes.bad_request,
    ):
        """
        Assert that response is a valid application/problem+json.

        Assert that the content_type is application/problem+json and the
        title exist and matches title.

        :param status_code: assert response.status_code == status_code
        :param title: exact match with response.data["title"]
        :param detail_pattern: if not None: assertRegex with
           response.data["detail"]. If None checks that response.data does not
           contain "detail".
        """
        self.assertEqual(
            response.status_code,
            status_code,
            f"response status {response.status_code} != {status_code}",
        )
        self.assertEqual(
            response.content_type,
            "application/problem+json",
            f'content_type {response.content_type} != '
            f'"application/problem+json"',
        )

        self.assertIn("title", response.data, '"title" not found in response')

        response_title = response.data["title"]
        self.assertEqual(
            response_title, title, f'title "{response_title}" != "{title}"'
        )

        if detail_pattern is not None:
            self.assertIn(
                "detail", response.data, '"detail" not found in response'
            )

            response_detail = response.data["detail"]
            self.assertRegex(
                response_detail,
                detail_pattern,
                f'Detail regexp "{detail_pattern}" did not '
                f'match "{response_detail}"',
            )
        else:
            self.assertNotIn(
                "detail", response.data, '"detail" is in the response'
            )


if "DJANGO_SETTINGS_MODULE" in os.environ:
    from debusine.test.django import DatabaseHelpersMixin

    class TestHelpersMixin(BaseTestHelpersMixin, DatabaseHelpersMixin):
        """
        Collection of methods to help write unit tests.

        This mixin-class provides different methods that can be handy
        when writing unit tests.
        """

else:

    class TestHelpersMixin(BaseTestHelpersMixin):
        """
        Collection of methods to help write unit tests.

        This mixin-class provides different methods that can be handy
        when writing unit tests.
        """
