# 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.

"""Tests for debusine.tests.TestHelpersMixin."""
import logging
import os
from configparser import ConfigParser
from unittest import TestCase

from asgiref.sync import async_to_sync

import requests

import responses

from rest_framework import status
from rest_framework.response import Response

from debusine.test import TestHelpersMixin
from debusine.test.django import ChannelsHelpersMixin


class TestChannelsHelpersMixinTests(ChannelsHelpersMixin, TestCase):
    """Tests for methods in ChannelsHelpersMixin."""

    def setUp(self):
        """Set up default channel to be used during the tests."""
        self.channel_name = "generic-channel-for-testing"
        self.channel = self.create_channel("generic-channel-for-testing")

    def test_create_channel(self):
        """Create channel return a dictionary with layer and name keys."""
        channel = self.create_channel("channel-test")

        self.assertEqual(channel.keys(), {"layer", "name"})

    def test_assert_channel_received_raises_exception(self):
        """assert_channel_received raise exception: nothing was received."""
        with self.assertRaisesRegex(
            self.failureException,
            "^Expected '{'type': 'work_request'}' received nothing$",
        ):
            self.assert_channel_received(self.channel, {"type": "work_request"})

    def test_assert_channel_received_raise_wrong_data(self):
        """assert_channel_received raise exception: unexpected data received."""
        message = {"type": "work_request.assigned"}
        async_to_sync(self.channel["layer"].group_send)(
            self.channel_name,
            {"some other message": "values"},
        )

        with self.assertRaises(AssertionError):
            self.assert_channel_received(self.channel, message)

    def test_assert_channel_received_do_not_raise(self):
        """assert_channel_received does not raise an exception."""
        message = {"type": "work_request.assigned"}
        async_to_sync(self.channel["layer"].group_send)(
            self.channel_name,
            message,
        )

        self.assert_channel_received(self.channel, message)

    def test_assert_channel_nothing_received_do_not_raise(self):
        """assert_channel_nothing_received does not raise an exception."""
        self.assert_channel_nothing_received(self.channel)

    def test_assert_channel_nothing_receive_raise(self):
        """assert_channel_nothing_received raise exception: data is received."""
        message = {"type": "work_request.assigned"}

        async_to_sync(self.channel["layer"].group_send)(
            self.channel_name,
            message,
        )

        with self.assertRaisesRegex(
            self.failureException,
            "^Expected nothing. Received: '{'type': 'work_request.assigned'}'$",
        ):
            self.assert_channel_nothing_received(self.channel)


