import asyncio
import typing
from functools import partial
from random import randint
from typing import Any, Dict, List, Optional, Tuple

import httpx
from sanic import Sanic
from sanic.application.state import ApplicationServerInfo
from sanic.log import logger
from sanic.request import Request

from sanic_testing.websocket import websocket_proxy

from .testing import HOST, PORT, TestingResponse


class ReusableClient:
    def __init__(
        self,
        app: Sanic,
        host=HOST,
        port=PORT,
        loop=None,
        server_kwargs=None,
        client_kwargs=None,
    ):
        if not loop:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
        server_kwargs = server_kwargs or {}
        client_kwargs = client_kwargs or {}

        Sanic.test_mode = True
        self.app = app
        self.host = host
        self.port = port or randint(5000, 65000)
        self._loop = loop
        self.debug = False
        self._server = None
        self.app.state.server_info.append(
            ApplicationServerInfo(
                settings={
                    "version": "1.1",
                    "ssl": None,
                    "unix": None,
                    "sock": None,
                    "loop": None,
                    "host": self.host,
                    "port": self.port,
                }
            )
        )

        self._session = httpx.AsyncClient(verify=False, **client_kwargs)
        self._server_co = self.app.create_server(
            host=self.host,
            debug=self.debug,
            port=self.port,
            return_asyncio_server=True,
            **server_kwargs,
        )

    def __enter__(self):
        self.run()
        return self

    def __exit__(self, *_):
        self.stop()

    def run(self):
        self._loop._stopping = False
        self.app.router.reset()
        self.app.signal_router.reset()
        self._run(self.app._startup())
        self._run(self.app._server_event("init", "before", loop=self._loop))
        self._server = self._run(self._server_co)
        self._run(self.app._server_event("init", "after", loop=self._loop))

    def stop(self):
        self._run(
            self.app._server_event("shutdown", "before", loop=self._loop)
        )
        if self._server:
            self._server.close()
            self._run(self._server.wait_closed())
            self._server = None

        if self._session:
            self._run(self._session.aclose())
            self._session = None
        self._run(self.app._server_event("shutdown", "after", loop=self._loop))

    def _sanic_endpoint_test(
        self,
        method: str = "get",
        uri: str = "/",
        gather_request: bool = True,
        debug: bool = False,
        server_kwargs: Optional[Dict[str, Any]] = None,
        host: Optional[str] = None,
        port: Optional[int] = None,
        allow_none: bool = False,
        *request_args,
        **request_kwargs,
    ) -> Tuple[Optional[Request], Optional[TestingResponse]]:
        request_data: Dict[str, Request] = {}
        exceptions: List[Exception] = []

        host = host or self.host
        port = port or self.port

        if gather_request:
            _collect_request = partial(self._collect_request, request_data)
            self.app.request_middleware.appendleft(_collect_request)  # type: ignore  # noqa

            for route in self.app.router.routes:
                if _collect_request not in route.extra.request_middleware:
                    route.extra.request_middleware.appendleft(_collect_request)

        if uri.startswith(
            ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
        ):
            url = uri
        else:
            uri = uri if uri.startswith("/") else f"/{uri}"
            scheme = "ws" if method == "websocket" else "http"
            url = f"{scheme}://{host}:{port}{uri}"

        if exceptions:
            raise ValueError(f"Exception during request: {exceptions}")

        response = self._run(
            self._local_request(method, url, *request_args, **request_kwargs)
        )

        try:
            self.app.request_middleware.remove(_collect_request)  # type: ignore  # noqa
        except BaseException:  # noqa
            pass

        try:
            request = request_data.get("request") if gather_request else None
            if response is None:
                if not allow_none:
                    raise ValueError(
                        "No response returned to Sanic Test Client."
                    )
            return request, response
        except BaseException:  # noqa
            if not allow_none:
                raise ValueError(
                    "Request and response object expected, "
                    f"got ({request}, {response})"
                )

        return None, None

    async def _local_request(self, method, url, *args, **kwargs):
        raw_cookies = kwargs.pop("raw_cookies", None)

        if method == "websocket":
            return await websocket_proxy(url, *args, **kwargs)
        else:
            session = self._session

            try:
                if method == "request":
                    args = tuple([url] + list(args))
                    url = kwargs.pop("http_method", "GET").upper()
                response = await getattr(session, method.lower())(
                    url, *args, **kwargs
                )
            except httpx.HTTPError as e:
                if hasattr(e, "response"):
                    response = getattr(e, "response")
                else:
                    logger.error(
                        f"{method.upper()} {url} received no response!",
                        exc_info=True,
                    )
                    return None

            response.__class__ = TestingResponse

            if raw_cookies:
                response.raw_cookies = {}

                for cookie in response.cookies.jar:
                    response.raw_cookies[cookie.name] = cookie

            return response

    def _run(self, coro):
        if not self._loop:
            raise RuntimeError("Test client has no loop")
        return self._loop.run_until_complete(coro)

    @staticmethod
    def _collect_request(data, request):
        data["request"] = request

    def request(self, *args, **kwargs):
        return self._sanic_endpoint_test("request", *args, **kwargs)

    def get(self, *args, **kwargs):
        return self._sanic_endpoint_test("get", *args, **kwargs)

    def post(self, *args, **kwargs):
        return self._sanic_endpoint_test("post", *args, **kwargs)

    def put(self, *args, **kwargs):
        return self._sanic_endpoint_test("put", *args, **kwargs)

    def delete(self, *args, **kwargs):
        return self._sanic_endpoint_test("delete", *args, **kwargs)

    def patch(self, *args, **kwargs):
        return self._sanic_endpoint_test("patch", *args, **kwargs)

    def options(self, *args, **kwargs):
        return self._sanic_endpoint_test("options", *args, **kwargs)

    def head(self, *args, **kwargs):
        return self._sanic_endpoint_test("head", *args, **kwargs)

    def websocket(
        self,
        *args,
        mimic: typing.Optional[
            typing.Callable[..., typing.Coroutine[None, None, typing.Any]]
        ] = None,
        **kwargs,
    ):
        kwargs["mimic"] = mimic
        return self._sanic_endpoint_test("websocket", *args, **kwargs)
