import pytest

from bs4 import BeautifulSoup

from sanic import Sanic
from sanic.exceptions import (
    Forbidden,
    InvalidUsage,
    NotFound,
    ServerError,
    Unauthorized,
    abort,
)
from sanic.response import text


class SanicExceptionTestException(Exception):
    pass


@pytest.fixture(scope="module")
def exception_app():
    app = Sanic("test_exceptions")

    @app.route("/")
    def handler(request):
        return text("OK")

    @app.route("/error")
    def handler_error(request):
        raise ServerError("OK")

    @app.route("/404")
    def handler_404(request):
        raise NotFound("OK")

    @app.route("/403")
    def handler_403(request):
        raise Forbidden("Forbidden")

    @app.route("/401")
    def handler_401(request):
        raise Unauthorized("Unauthorized")

    @app.route("/401/basic")
    def handler_401_basic(request):
        raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")

    @app.route("/401/digest")
    def handler_401_digest(request):
        raise Unauthorized(
            "Unauthorized",
            scheme="Digest",
            realm="Sanic",
            qop="auth, auth-int",
            algorithm="MD5",
            nonce="abcdef",
            opaque="zyxwvu",
        )

    @app.route("/401/bearer")
    def handler_401_bearer(request):
        raise Unauthorized("Unauthorized", scheme="Bearer")

    @app.route("/invalid")
    def handler_invalid(request):
        raise InvalidUsage("OK")

    @app.route("/abort/401")
    def handler_401_error(request):
        abort(401)

    @app.route("/abort")
    def handler_500_error(request):
        abort(500)
        return text("OK")

    @app.route("/abort/message")
    def handler_abort_message(request):
        abort(500, message="Abort")

    @app.route("/divide_by_zero")
    def handle_unhandled_exception(request):
        _ = 1 / 0

    @app.route("/error_in_error_handler_handler")
    def custom_error_handler(request):
        raise SanicExceptionTestException("Dummy message!")

    @app.exception(SanicExceptionTestException)
    def error_in_error_handler_handler(request, exception):
        _ = 1 / 0

    return app


def test_catch_exception_list(app):
    @app.exception([SanicExceptionTestException, NotFound])
    def exception_list(request, exception):
        return text("ok")

    @app.route("/")
    def exception(request):
        raise SanicExceptionTestException("You won't see me")

    request, response = app.test_client.get("/random")
    assert response.text == "ok"

    request, response = app.test_client.get("/")
    assert response.text == "ok"


def test_no_exception(exception_app):
    """Test that a route works without an exception"""
    request, response = exception_app.test_client.get("/")
    assert response.status == 200
    assert response.text == "OK"


def test_server_error_exception(exception_app):
    """Test the built-in ServerError exception works"""
    request, response = exception_app.test_client.get("/error")
    assert response.status == 500


def test_invalid_usage_exception(exception_app):
    """Test the built-in InvalidUsage exception works"""
    request, response = exception_app.test_client.get("/invalid")
    assert response.status == 400


def test_not_found_exception(exception_app):
    """Test the built-in NotFound exception works"""
    request, response = exception_app.test_client.get("/404")
    assert response.status == 404


def test_forbidden_exception(exception_app):
    """Test the built-in Forbidden exception"""
    request, response = exception_app.test_client.get("/403")
    assert response.status == 403


def test_unauthorized_exception(exception_app):
    """Test the built-in Unauthorized exception"""
    request, response = exception_app.test_client.get("/401")
    assert response.status == 401

    request, response = exception_app.test_client.get("/401/basic")
    assert response.status == 401
    assert response.headers.get("WWW-Authenticate") is not None
    assert response.headers.get("WWW-Authenticate") == 'Basic realm="Sanic"'

    request, response = exception_app.test_client.get("/401/digest")
    assert response.status == 401

    auth_header = response.headers.get("WWW-Authenticate")
    assert auth_header is not None
    assert auth_header.startswith("Digest")
    assert 'qop="auth, auth-int"' in auth_header
    assert 'algorithm="MD5"' in auth_header
    assert 'nonce="abcdef"' in auth_header
    assert 'opaque="zyxwvu"' in auth_header

    request, response = exception_app.test_client.get("/401/bearer")
    assert response.status == 401
    assert response.headers.get("WWW-Authenticate") == "Bearer"


def test_handled_unhandled_exception(exception_app):
    """Test that an exception not built into sanic is handled"""
    request, response = exception_app.test_client.get("/divide_by_zero")
    assert response.status == 500
    soup = BeautifulSoup(response.body, "html.parser")
    assert soup.h1.text == "Internal Server Error"

    message = " ".join(soup.p.text.split())
    assert message == (
        "The server encountered an internal error and "
        "cannot complete your request."
    )


def test_exception_in_exception_handler(exception_app):
    """Test that an exception thrown in an error handler is handled"""
    request, response = exception_app.test_client.get(
        "/error_in_error_handler_handler"
    )
    assert response.status == 500
    assert response.body == b"An error occurred while handling an error"


def test_exception_in_exception_handler_debug_off(exception_app):
    """Test that an exception thrown in an error handler is handled"""
    request, response = exception_app.test_client.get(
        "/error_in_error_handler_handler", debug=False
    )
    assert response.status == 500
    assert response.body == b"An error occurred while handling an error"


def test_exception_in_exception_handler_debug_on(exception_app):
    """Test that an exception thrown in an error handler is handled"""
    request, response = exception_app.test_client.get(
        "/error_in_error_handler_handler", debug=True
    )
    assert response.status == 500
    assert response.body.startswith(b"Exception raised in exception ")


def test_abort(exception_app):
    """Test the abort function"""
    request, response = exception_app.test_client.get("/abort/401")
    assert response.status == 401

    request, response = exception_app.test_client.get("/abort")
    assert response.status == 500

    request, response = exception_app.test_client.get("/abort/message")
    assert response.status == 500
    assert response.text == "Error: Abort"