import os
import re
import unittest
from unittest import mock

import httptest
import requests

from atlassian_jwt_auth.key import (
    HTTPSPublicKeyRetriever,
    HTTPSMultiRepositoryPublicKeyRetriever,
    PEM_FILE_TYPE,
)
from atlassian_jwt_auth.tests import utils


def get_expected_and_os_proxies_dict(proxy_location):
    """ returns expected proxy & environmental
        proxy dictionary based upon the provided proxy location.
    """
    expected_proxies = {
        'http': proxy_location,
        'https': proxy_location,
    }
    os_proxy_dict = {
        'HTTP_PROXY': proxy_location,
        'HTTPS_PROXY': proxy_location
    }
    return expected_proxies, os_proxy_dict


class BaseHTTPSPublicKeyRetrieverTest(object):
    """ tests for the HTTPSPublicKeyRetriever class. """

    def create_retriever(self, url):
        """ returns a public key retriever created using the given url. """
        return HTTPSPublicKeyRetriever(url)

    def setUp(self):
        self._private_key_pem = self.get_new_private_key_in_pem_format()
        self._public_key_pem = utils.get_public_key_pem_for_private_key_pem(
            self._private_key_pem)
        self.base_url = 'https://example.com'

    def test_https_public_key_retriever_does_not_support_http_url(self):
        """ tests that HTTPSPublicKeyRetriever does not support http://
            base urls.
        """
        with self.assertRaises(ValueError):
            self.create_retriever('http://example.com')

    def test_https_public_key_retriever_does_not_support_none_url(self):
        """ tests that HTTPSPublicKeyRetriever does not support None
            base urls.
        """
        with self.assertRaises(ValueError):
            self.create_retriever(None)

    def test_https_public_key_retriever_session_uses_env_proxy(self):
        """ tests that the underlying session makes use of environmental
            proxy configured.
        """
        proxy_location = 'https://example.proxy'
        expected_proxies, proxy_dict = get_expected_and_os_proxies_dict(
            proxy_location)
        with mock.patch.dict(os.environ, proxy_dict, clear=True):
            retriever = self.create_retriever(self.base_url)
            key_retrievers = [retriever]
            if isinstance(retriever, HTTPSMultiRepositoryPublicKeyRetriever):
                key_retrievers = retriever._retrievers
            for key_retriever in key_retrievers:
                self.assertEqual(key_retriever._proxies, expected_proxies)

    def test_https_public_key_retriever_supports_https_url(self):
        """ tests that HTTPSPublicKeyRetriever supports https://
            base urls.
        """
        self.create_retriever(self.base_url)

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve(self, mock_get_method):
        """ tests that the retrieve method works expected. """
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem)
        retriever = self.create_retriever(self.base_url)
        self.assertEqual(
            retriever.retrieve('example/eg'),
            self._public_key_pem)

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve_with_proxy(self, mock_get_method):
        """ tests that the retrieve method works as expected when a proxy
            should be used.
        """
        proxy_location = 'https://example.proxy'
        key_id = 'example/eg'
        expected_proxies, proxy_dict = get_expected_and_os_proxies_dict(
            proxy_location)
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem)
        with mock.patch.dict(os.environ, proxy_dict, clear=True):
            retriever = self.create_retriever(self.base_url)
            retriever.retrieve(key_id)
            mock_get_method.assert_called_once_with(
                '%s/%s' % (self.base_url, key_id),
                headers={'accept': PEM_FILE_TYPE},
                proxies=expected_proxies
            )

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve_with_proxy_explicitly_set(self, mock_get_method):
        """ tests that the retrieve method works as expected when a proxy
            should be used and has been explicitly provided.
        """
        proxy_location = 'https://example.proxy'
        explicit_proxy_location = 'https://explicit.proxy'
        key_id = 'example/eg'
        _, proxy_dict = get_expected_and_os_proxies_dict(proxy_location)
        expected_proxies, _ = get_expected_and_os_proxies_dict(
            explicit_proxy_location)
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem)
        with mock.patch.dict(os.environ, proxy_dict, clear=True):
            retriever = self.create_retriever(self.base_url)
            retriever.retrieve(key_id, proxies=expected_proxies)
            mock_get_method.assert_called_once_with(
                '%s/%s' % (self.base_url, key_id),
                headers={'accept': PEM_FILE_TYPE},
                proxies=expected_proxies
            )

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve_with_charset_in_content_type_h(self, mock_get_method):
        """ tests that the retrieve method works expected when there is
            a charset in the response content-type header.
        """
        headers = {'content-type': 'application/x-pem-file;charset=UTF-8'}
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem, headers)
        retriever = self.create_retriever(self.base_url)
        self.assertEqual(
            retriever.retrieve('example/eg'),
            self._public_key_pem)

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve_fails_with_different_content_type(self, mock_get_method):
        """ tests that the retrieve method fails when the response is for a
            media type that is not supported.
        """
        headers = {'content-type': 'different/not-supported'}
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem, headers)
        retriever = self.create_retriever(self.base_url)
        with self.assertRaises(ValueError):
            retriever.retrieve('example/eg')

    @mock.patch.object(requests.Session, 'get',
                       side_effect=requests.exceptions.HTTPError(
                           mock.Mock(response=mock.Mock(status_code=403)),
                           'forbidden'))
    def test_retrieve_fails_with_forbidden_error(self, mock_get_method):
        """ tests that the retrieve method fails when the response is an
        403 forbidden error.
        """
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem)
        retriever = self.create_retriever(self.base_url)
        with self.assertRaises(ValueError):
            retriever.retrieve('example/eg')


