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

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

from ereuse_devicehub.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])