# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import json as jsonutils

from requests.adapters import HTTPAdapter
from requests.packages.urllib3.response import HTTPResponse
import six

from requests_mock import compat
from requests_mock import exceptions

_BODY_ARGS = frozenset(['raw', 'body', 'content', 'text', 'json'])
_HTTP_ARGS = frozenset(['status_code', 'reason', 'headers'])

_DEFAULT_STATUS = 200
_http_adapter = HTTPAdapter()


def _check_body_arguments(**kwargs):
    # mutual exclusion, only 1 body method may be provided
    provided = [x for x in _BODY_ARGS if kwargs.pop(x, None) is not None]

    if len(provided) > 1:
        raise RuntimeError('You may only supply one body element. You '
                           'supplied %s' % ', '.join(provided))

    extra = [x for x in kwargs if x not in _HTTP_ARGS]

    if extra:
        raise TypeError('Too many arguments provided. Unexpected '
                        'arguments %s.' % ', '.join(extra))


class _FakeConnection(object):
    """An object that can mock the necessary parts of a socket interface."""

    def send(self, request, **kwargs):
        msg = 'This response was created without a connection. You are ' \
              'therefore unable to make a request directly on that connection.'
        raise exceptions.InvalidRequest(msg)

    def close(self):
        pass


def create_response(request, **kwargs):
    """
    :param int status_code: The status code to return upon a successful
        match. Defaults to 200.
    :param HTTPResponse raw: A HTTPResponse object to return upon a
        successful match.
    :param io.IOBase body: An IO object with a read() method that can
        return a body on successful match.
    :param bytes content: A byte string to return upon a successful match.
    :param unicode text: A text string to return upon a successful match.
    :param object json: A python object to be converted to a JSON string
        and returned upon a successful match.
    :param dict headers: A dictionary object containing headers that are
        returned upon a successful match.
    """
    connection = kwargs.pop('connection', _FakeConnection())

    _check_body_arguments(**kwargs)

    raw = kwargs.pop('raw', None)
    body = kwargs.pop('body', None)
    content = kwargs.pop('content', None)
    text = kwargs.pop('text', None)
    json = kwargs.pop('json', None)
    encoding = None

    if content and not isinstance(content, six.binary_type):
        raise TypeError('Content should be a callback or binary data')
    if text and not isinstance(text, six.string_types):
        raise TypeError('Text should be a callback or string data')

    if json is not None:
        text = jsonutils.dumps(json)
    if text is not None:
        encoding = 'utf-8'
        content = text.encode(encoding)
    if content is not None:
        body = six.BytesIO(content)
    if not raw:
        raw = HTTPResponse(status=kwargs.get('status_code', _DEFAULT_STATUS),
                           headers=kwargs.get('headers', {}),
                           reason=kwargs.get('reason'),
                           body=body or six.BytesIO(six.b('')),
                           decode_content=False,
                           preload_content=False,
                           original_response=compat._fake_http_response)

    response = _http_adapter.build_response(request, raw)
    response.connection = connection
    response.encoding = encoding
    return response


class _Context(object):
    """Stores the data being used to process a current URL match."""

    def __init__(self, headers, status_code, reason):
        self.headers = headers
        self.status_code = status_code
        self.reason = reason


class _MatcherResponse(object):

    def __init__(self, **kwargs):
        self._exc = kwargs.pop('exc', None)

        # If the user is asking for an exception to be thrown then prevent them
        # specifying any sort of body or status response as it won't be used.
        # This may be protecting the user too much but can be removed later.
        if self._exc and kwargs:
            raise TypeError('Cannot provide other arguments with exc.')

        _check_body_arguments(**kwargs)
        self._params = kwargs

        # whilst in general you shouldn't do type checking in python this
        # makes sure we don't end up with differences between the way types
        # are handled between python 2 and 3.
        content = self._params.get('content')
        text = self._params.get('text')

        if content and not (callable(content) or
                            isinstance(content, six.binary_type)):
            raise TypeError('Content should be a callback or binary data')

        if text and not (callable(text) or
                         isinstance(text, six.string_types)):
            raise TypeError('Text should be a callback or string data')

    def get_response(self, request):
        # if an error was requested then raise that instead of doing response
        if self._exc:
            raise self._exc

        context = _Context(self._params.get('headers', {}).copy(),
                           self._params.get('status_code', _DEFAULT_STATUS),
                           self._params.get('reason'))

        # if a body element is a callback then execute it
        def _call(f, *args, **kwargs):
            return f(request, context, *args, **kwargs) if callable(f) else f

        return create_response(request,
                               json=_call(self._params.get('json')),
                               text=_call(self._params.get('text')),
                               content=_call(self._params.get('content')),
                               body=_call(self._params.get('body')),
                               raw=self._params.get('raw'),
                               status_code=context.status_code,
                               reason=context.reason,
                               headers=context.headers)