class CachedHTTPPublicKeyRetrieverTest(utils.ES256KeyTestMixin,
                                       unittest.TestCase):

    class HTTPPublicKeyRetriever(HTTPSPublicKeyRetriever):
        """A subclass of HTTPSPublicKeyRetriever that allows us to use plain
        HTTP during testing so we don't have to run an actual SSL server.
        """

        def __init__(self, base_url):
            # pretend to the super class that this is an HTTPS url
            super(CachedHTTPPublicKeyRetrieverTest.HTTPPublicKeyRetriever,
                  self).__init__(
                re.sub(r'^http', 'https', base_url, flags=re.IGNORECASE))
            self.base_url = base_url

    def setUp(self):
        super(CachedHTTPPublicKeyRetrieverTest, self).setUp()
        self._private_key_pem = self.get_new_private_key_in_pem_format()
        self._public_key_pem = utils.get_public_key_pem_for_private_key_pem(
            self._private_key_pem)

    def test_http_caching(self):
        """Asserts that our use of requests properly caches keys between
        invocations across different `HTTPSPublicKeyRetriever` instances.
        """
        def wsgi(environ, start_response):
            print(environ['PATH_INFO'])
            start_response('200 OK', [
                ('content-type', 'application/x-pem-file;charset=UTF-8'),
                ('Cache-Control', 'public,max-age=300,stale-while-revalidate='
                                  '300,stale-if-error=300'),
                ('Last-Modified', 'Sun, 18 Jan 1970 18:14:21 GMT')])
            return [self._public_key_pem]

        with httptest.testserver(wsgi) as server:

            retriever = self.HTTPPublicKeyRetriever(server.url())
            retriever.retrieve('example/eg')

            retriever = self.HTTPPublicKeyRetriever(server.url())
            retriever.retrieve('example/eg')

            self.assertEqual(1, len(server.log()),
                             msg='HTTP caching should suppress second GET')


class BaseHTTPSMultiRepositoryPublicKeyRetrieverTest(
        BaseHTTPSPublicKeyRetrieverTest):
    """ tests for the HTTPSMultiRepositoryPublicKeyRetriever class. """

    def create_retriever(self, url):
        """ returns a public key retriever created using the given url. """
        return HTTPSMultiRepositoryPublicKeyRetriever([url])

    def setUp(self):
        self._private_key_pem = self.get_new_private_key_in_pem_format()
        self._public_key_pem = utils.get_public_key_pem_for_private_key_pem(
            self._private_key_pem)
        self.keystore_urls = ['https://example.com', 'https://example.ly']
        self.base_url = self.keystore_urls[0]

    def test_https_multi_public_key_retriever_does_not_support_strings(self):
        """ tests that HTTPSMultiRepositoryPublicKeyRetriever does not
            support a string key repository url.
        """
        with self.assertRaises(TypeError):
            HTTPSMultiRepositoryPublicKeyRetriever('https://example.com')

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve(self, mock_get_method):
        """ tests that the retrieve method works expected. """
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem)
        retriever = HTTPSMultiRepositoryPublicKeyRetriever(self.keystore_urls)
        self.assertEqual(
            retriever.retrieve('example/eg'),
            self._public_key_pem)

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve_with_500_error(self, mock_get_method):
        """ tests that the retrieve method works as expected
            when the first key repository returns a server error response.
        """
        retriever = HTTPSMultiRepositoryPublicKeyRetriever(self.keystore_urls)
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem)
        valid_response = mock_get_method.return_value
        del mock_get_method.return_value
        server_exception = requests.exceptions.HTTPError(
            response=mock.Mock(status_code=500))
        mock_get_method.side_effect = [server_exception, valid_response]
        self.assertEqual(
            retriever.retrieve('example/eg'),
            self._public_key_pem)

    @mock.patch.object(requests.Session, 'get')
    def test_retrieve_with_connection_error(self, mock_get_method):
        """ tests that the retrieve method works as expected
            when the first key repository encounters a connection error.
        """
        retriever = HTTPSMultiRepositoryPublicKeyRetriever(self.keystore_urls)
        _setup_mock_response_for_retriever(
            mock_get_method, self._public_key_pem)
        valid_response = mock_get_method.return_value
        del mock_get_method.return_value
        connection_exception = requests.exceptions.ConnectionError(
            response=mock.Mock(status_code=None))
        mock_get_method.side_effect = [connection_exception, valid_response]
        self.assertEqual(
            retriever.retrieve('example/eg'),
            self._public_key_pem)


def _setup_mock_response_for_retriever(
        mock_method, public_key_pem, headers=None):
    """ returns a setup mock response for use with a https public key
        retriever.
    """
    if headers is None:
        headers = {'content-type': 'application/x-pem-file'}
    mock_response = mock.Mock()
    mock_response.headers = headers
    mock_response.text = public_key_pem
    mock_method.return_value = mock_response
    return mock_method


class HTTPSPublicKeyRetrieverRS256Test(BaseHTTPSPublicKeyRetrieverTest,
                                       utils.RS256KeyTestMixin,
                                       unittest.TestCase):
    pass


class HTTPSPublicKeyRetrieverES256Test(BaseHTTPSPublicKeyRetrieverTest,
                                       utils.ES256KeyTestMixin,
                                       unittest.TestCase):
    pass


class HTTPSMultiRepositoryPublicKeyRetrieverRS256Test(
        BaseHTTPSMultiRepositoryPublicKeyRetrieverTest,
        utils.RS256KeyTestMixin,
        unittest.TestCase):
    pass


class HTTPSMultiRepositoryPublicKeyRetrieverES256Test(
        BaseHTTPSMultiRepositoryPublicKeyRetrieverTest,
        utils.ES256KeyTestMixin,
        unittest.TestCase):
    pass
