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

"""Unit tests for the Debusine class."""
import json
from unittest import TestCase

import requests

import responses

from debusine.client import exceptions
from debusine.client.debusine import Debusine
from debusine.client.models import WorkRequest
from debusine.test import TestHelpersMixin


class DebusineTests(TestHelpersMixin, TestCase):
    """Tests for the Debusine class."""

    def setUp(self) -> None:
        """Initialize tests."""
        self.api_url = "https://debusine.debian.org/api"
        self.api_test_url = f"{self.api_url}/1.0/test-endpoint"

        self.post_work_request_response = {
            'id': 11,
            'task_name': 'sbuild',
            'status': 'pending',
        }

    def responses_add_test_endpoint(self, status):
        """
        Add a response (requests.get() will receive this response).

        Used for testing the debusine API client low level methods.
        """
        responses.add(
            responses.GET,
            self.api_test_url,
            json={'id': 12},
            status=status,
        )

    @responses.activate
    def test_work_request_status(self):
        """Debusine.work_request_status returns the status of a work request."""
        responses.add(
            responses.GET,
            f'{self.api_url}/1.0/work-request/10/',
            json={'id': 10, 'status': 'running'},
            status=requests.codes.ok,
        )

        debusine = Debusine(self.api_url, 'token')

        self.assertEqual(
            debusine.work_request_status(10),
            WorkRequest(id=10, status='running'),
        )
        self.assert_token_key_included_in_all_requests('token')

    @responses.activate
    def test_work_request_create_success(self):
        """Post a new work request."""
        responses.add(
            responses.POST,
            f'{self.api_url}/1.0/work-request/',
            json=self.post_work_request_response,
            status=requests.codes.ok,
        )
        debusine = Debusine(self.api_url, 'token-for-debian')

        work_request_to_post = WorkRequest(task_name='sbuild')
        actual_work_request = debusine.work_request_create(work_request_to_post)
        expected_work_request = WorkRequest(**self.post_work_request_response)

        self.assertEqual(actual_work_request, expected_work_request)

        self.assert_token_key_included_in_all_requests('token-for-debian')
        self.assertEqual(
            json.loads(responses.calls[0].request.body),
            work_request_to_post.dict(include={'task_name', 'task_data'}),
        )

    @responses.activate
    def test_api_request_not_found_raise_exception(self):
        """Raise WorkRequestNotFound if the request returns 404."""
        self.responses_add_test_endpoint(status=requests.codes.not_found)
        debusine = Debusine(self.api_url, 'token-for-debian')
        with self.assertRaises(exceptions.NotFoundError):
            debusine._api_request('GET', self.api_test_url, requests.codes.ok)

        self.assert_token_key_included_in_all_requests('token-for-debian')

    @responses.activate
    def test_api_request_returns_not_json_raise_exception(self):
        """Raise UnexpectedResponseError if body is not a valid JSON."""
        responses.add(
            responses.GET,
            self.api_test_url,
            body='Something that is not JSON as client expects',
        )
        debusine = Debusine(self.api_url, 'token-for-debian')
        with self.assertRaisesRegex(
            exceptions.UnexpectedResponseError,
            fr'^Server did not return valid object \({self.api_test_url}\)$',
        ):
            debusine._api_request('GET', self.api_test_url, WorkRequest)

        self.assert_token_key_included_in_all_requests('token-for-debian')

    @responses.activate
    def test_api_request_raise_client_connection_error(self):
        """Raise ClientConnectionError for RequestException/ConnectionError."""
        debusine = Debusine(self.api_url, 'token-for-debian')

        for exception in (
            requests.exceptions.RequestException,
            ConnectionError,
        ):
            with self.subTest(exception=exception):
                responses.add(
                    responses.GET,
                    self.api_test_url,
                    body=exception(),
                )

                with self.assertRaisesRegex(
                    exceptions.ClientConnectionError,
                    rf'^Cannot connect to {self.api_test_url}. Error: $',
                ):
                    debusine._api_request('GET', self.api_test_url, WorkRequest)

    @responses.activate
    def test_api_request_raise_unexpected_error(self):
        """Raise UnexpectedResponseError for unexpected status code."""
        self.responses_add_test_endpoint(status=requests.codes.teapot)
        debusine = Debusine(self.api_test_url, 'token-for-debian')
        with self.assertRaisesRegex(
            exceptions.UnexpectedResponseError,
            r'^Server returned unexpected status code: 418 '
            rf'\({self.api_test_url}\)$',
        ):
            debusine._api_request('GET', self.api_test_url, WorkRequest)

        self.assert_token_key_included_in_all_requests('token-for-debian')

    @responses.activate
    def test_api_request_raise_token_disabled_error(self):
        """Raise TokenDisabledError: server returned HTTP 403."""
        responses.add(
            responses.GET,
            self.api_test_url,
            status=requests.codes.forbidden,
        )

        token = 'b3ecf243c43'
        debusine = Debusine(self.api_test_url, token)
        with self.assertRaisesRegex(
            exceptions.ClientForbiddenError,
            rf'^HTTP 403. Token \({token}\) is invalid or disabled$',
        ):
            debusine._api_request('GET', self.api_test_url, WorkRequest)

    @responses.activate
    def test_api_request_raise_debusine_error(self):
        """Raise DebusineError: server returned HTTP 400 and error message."""
        detail = "Invalid task_name"
        errors = {"field": "invalid value"}

        responses.add(
            responses.GET,
            self.api_test_url,
            json={
                "title": "Cannot update task",
                "detail": detail,
                "errors": errors,
            },
            content_type="application/problem+json",
            status=requests.codes.bad_request,
        )
        debusine = Debusine(self.api_test_url, 'token-for-debian')
        with self.assertRaises(exceptions.DebusineError) as debusine_error:
            debusine._api_request('GET', self.api_test_url, WorkRequest)

        exception = debusine_error.exception

        self.assertEqual(
            exception.problem,
            {"title": "Cannot update task", "detail": detail, "errors": errors},
        )

    @responses.activate
    def test_api_request_raise_unexpected_response_400_no_error_msg(self):
        """Raise UnexpectedError: server returned HTTP 400 and no error msg."""
        responses.add(
            responses.GET,
            self.api_test_url,
            json={'foo': 'bar'},
            status=requests.codes.bad_request,
        )
        debusine = Debusine(self.api_test_url, 'token-for-debian')
        with self.assertRaisesRegex(
            exceptions.UnexpectedResponseError,
            '^Server returned unexpected status code: 400 '
            fr'\({self.api_test_url}\)$',
        ):
            debusine._api_request('GET', self.api_test_url, WorkRequest)

    @responses.activate
    def test_api_request_raise_unexpected_response_400_no_json(self):
        """Raise UnexpectedError: server returned HTTP 400 and no JSON."""
        responses.add(
            responses.GET,
            self.api_test_url,
            body='Not including JSON',
            status=requests.codes.bad_request,
        )
        debusine = Debusine(self.api_test_url, 'token-for-debian')
        with self.assertRaisesRegex(
            exceptions.UnexpectedResponseError,
            '^Server returned unexpected status code: 400 '
            fr'\({self.api_test_url}\)$',
        ):
            debusine._api_request('GET', self.api_test_url, WorkRequest)

    @responses.activate
    def test_api_request_submit_data(self):
        """Data is submitted to the server in a POST request."""
        responses.add(
            responses.POST,
            self.api_test_url,
            status=requests.codes.ok,
            json={'id': 14},
        )
        debusine = Debusine(self.api_test_url, 'token-for-debian')

        data = {'foo': 'bar'}

        debusine._api_request('POST', self.api_test_url, WorkRequest, data=data)

        self.assert_token_key_included_in_all_requests('token-for-debian')
        self.assertEqual(json.loads(responses.calls[0].request.body), data)

    @responses.activate
    def test_debusine_error_details_combinations_everything(self):
        """Test combinations of status and body for _debusine_error_details."""
        json_with_error_message = {'title': 'error message'}
        no_json = 'There is no JSON'

        content_type_problem = 'application/problem+json'
        content_type_not_problem = 'application/json'

        all_tests = [
            {
                'body': json_with_error_message,
                'expected': json_with_error_message,
                'content_type': content_type_problem,
            },
            {
                'body': json_with_error_message,
                'expected': None,
                'content_type': content_type_not_problem,
            },
            {
                'body': no_json,
                'expected': None,
                'content_type': content_type_problem,
            },
            {
                'body': json_with_error_message,
                'expected': None,
                'content_type': content_type_not_problem,
            },
            {
                'body': no_json,
                'expected': None,
                'content_type': content_type_problem,
            },
        ]

        for test in all_tests:
            with self.subTest(test=test):
                responses.reset()
                kwargs = {'content_type': test['content_type']}

                if isinstance(test['body'], dict):
                    kwargs['json'] = test['body']
                else:
                    kwargs['body'] = test['body']

                responses.add(responses.GET, self.api_test_url, **kwargs)
                response = requests.get(self.api_test_url)

                error = Debusine._debusine_problem(response)

                self.assertEqual(error, test['expected'])

    def test_api_request_get_with_body_raise_value_error(self):
        """Raise ValueError if passing a body for the GET method."""
        debusine = Debusine(self.api_test_url, 'token-for-debian')
        with self.assertRaisesRegex(
            ValueError, '^data argument not allowed with HTTP GET$'
        ):
            debusine._api_request(
                'GET', self.api_test_url, WorkRequest, data={}
            )

    def test_api_request_only_valid_methods(self):
        """Raise ValueError if the method is not recognized."""
        debusine = Debusine(self.api_test_url, 'token-for-debian')
        with self.assertRaisesRegex(
            ValueError, '^Method must be one of: GET, POST$'
        ):
            debusine._api_request('BAD-METHOD', self.api_test_url, WorkRequest)
