from typing import Any, Iterable, Tuple, Type, Union

from boltons.urlutils import URL
from ereuse_utils.test import Client as EreuseUtilsClient, JSON, Res
from werkzeug.exceptions import HTTPException

from teal.marshmallow import ValidationError

Status = Union[int, Type[HTTPException], Type[ValidationError]]
Query = Iterable[Tuple[str, Any]]


class Client(EreuseUtilsClient):
    """A REST interface to a Teal app."""

    def open(self,
             uri: str,
             res: str = None,
             status: Status = 200,
             query: Query = tuple(),
             accept=JSON,
             content_type=JSON,
             item=None,
             headers: dict = None,
             token: str = None, **kw) -> Res:
        headers = headers or {}
        if res:
            resource_url = self.application.resources[res].url_prefix + '/'
            uri = URL(uri).navigate(resource_url).to_text()
        if token:
            headers['Authorization'] = 'Basic {}'.format(token)
        res = super().open(uri, status, query, accept, content_type, item, headers, **kw)
        # ereuse-utils checks for status code
        # here we check for specific type
        # (when response: {'type': 'foobar', 'code': 422})
        _status = getattr(status, 'code', status)
        if not isinstance(status, int) and res[1].status_code == _status:
            assert status.__name__ == res[0]['type'], \
                'Expected exception {0} but it was {1}'.format(status.__name__, res[0]['type'])
        return res

    def get(self,
            uri: str = '',
            res: str = None,
            query: Query = tuple(),
            status: Status = 200,
            item=None,
            accept: str = JSON,
            headers: dict = None,
            token: str = None,
            **kw) -> Res:
        """
        Performs GET.

        :param uri: The uri where to GET from. This is optional, as you
                    can build the URI too through ``res`` and ``item``.
        :param res: The resource where to GET from, if any.
                    If this is set, the client will try to get the
                    url from the resource definition.
        :param query: The query params in a dict. This method
                      automatically converts the dict to URL params,
                      and if the dict had nested dictionaries, those
                      are converted to JSON.
        :param status: A status code or exception to assert.
        :param item: The id of a resource to GET from, if any.
        :param accept: The accept headers. By default
                       ``application/json``.
        :param headers: A dictionary of header name - header value.
        :param token: A token to add to an ``Authentication`` header.
        :return: A tuple containing 1. a dict (if content-type is JSON)
                 or a str with the data, and 2. the ``Response`` object.
        """
        kw['res'] = res
        kw['token'] = token
        return super().get(uri, query, item, status, accept, headers, **kw)

    def post(self,
             data: str or dict,
             uri: str = '',
             res: str = None,
             query: Query = tuple(),
             status: Status = 201,
             content_type: str = JSON,
             accept: str = JSON,
             headers: dict = None,
             token: str = None,
             **kw) -> Res:
        kw['res'] = res
        kw['token'] = token
        return super().post(uri, data, query, status, content_type, accept, headers, **kw)

    def patch(self,
              data: str or dict,
              uri: str = '',
              res: str = None,
              query: Query = tuple(),
              item=None,
              status: Status = 200,
              content_type: str = JSON,
              accept: str = JSON,
              token: str = None,
              headers: dict = None,
              **kw) -> Res:
        kw['res'] = res
        kw['token'] = token
        return super().patch(uri, data, query, status, content_type, item, accept, headers, **kw)

    def put(self,
            data: str or dict,
            uri: str = '',
            res: str = None,
            query: Query = tuple(),
            item=None,
            status: Status = 201,
            content_type: str = JSON,
            accept: str = JSON,
            token: str = None,
            headers: dict = None,
            **kw) -> Res:
        kw['res'] = res
        kw['token'] = token
        return super().put(uri, data, query, status, content_type, item, accept, headers, **kw)

    def delete(self,
               uri: str = '',
               res: str = None,
               query: Query = tuple(),
               status: Status = 204,
               item=None,
               accept: str = JSON,
               headers: dict = None,
               token: str = None,
               **kw) -> Res:
        kw['res'] = res
        kw['token'] = token
        return super().delete(uri, query, item, status, accept, headers, **kw)

    def post_get(self,
                 res: str,
                 data: str or dict,
                 query: Query = tuple(),
                 status: Status = 200,
                 content_type: str = JSON,
                 accept: str = JSON,
                 headers: dict = None,
                 key='id',
                 token: str = None, **kw) -> Res:
        """Performs post and then gets the resource through its key."""
        r, _ = self.post('', data, res, query, status, content_type, accept, token, headers, **kw)
        return self.get(res=res, item=r[key])
