# mypy: allow-untyped-defs

import collections
import json
import string

from typing import ClassVar, DefaultDict, Type


class WebDriverException(Exception):
    # The status_code class variable is used to map the JSON Error Code (see
    # https://w3c.github.io/webdriver/#errors) to a WebDriverException subclass.
    # However, http_status need not match, and both are set as instance
    # variables, shadowing the class variables. TODO: Match on both http_status
    # and status_code and let these be class variables only.
    http_status: ClassVar[int]
    status_code: ClassVar[str]

    def __init__(
        self,
        http_status=None,
        status_code=None,
        message=None,
        stacktrace=None,
        data=None,
    ):
        super().__init__()

        if http_status is not None:
            self.http_status = http_status
        if status_code is not None:
            self.status_code = status_code
        self.message = message
        self.stacktrace = stacktrace
        self.data = data

    def __repr__(self):
        return f"<{self.__class__.__name__} http_status={self.http_status}>"

    def __str__(self):
        message = f"{self.status_code} ({self.http_status})"

        if self.message is not None:
            message += ": %s" % self.message.strip(string.whitespace)

        if self.stacktrace is not None:
            message += ("\n\nRemote-end stacktrace:\n\n%s" % self.stacktrace.strip("\n"))

        return message


class DetachedShadowRootException(WebDriverException):
    http_status = 404
    status_code = "detached shadow root"


class ElementClickInterceptedException(WebDriverException):
    http_status = 400
    status_code = "element click intercepted"


class ElementNotSelectableException(WebDriverException):
    http_status = 400
    status_code = "element not selectable"


class ElementNotVisibleException(WebDriverException):
    http_status = 400
    status_code = "element not visible"


class InsecureCertificateException(WebDriverException):
    http_status = 400
    status_code = "insecure certificate"


class InvalidArgumentException(WebDriverException):
    http_status = 400
    status_code = "invalid argument"


class InvalidCookieDomainException(WebDriverException):
    http_status = 400
    status_code = "invalid cookie domain"


class InvalidElementCoordinatesException(WebDriverException):
    http_status = 400
    status_code = "invalid element coordinates"


class InvalidElementStateException(WebDriverException):
    http_status = 400
    status_code = "invalid element state"


class InvalidSelectorException(WebDriverException):
    http_status = 400
    status_code = "invalid selector"


class InvalidSessionIdException(WebDriverException):
    http_status = 404
    status_code = "invalid session id"


class JavascriptErrorException(WebDriverException):
    http_status = 500
    status_code = "javascript error"


class MoveTargetOutOfBoundsException(WebDriverException):
    http_status = 500
    status_code = "move target out of bounds"


class NoSuchAlertException(WebDriverException):
    http_status = 404
    status_code = "no such alert"


class NoSuchCookieException(WebDriverException):
    http_status = 404
    status_code = "no such cookie"


class NoSuchElementException(WebDriverException):
    http_status = 404
    status_code = "no such element"


class NoSuchFrameException(WebDriverException):
    http_status = 404
    status_code = "no such frame"


class NoSuchShadowRootException(WebDriverException):
    http_status = 404
    status_code = "no such shadow root"


class NoSuchWindowException(WebDriverException):
    http_status = 404
    status_code = "no such window"


class ScriptTimeoutException(WebDriverException):
    http_status = 500
    status_code = "script timeout"


class SessionNotCreatedException(WebDriverException):
    http_status = 500
    status_code = "session not created"


class StaleElementReferenceException(WebDriverException):
    http_status = 404
    status_code = "stale element reference"


class TimeoutException(WebDriverException):
    http_status = 500
    status_code = "timeout"


class UnableToSetCookieException(WebDriverException):
    http_status = 500
    status_code = "unable to set cookie"


class UnexpectedAlertOpenException(WebDriverException):
    http_status = 500
    status_code = "unexpected alert open"


class UnknownErrorException(WebDriverException):
    http_status = 500
    status_code = "unknown error"


class UnknownCommandException(WebDriverException):
    http_status = 404
    status_code = "unknown command"


class UnknownMethodException(WebDriverException):
    http_status = 405
    status_code = "unknown method"


class UnsupportedOperationException(WebDriverException):
    http_status = 500
    status_code = "unsupported operation"


def from_response(response):
    """
    Unmarshals an error from a ``Response``'s `body`, failing
    if not all three required `error`, `message`, and `stacktrace`
    fields are given.  Defaults to ``WebDriverException`` if `error`
    is unknown.
    """
    if response.status == 200:
        raise UnknownErrorException(
            response.status,
            None,
            "Response is not an error:\n"
            "%s" % json.dumps(response.body))

    if "value" in response.body:
        value = response.body["value"]
    else:
        raise UnknownErrorException(
            response.status,
            None,
            "Expected 'value' key in response body:\n"
            "%s" % json.dumps(response.body))

    # all fields must exist, but both message and stacktrace are
    # implementation-defined and could be empty
    code = value["error"]
    message = value["message"] or None
    stack = value["stacktrace"] or None
    # data is optional, and could even be an empty dict
    data = value.get("data") or None

    cls = get(code)
    return cls(response.status, code, message, stacktrace=stack, data=data)


def get(error_code):
    """
    Gets exception from `error_code`, falling back to
    ``WebDriverException`` if it is not found.
    """
    return _errors.get(error_code, WebDriverException)


_errors: DefaultDict[str, Type[WebDriverException]] = collections.defaultdict()
for item in list(locals().values()):
    if isinstance(item, type) and item != WebDriverException and issubclass(item, WebDriverException):
        _errors[item.status_code] = item
