#
# coding: utf-8
# Copyright (c) 2018 DATADVANCE
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Utilities to test Websocket GraphQL backend."""

import textwrap
import uuid

import channels.testing


# Increase default `channels.testing.WebsocketCommunicator` timeout to
# avoid errors on slow machines.
TIMEOUT = 5


class GraphqlWsCommunicator(channels.testing.WebsocketCommunicator):
    """Auxiliary communicator with extra GraphQL-related methods."""

    def __init__(self, *args, **kwds):
        kwds.setdefault("subprotocols", ["graphql-ws"])
        kwds.setdefault("path", "graphql/")
        super().__init__(*args, **kwds)

    async def gql_connect_and_init(self, connect_only=False):
        """Establish and initialize WebSocket GraphQL connection.

        1. Establish WebSocket connection checking the subprotocol.
        2. Initialize GraphQL connection. Skipped if connect_only=True.
        """
        connected, subprotocol = await self.connect(timeout=TIMEOUT)
        assert connected, "Could not connect to the GraphQL subscriptions WebSocket!"
        assert subprotocol == "graphql-ws", "Wrong subprotocol received!"

        if not connect_only:
            await self.send_json_to({"type": "connection_init", "payload": ""})
            resp = await self.receive_json_from()
            assert resp["type"] == "connection_ack", f"Unexpected response `{resp}`!"

    AUTO = object()

    async def gql_send(self, *, id=AUTO, type=None, payload=None):
        """Send GraphQL message.

        If any argument is `None` it is excluded from the message.

        Args:
            id: The message identifier. Automatically generated by
                default.
            type: The message type.
            payload: The payload dict.
        Returns:
            The message identifier.
        """

        if id is self.AUTO:
            id = str(uuid.uuid4().hex)
        message = {}
        message.update({"id": id} if id is not None else {})
        message.update({"type": type} if type is not None else {})
        message.update({"payload": payload} if payload is not None else {})
        await self.send_json_to(message)
        return id

    async def gql_receive(
        self, *, assert_id=None, assert_type=None, assert_no_errors=True
    ):
        """Receive GraphQL message checking its content.

        Args:
            assert_id: Assert the response has a given message id.
            assert_type: Assert the response has a given message type.
            assert_no_errors: Assert the response has no
                `payload.errors` field.
        Returns:
            The `payload` field of the message received or `None`.
        """
        response = await self.receive_json_from()
        if assert_id is not None:
            assert response["id"] == assert_id, "Response id != expected id!"
        if assert_type is not None:
            assert response["type"] == assert_type, (
                f"Type `{assert_type}` expected, but `{response['type']}` received! "
                f"Response: {response}."
            )
        if assert_no_errors and "payload" in response:
            assert (
                "errors" not in response["payload"]
            ), f"Response contains errors! Response: {response}"

        return response.get("payload", None)

    async def gql_execute(self, query, variables=None):
        """Execute query or mutation request.

        Args:
            query: A GraphQL string query. We `dedent` it, so you do not
                have to.
            variables: Dict of variables (optional).
        Returns:
            Dictionary with the GraphQL response.
        """

        id = await self.gql_send(
            type="start",
            payload={
                "query": textwrap.dedent(query),
                "variables": {} if variables is None else variables,
            },
        )
        resp = await self.gql_receive(assert_id=id, assert_type="data")
        await self.gql_receive(assert_id=id, assert_type="complete")
        return resp

    async def gql_finalize(self):
        """Disconnect and wait the application to finish gracefully."""

        await self.disconnect(timeout=TIMEOUT)
        await self.wait(timeout=TIMEOUT)

    async def gql_assert_no_messages(self, message=None):
        """Assure no response received."""

        assert await self.receive_nothing(), (
            f"{message}"
            if message is not None
            else f"Message received when nothing expected!"
        )

    async def receive_json_from(self, timeout=TIMEOUT):
        """Overwrite to tune the `timeout` argument."""
        return await super().receive_json_from(timeout)