class TestHelpersMixinTests(TestHelpersMixin, TestCase):
    """Tests for methods in TestHelpersMixin."""

    def test_create_temp_config_directory(self):
        """create_temp_config_directory write the configuration."""
        config = {
            'General': {'default-server': 'debian'},
            'server:debian': {
                'url': 'https://debusine.debian.org',
                'token': 'token-for-debian',
            },
        }
        directory = self.create_temp_config_directory(config)

        actual_config = ConfigParser()
        actual_config.read(os.path.join(directory, 'config.ini'))

        expected_config = ConfigParser()
        expected_config.read_dict(config)

        self.assertEqual(actual_config, expected_config)

    def test_assert_dict_contains_subset_raises_exception(self):
        """Raise an exception (subset not in dictionary)."""
        expected_message = "{'b': 1} does not contain the subset {'a': 1}"
        with self.assertRaisesRegex(self.failureException, expected_message):
            self.assertDictContainsSubset({'b': 1}, {'a': 1})

    def test_assert_dict_use_error_msg(self):
        """Raise exception using a specific error message."""
        expected_message = 'Missing values'

        with self.assertRaisesRegex(self.failureException, expected_message):
            self.assertDictContainsSubset({}, {'a': 1}, expected_message)

    def test_assert_dict_contains_subset_does_not_raise_exception(self):
        """Do not raise any exception (subset in dictionary)."""
        self.assertDictContainsSubset({'a': 1, 'b': 2}, {'a': 1})

    def test_assert_dict_contains_subset_arg1_not_a_dictionary(self):
        """Raise exception because of wrong type argument 1."""
        expected_message = (
            "'a' is not an instance of <class 'dict'> : "
            "First argument is not a dictionary"
        )
        with self.assertRaisesRegex(self.failureException, expected_message):
            self.assertDictContainsSubset('a', {})

    def test_assert_dict_contains_subset_arg2_not_a_dictionary(self):
        """Raise exception because of wrong type argument 2."""
        expected_message = (
            "'b' is not an instance of <class 'dict'> : "
            "Second argument is not a dictionary"
        )
        with self.assertRaisesRegex(self.failureException, expected_message):
            self.assertDictContainsSubset({}, 'b')

    def test_assert_raises_system_exit_no_system_exit(self):
        """Raise self.failureException because missing SystemExit."""
        expected_message = (
            r'SystemExit not raised : Did not raise '
            r'SystemExit with exit_code=\^3\$'
        )
        with self.assertRaisesRegex(self.failureException, expected_message):
            with self.assertRaisesSystemExit(3):
                pass

    def test_assert_raises_system_exit_unexpected_exit_code(self):
        """Raise self.failureException because wrong exit_code in SystemExit."""
        expected_message = (
            r'\^3\$" does not match "7" : Did not raise '
            r'SystemExit with exit_code=\^3\$'
        )
        with self.assertRaisesRegex(self.failureException, expected_message):
            with self.assertRaisesSystemExit(3):
                raise SystemExit(7)

    def test_assert_raises_system_exit_success(self):
        """Do not raise self.failureException: expected SystemExit is raised."""
        with self.assertRaisesSystemExit(3):
            raise SystemExit(3)

    @responses.activate
    def test_assert_token_key_included_in_all_requests(self):
        """Do not raise any exception (all requests had the token key)."""
        responses.add(
            responses.GET,
            'https://example.net/something',
        )

        token_key = 'some-key'

        requests.get(
            'https://example.net/something',
            headers={'Token': token_key},
        )

        self.assert_token_key_included_in_all_requests(token_key)

    @responses.activate
    def test_assert_token_key_included_in_all_requests_raise_missing(self):
        """Raise exception because token not included in the request."""
        responses.add(
            responses.GET,
            'https://example.net/something',
        )

        requests.get('https://example.net/something')

        expected_message = (
            "Token missing in the headers for the request "
            "'https://example.net/something'"
        )

        with self.assertRaisesRegex(self.failureException, expected_message):
            self.assert_token_key_included_in_all_requests('some-token')

    @responses.activate
    def test_assert_token_key_included_in_all_requests_raise_mismatch(self):
        """Raise exception because token mismatch included in the request."""
        responses.add(
            responses.GET,
            'https://example.net/something',
        )

        token = 'token-for-server'

        requests.get(
            'https://example.net/something',
            headers={'Token': 'some-invalid-token'},
        )

        expected_message = (
            "Unexpected token. In the request: "
            "'https://example.net/something' "
            "Actual: 'some-invalid-token' Expected: 'token-for-server'"
        )

        with self.assertRaisesRegex(self.failureException, expected_message):
            self.assert_token_key_included_in_all_requests(token)

    def test_create_enabled_token(self):
        """create_token_enabled() returns a token that is enabled."""
        token = self.create_token_enabled()
        self.assertTrue(token.enabled)

    def test_assertLogsContains_log_found(self):
        """assertLogsContains() does not raise self.failureException."""
        with self.assertLogsContains('required-log') as logs:
            logging.warning('required-log')

        self.assertEqual(logs.output, ["WARNING:root:required-log"])

    def test_assertLogsContains_log_expected_count_wrong(self):
        """assertLogsContains() raise self.failureException (wrong count)."""
        expected_message = (
            '^Expected: "required-log"\n'
            'Actual: "WARNING:root:required-log"\n'
            'Expected msg found 1 times, expected 2 times$'
        )

        with self.assertRaisesRegex(self.failureException, expected_message):
            with self.assertLogsContains('required-log', expected_count=2):
                logging.warning('required-log')

    def test_assertLogsContains_log_expected_not_found(self):
        """assertLogsContains() raise self.failureException (wrong count)."""
        expected_message = (
            '^Expected: "required-log"\n'
            'Actual: "WARNING:root:some-log"\n'
            'Expected msg found 0 times, expected 1 times$'
        )

        with self.assertRaisesRegex(self.failureException, expected_message):
            with self.assertLogsContains('required-log'):
                logging.warning('some-log')

    def test_assertLogsContains_log_expected_not_found_wrong_level(self):
        """assertLogsContains() raise self.failureException (wrong level)."""
        expected_message = (
            'no logs of level WARNING or higher triggered on root'
        )

        with self.assertRaisesRegex(self.failureException, expected_message):
            with self.assertLogsContains('required-log', level=logging.WARNING):
                logging.debug('some-log')

    def test_assertLogsContains_log_not_found_in_raise_exception(self):
        """
        assertLogsContains() raise self.failureException.

        It handles raised exceptions in the context code.
        """
        expected_message = (
            '^Expected: "The wanted message"\n'
            'Actual: "WARNING:root:Unrelated message"\n'
            'Expected msg found 0 times, expected 1 times$'
        )

        with self.assertRaisesRegex(self.failureException, expected_message):
            with self.assertRaisesRegex(
                SystemExit, '3'
            ), self.assertLogsContains('The wanted message'):
                logging.warning('Unrelated message')
                raise SystemExit(3)

    def test_assertResponseProblem_valid(self):
        """assertResponseProblem() does not raise any exception."""
        problem = {
            "title": "Invalid task name",
            "detail": "Task name sbuild unrecognised",
        }
        response = Response(
            problem,
            status=status.HTTP_400_BAD_REQUEST,
            content_type="application/problem+json",
        )

        self.assertResponseProblem(
            response, problem["title"], "Task name .* unrecognised"
        )

    def test_assertResponseProblem_assertions(self):
        """Exercise all the checks done by assertResponseProblem()."""
        data = {"title": "The title", "detail": "The detail"}
        status_400 = status.HTTP_400_BAD_REQUEST
        status_content = {
            "status": status_400,
            "content_type": "application/problem+json",
        }

        params = [
            {
                "response": Response({}, status.HTTP_200_OK),
                "assert_params": ["title", "detail"],
                "expected_regex": "response status 200 != 400",
            },
            {
                "response": Response(status=status_400, content_type="html"),
                "assert_params": ["title", "detail"],
                "expected_regex": r"application/problem\+json",
            },
            {
                "response": Response({"detail": "detail"}, **status_content),
                "assert_params": ["Something", "The detail"],
                "expected_regex": '"title" not found in response',
            },
            {
                "response": Response(data, **status_content),
                "assert_params": ["Another title", "A detail"],
                "expected_regex": "title",
            },
            {
                "response": Response(data, **status_content),
                "assert_params": [data["title"]],
                "expected_regex": '"detail" is in the response',
            },
            {
                "response": Response({"title": "title"}, **status_content),
                "assert_params": ["title", "detail"],
                "expected_regex": '"detail" not found in response',
            },
            {
                "response": Response(data, **status_content),
                "assert_params": [data["title"], "two"],
                "expected_regex": "Detail regexp",
            },
        ]

        for param in params:
            with self.subTest():
                with self.assertRaisesRegex(
                    self.failureException, param["expected_regex"]
                ):
                    self.assertResponseProblem(
                        param["response"], *param["assert_params"]
                    )
