import pytest
from jinja2 import TemplateNotFound
from werkzeug.http import parse_cache_control_header

import flask


def test_blueprint_specific_error_handling(app, client):
    frontend = flask.Blueprint("frontend", __name__)
    backend = flask.Blueprint("backend", __name__)
    sideend = flask.Blueprint("sideend", __name__)

    @frontend.errorhandler(403)
    def frontend_forbidden(e):
        return "frontend says no", 403

    @frontend.route("/frontend-no")
    def frontend_no():
        flask.abort(403)

    @backend.errorhandler(403)
    def backend_forbidden(e):
        return "backend says no", 403

    @backend.route("/backend-no")
    def backend_no():
        flask.abort(403)

    @sideend.route("/what-is-a-sideend")
    def sideend_no():
        flask.abort(403)

    app.register_blueprint(frontend)
    app.register_blueprint(backend)
    app.register_blueprint(sideend)

    @app.errorhandler(403)
    def app_forbidden(e):
        return "application itself says no", 403

    assert client.get("/frontend-no").data == b"frontend says no"
    assert client.get("/backend-no").data == b"backend says no"
    assert client.get("/what-is-a-sideend").data == b"application itself says no"


def test_blueprint_specific_user_error_handling(app, client):
    class MyDecoratorException(Exception):
        pass

    class MyFunctionException(Exception):
        pass

    blue = flask.Blueprint("blue", __name__)

    @blue.errorhandler(MyDecoratorException)
    def my_decorator_exception_handler(e):
        assert isinstance(e, MyDecoratorException)
        return "boom"

    def my_function_exception_handler(e):
        assert isinstance(e, MyFunctionException)
        return "bam"

    blue.register_error_handler(MyFunctionException, my_function_exception_handler)

    @blue.route("/decorator")
    def blue_deco_test():
        raise MyDecoratorException()

    @blue.route("/function")
    def blue_func_test():
        raise MyFunctionException()

    app.register_blueprint(blue)

    assert client.get("/decorator").data == b"boom"
    assert client.get("/function").data == b"bam"


def test_blueprint_app_error_handling(app, client):
    errors = flask.Blueprint("errors", __name__)

    @errors.app_errorhandler(403)
    def forbidden_handler(e):
        return "you shall not pass", 403

    @app.route("/forbidden")
    def app_forbidden():
        flask.abort(403)

    forbidden_bp = flask.Blueprint("forbidden_bp", __name__)

    @forbidden_bp.route("/nope")
    def bp_forbidden():
        flask.abort(403)

    app.register_blueprint(errors)
    app.register_blueprint(forbidden_bp)

    assert client.get("/forbidden").data == b"you shall not pass"
    assert client.get("/nope").data == b"you shall not pass"


@pytest.mark.parametrize(
    ("prefix", "rule", "url"),
    (
        ("", "/", "/"),
        ("/", "", "/"),
        ("/", "/", "/"),
        ("/foo", "", "/foo"),
        ("/foo/", "", "/foo/"),
        ("", "/bar", "/bar"),
        ("/foo/", "/bar", "/foo/bar"),
        ("/foo/", "bar", "/foo/bar"),
        ("/foo", "/bar", "/foo/bar"),
        ("/foo/", "//bar", "/foo/bar"),
        ("/foo//", "/bar", "/foo/bar"),
    ),
)
def test_blueprint_prefix_slash(app, client, prefix, rule, url):
    bp = flask.Blueprint("test", __name__, url_prefix=prefix)

    @bp.route(rule)
    def index():
        return "", 204

    app.register_blueprint(bp)
    assert client.get(url).status_code == 204


def test_blueprint_url_defaults(app, client):
    bp = flask.Blueprint("test", __name__)

    @bp.route("/foo", defaults={"baz": 42})
    def foo(bar, baz):
        return f"{bar}/{baz:d}"

    @bp.route("/bar")
    def bar(bar):
        return str(bar)

    app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23})
    app.register_blueprint(bp, name="test2", url_prefix="/2", url_defaults={"bar": 19})

    assert client.get("/1/foo").data == b"23/42"
    assert client.get("/2/foo").data == b"19/42"
    assert client.get("/1/bar").data == b"23"
    assert client.get("/2/bar").data == b"19"


def test_blueprint_url_processors(app, client):
    bp = flask.Blueprint("frontend", __name__, url_prefix="/<lang_code>")

    @bp.url_defaults
    def add_language_code(endpoint, values):
        values.setdefault("lang_code", flask.g.lang_code)

    @bp.url_value_preprocessor
    def pull_lang_code(endpoint, values):
        flask.g.lang_code = values.pop("lang_code")

    @bp.route("/")
    def index():
        return flask.url_for(".about")

    @bp.route("/about")
    def about():
        return flask.url_for(".index")

    app.register_blueprint(bp)

    assert client.get("/de/").data == b"/de/about"
    assert client.get("/de/about").data == b"/de/"


def test_templates_and_static(test_apps):
    from blueprintapp import app

    client = app.test_client()

    rv = client.get("/")
    assert rv.data == b"Hello from the Frontend"
    rv = client.get("/admin/")
    assert rv.data == b"Hello from the Admin"
    rv = client.get("/admin/index2")
    assert rv.data == b"Hello from the Admin"
    rv = client.get("/admin/static/test.txt")
    assert rv.data.strip() == b"Admin File"
    rv.close()
    rv = client.get("/admin/static/css/test.css")
    assert rv.data.strip() == b"/* nested file */"
    rv.close()

    # try/finally, in case other tests use this app for Blueprint tests.
    max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
    try:
        expected_max_age = 3600
        if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age:
            expected_max_age = 7200
        app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age
        rv = client.get("/admin/static/css/test.css")
        cc = parse_cache_control_header(rv.headers["Cache-Control"])
        assert cc.max_age == expected_max_age
        rv.close()
    finally:
        app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default

    with app.test_request_context():
        assert (
            flask.url_for("admin.static", filename="test.txt")
            == "/admin/static/test.txt"
        )

    with app.test_request_context():
        with pytest.raises(TemplateNotFound) as e:
            flask.render_template("missing.html")
        assert e.value.name == "missing.html"

    with flask.Flask(__name__).test_request_context():
        assert flask.render_template("nested/nested.txt") == "I'm nested"


def test_default_static_max_age(app):
    class MyBlueprint(flask.Blueprint):
        def get_send_file_max_age(self, filename):
            return 100

    blueprint = MyBlueprint("blueprint", __name__, static_folder="static")
    app.register_blueprint(blueprint)

    # try/finally, in case other tests use this app for Blueprint tests.
    max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
    try:
        with app.test_request_context():
            unexpected_max_age = 3600
            if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age:
                unexpected_max_age = 7200
            app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age
            rv = blueprint.send_static_file("index.html")
            cc = parse_cache_control_header(rv.headers["Cache-Control"])
            assert cc.max_age == 100
            rv.close()
    finally:
        app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default


def test_templates_list(test_apps):
    from blueprintapp import app

    templates = sorted(app.jinja_env.list_templates())
    assert templates == ["admin/index.html", "frontend/index.html"]


def test_dotted_name_not_allowed(app, client):
    with pytest.raises(ValueError):
        flask.Blueprint("app.ui", __name__)


def test_empty_name_not_allowed(app, client):
    with pytest.raises(ValueError):
        flask.Blueprint("", __name__)


def test_dotted_names_from_app(app, client):
    test = flask.Blueprint("test", __name__)

    @app.route("/")
    def app_index():
        return flask.url_for("test.index")

    @test.route("/test/")
    def index():
        return flask.url_for("app_index")

    app.register_blueprint(test)

    rv = client.get("/")
    assert rv.data == b"/test/"


def test_empty_url_defaults(app, client):
    bp = flask.Blueprint("bp", __name__)

    @bp.route("/", defaults={"page": 1})
    @bp.route("/page/<int:page>")
    def something(page):
        return str(page)

    app.register_blueprint(bp)

    assert client.get("/").data == b"1"
    assert client.get("/page/2").data == b"2"


def test_route_decorator_custom_endpoint(app, client):
    bp = flask.Blueprint("bp", __name__)

    @bp.route("/foo")
    def foo():
        return flask.request.endpoint

    @bp.route("/bar", endpoint="bar")
    def foo_bar():
        return flask.request.endpoint

    @bp.route("/bar/123", endpoint="123")
    def foo_bar_foo():
        return flask.request.endpoint

    @bp.route("/bar/foo")
    def bar_foo():
        return flask.request.endpoint

    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.request.endpoint

    assert client.get("/").data == b"index"
    assert client.get("/py/foo").data == b"bp.foo"
    assert client.get("/py/bar").data == b"bp.bar"
    assert client.get("/py/bar/123").data == b"bp.123"
    assert client.get("/py/bar/foo").data == b"bp.bar_foo"


def test_route_decorator_custom_endpoint_with_dots(app, client):
    bp = flask.Blueprint("bp", __name__)

    with pytest.raises(ValueError):
        bp.route("/", endpoint="a.b")(lambda: "")

    with pytest.raises(ValueError):
        bp.add_url_rule("/", endpoint="a.b")

    def view():
        return ""

    view.__name__ = "a.b"

    with pytest.raises(ValueError):
        bp.add_url_rule("/", view_func=view)


def test_endpoint_decorator(app, client):
    from werkzeug.routing import Rule

    app.url_map.add(Rule("/foo", endpoint="bar"))

    bp = flask.Blueprint("bp", __name__)

    @bp.endpoint("bar")
    def foobar():
        return flask.request.endpoint

    app.register_blueprint(bp, url_prefix="/bp_prefix")

    assert client.get("/foo").data == b"bar"
    assert client.get("/bp_prefix/bar").status_code == 404


def test_template_filter(app):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_filter()
    def my_reverse(s):
        return s[::-1]

    app.register_blueprint(bp, url_prefix="/py")
    assert "my_reverse" in app.jinja_env.filters.keys()
    assert app.jinja_env.filters["my_reverse"] == my_reverse
    assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"


def test_add_template_filter(app):
    bp = flask.Blueprint("bp", __name__)

    def my_reverse(s):
        return s[::-1]

    bp.add_app_template_filter(my_reverse)
    app.register_blueprint(bp, url_prefix="/py")
    assert "my_reverse" in app.jinja_env.filters.keys()
    assert app.jinja_env.filters["my_reverse"] == my_reverse
    assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"


def test_template_filter_with_name(app):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_filter("strrev")
    def my_reverse(s):
        return s[::-1]

    app.register_blueprint(bp, url_prefix="/py")
    assert "strrev" in app.jinja_env.filters.keys()
    assert app.jinja_env.filters["strrev"] == my_reverse
    assert app.jinja_env.filters["strrev"]("abcd") == "dcba"


def test_add_template_filter_with_name(app):
    bp = flask.Blueprint("bp", __name__)

    def my_reverse(s):
        return s[::-1]

    bp.add_app_template_filter(my_reverse, "strrev")
    app.register_blueprint(bp, url_prefix="/py")
    assert "strrev" in app.jinja_env.filters.keys()
    assert app.jinja_env.filters["strrev"] == my_reverse
    assert app.jinja_env.filters["strrev"]("abcd") == "dcba"


def test_template_filter_with_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_filter()
    def super_reverse(s):
        return s[::-1]

    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_filter.html", value="abcd")

    rv = client.get("/")
    assert rv.data == b"dcba"


def test_template_filter_after_route_with_template(app, client):
    @app.route("/")
    def index():
        return flask.render_template("template_filter.html", value="abcd")

    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_filter()
    def super_reverse(s):
        return s[::-1]

    app.register_blueprint(bp, url_prefix="/py")
    rv = client.get("/")
    assert rv.data == b"dcba"


def test_add_template_filter_with_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    def super_reverse(s):
        return s[::-1]

    bp.add_app_template_filter(super_reverse)
    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_filter.html", value="abcd")

    rv = client.get("/")
    assert rv.data == b"dcba"


def test_template_filter_with_name_and_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_filter("super_reverse")
    def my_reverse(s):
        return s[::-1]

    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_filter.html", value="abcd")

    rv = client.get("/")
    assert rv.data == b"dcba"


def test_add_template_filter_with_name_and_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    def my_reverse(s):
        return s[::-1]

    bp.add_app_template_filter(my_reverse, "super_reverse")
    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_filter.html", value="abcd")

    rv = client.get("/")
    assert rv.data == b"dcba"


def test_template_test(app):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_test()
    def is_boolean(value):
        return isinstance(value, bool)

    app.register_blueprint(bp, url_prefix="/py")
    assert "is_boolean" in app.jinja_env.tests.keys()
    assert app.jinja_env.tests["is_boolean"] == is_boolean
    assert app.jinja_env.tests["is_boolean"](False)


def test_add_template_test(app):
    bp = flask.Blueprint("bp", __name__)

    def is_boolean(value):
        return isinstance(value, bool)

    bp.add_app_template_test(is_boolean)
    app.register_blueprint(bp, url_prefix="/py")
    assert "is_boolean" in app.jinja_env.tests.keys()
    assert app.jinja_env.tests["is_boolean"] == is_boolean
    assert app.jinja_env.tests["is_boolean"](False)


def test_template_test_with_name(app):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_test("boolean")
    def is_boolean(value):
        return isinstance(value, bool)

    app.register_blueprint(bp, url_prefix="/py")
    assert "boolean" in app.jinja_env.tests.keys()
    assert app.jinja_env.tests["boolean"] == is_boolean
    assert app.jinja_env.tests["boolean"](False)


def test_add_template_test_with_name(app):
    bp = flask.Blueprint("bp", __name__)

    def is_boolean(value):
        return isinstance(value, bool)

    bp.add_app_template_test(is_boolean, "boolean")
    app.register_blueprint(bp, url_prefix="/py")
    assert "boolean" in app.jinja_env.tests.keys()
    assert app.jinja_env.tests["boolean"] == is_boolean
    assert app.jinja_env.tests["boolean"](False)


def test_template_test_with_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_test()
    def boolean(value):
        return isinstance(value, bool)

    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_test.html", value=False)

    rv = client.get("/")
    assert b"Success!" in rv.data


def test_template_test_after_route_with_template(app, client):
    @app.route("/")
    def index():
        return flask.render_template("template_test.html", value=False)

    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_test()
    def boolean(value):
        return isinstance(value, bool)

    app.register_blueprint(bp, url_prefix="/py")
    rv = client.get("/")
    assert b"Success!" in rv.data


def test_add_template_test_with_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    def boolean(value):
        return isinstance(value, bool)

    bp.add_app_template_test(boolean)
    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_test.html", value=False)

    rv = client.get("/")
    assert b"Success!" in rv.data


def test_template_test_with_name_and_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_test("boolean")
    def is_boolean(value):
        return isinstance(value, bool)

    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_test.html", value=False)

    rv = client.get("/")
    assert b"Success!" in rv.data


def test_add_template_test_with_name_and_template(app, client):
    bp = flask.Blueprint("bp", __name__)

    def is_boolean(value):
        return isinstance(value, bool)

    bp.add_app_template_test(is_boolean, "boolean")
    app.register_blueprint(bp, url_prefix="/py")

    @app.route("/")
    def index():
        return flask.render_template("template_test.html", value=False)

    rv = client.get("/")
    assert b"Success!" in rv.data


def test_context_processing(app, client):
    answer_bp = flask.Blueprint("answer_bp", __name__)

    def template_string():
        return flask.render_template_string(
            "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}"
            "{% if answer %}{{ answer }} is the answer.{% endif %}"
        )

    # App global context processor
    @answer_bp.app_context_processor
    def not_answer_context_processor():
        return {"notanswer": 43}

    # Blueprint local context processor
    @answer_bp.context_processor
    def answer_context_processor():
        return {"answer": 42}

    # Setup endpoints for testing
    @answer_bp.route("/bp")
    def bp_page():
        return template_string()

    @app.route("/")
    def app_page():
        return template_string()

    # Register the blueprint
    app.register_blueprint(answer_bp)

    app_page_bytes = client.get("/").data
    answer_page_bytes = client.get("/bp").data

    assert b"43" in app_page_bytes
    assert b"42" not in app_page_bytes

    assert b"42" in answer_page_bytes
    assert b"43" in answer_page_bytes


def test_template_global(app):
    bp = flask.Blueprint("bp", __name__)

    @bp.app_template_global()
    def get_answer():
        return 42

    # Make sure the function is not in the jinja_env already
    assert "get_answer" not in app.jinja_env.globals.keys()
    app.register_blueprint(bp)

    # Tests
    assert "get_answer" in app.jinja_env.globals.keys()
    assert app.jinja_env.globals["get_answer"] is get_answer
    assert app.jinja_env.globals["get_answer"]() == 42

    with app.app_context():
        rv = flask.render_template_string("{{ get_answer() }}")
        assert rv == "42"


def test_request_processing(app, client):
    bp = flask.Blueprint("bp", __name__)
    evts = []

    @bp.before_request
    def before_bp():
        evts.append("before")

    @bp.after_request
    def after_bp(response):
        response.data += b"|after"
        evts.append("after")
        return response

    @bp.teardown_request
    def teardown_bp(exc):
        evts.append("teardown")

    # Setup routes for testing
    @bp.route("/bp")
    def bp_endpoint():
        return "request"

    app.register_blueprint(bp)

    assert evts == []
    rv = client.get("/bp")
    assert rv.data == b"request|after"
    assert evts == ["before", "after", "teardown"]


def test_app_request_processing(app, client):
    bp = flask.Blueprint("bp", __name__)
    evts = []

    @bp.before_app_request
    def before_app():
        evts.append("before")

    @bp.after_app_request
    def after_app(response):
        response.data += b"|after"
        evts.append("after")
        return response

    @bp.teardown_app_request
    def teardown_app(exc):
        evts.append("teardown")

    app.register_blueprint(bp)

    # Setup routes for testing
    @app.route("/")
    def bp_endpoint():
        return "request"

    # before first request
    assert evts == []

    # first request
    resp = client.get("/").data
    assert resp == b"request|after"
    assert evts == ["before", "after", "teardown"]

    # second request
    resp = client.get("/").data
    assert resp == b"request|after"
    assert evts == ["before", "after", "teardown"] * 2


def test_app_url_processors(app, client):
    bp = flask.Blueprint("bp", __name__)

    # Register app-wide url defaults and preprocessor on blueprint
    @bp.app_url_defaults
    def add_language_code(endpoint, values):
        values.setdefault("lang_code", flask.g.lang_code)

    @bp.app_url_value_preprocessor
    def pull_lang_code(endpoint, values):
        flask.g.lang_code = values.pop("lang_code")

    # Register route rules at the app level
    @app.route("/<lang_code>/")
    def index():
        return flask.url_for("about")

    @app.route("/<lang_code>/about")
    def about():
        return flask.url_for("index")

    app.register_blueprint(bp)

    assert client.get("/de/").data == b"/de/about"
    assert client.get("/de/about").data == b"/de/"


def test_nested_blueprint(app, client):
    parent = flask.Blueprint("parent", __name__)
    child = flask.Blueprint("child", __name__)
    grandchild = flask.Blueprint("grandchild", __name__)

    @parent.errorhandler(403)
    def forbidden(e):
        return "Parent no", 403

    @parent.route("/")
    def parent_index():
        return "Parent yes"

    @parent.route("/no")
    def parent_no():
        flask.abort(403)

    @child.route("/")
    def child_index():
        return "Child yes"

    @child.route("/no")
    def child_no():
        flask.abort(403)

    @grandchild.errorhandler(403)
    def grandchild_forbidden(e):
        return "Grandchild no", 403

    @grandchild.route("/")
    def grandchild_index():
        return "Grandchild yes"

    @grandchild.route("/no")
    def grandchild_no():
        flask.abort(403)

    child.register_blueprint(grandchild, url_prefix="/grandchild")
    parent.register_blueprint(child, url_prefix="/child")
    app.register_blueprint(parent, url_prefix="/parent")

    assert client.get("/parent/").data == b"Parent yes"
    assert client.get("/parent/child/").data == b"Child yes"
    assert client.get("/parent/child/grandchild/").data == b"Grandchild yes"
    assert client.get("/parent/no").data == b"Parent no"
    assert client.get("/parent/child/no").data == b"Parent no"
    assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"


def test_nested_callback_order(app, client):
    parent = flask.Blueprint("parent", __name__)
    child = flask.Blueprint("child", __name__)

    @app.before_request
    def app_before1():
        flask.g.setdefault("seen", []).append("app_1")

    @app.teardown_request
    def app_teardown1(e=None):
        assert flask.g.seen.pop() == "app_1"

    @app.before_request
    def app_before2():
        flask.g.setdefault("seen", []).append("app_2")

    @app.teardown_request
    def app_teardown2(e=None):
        assert flask.g.seen.pop() == "app_2"

    @app.context_processor
    def app_ctx():
        return dict(key="app")

    @parent.before_request
    def parent_before1():
        flask.g.setdefault("seen", []).append("parent_1")

    @parent.teardown_request
    def parent_teardown1(e=None):
        assert flask.g.seen.pop() == "parent_1"

    @parent.before_request
    def parent_before2():
        flask.g.setdefault("seen", []).append("parent_2")

    @parent.teardown_request
    def parent_teardown2(e=None):
        assert flask.g.seen.pop() == "parent_2"

    @parent.context_processor
    def parent_ctx():
        return dict(key="parent")

    @child.before_request
    def child_before1():
        flask.g.setdefault("seen", []).append("child_1")

    @child.teardown_request
    def child_teardown1(e=None):
        assert flask.g.seen.pop() == "child_1"

    @child.before_request
    def child_before2():
        flask.g.setdefault("seen", []).append("child_2")

    @child.teardown_request
    def child_teardown2(e=None):
        assert flask.g.seen.pop() == "child_2"

    @child.context_processor
    def child_ctx():
        return dict(key="child")

    @child.route("/a")
    def a():
        return ", ".join(flask.g.seen)

    @child.route("/b")
    def b():
        return flask.render_template_string("{{ key }}")

    parent.register_blueprint(child)
    app.register_blueprint(parent)
    assert (
        client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2"
    )
    assert client.get("/b").data == b"child"


@pytest.mark.parametrize(
    "parent_init, child_init, parent_registration, child_registration",
    [
        ("/parent", "/child", None, None),
        ("/parent", None, None, "/child"),
        (None, None, "/parent", "/child"),
        ("/other", "/something", "/parent", "/child"),
    ],
)
def test_nesting_url_prefixes(
    parent_init,
    child_init,
    parent_registration,
    child_registration,
    app,
    client,
) -> None:
    parent = flask.Blueprint("parent", __name__, url_prefix=parent_init)
    child = flask.Blueprint("child", __name__, url_prefix=child_init)

    @child.route("/")
    def index():
        return "index"

    parent.register_blueprint(child, url_prefix=child_registration)
    app.register_blueprint(parent, url_prefix=parent_registration)

    response = client.get("/parent/child/")
    assert response.status_code == 200


def test_nesting_subdomains(app, client) -> None:
    app.subdomain_matching = True
    app.config["SERVER_NAME"] = "example.test"
    client.allow_subdomain_redirects = True

    parent = flask.Blueprint("parent", __name__)
    child = flask.Blueprint("child", __name__)

    @child.route("/child/")
    def index():
        return "child"

    parent.register_blueprint(child)
    app.register_blueprint(parent, subdomain="api")

    response = client.get("/child/", base_url="http://api.example.test")
    assert response.status_code == 200


def test_child_and_parent_subdomain(app, client) -> None:
    app.subdomain_matching = True
    app.config["SERVER_NAME"] = "example.test"
    client.allow_subdomain_redirects = True

    parent = flask.Blueprint("parent", __name__)
    child = flask.Blueprint("child", __name__, subdomain="api")

    @child.route("/")
    def index():
        return "child"

    parent.register_blueprint(child)
    app.register_blueprint(parent, subdomain="parent")

    response = client.get("/", base_url="http://api.parent.example.test")
    assert response.status_code == 200

    response = client.get("/", base_url="http://parent.example.test")
    assert response.status_code == 404


def test_unique_blueprint_names(app, client) -> None:
    bp = flask.Blueprint("bp", __name__)
    bp2 = flask.Blueprint("bp", __name__)

    app.register_blueprint(bp)

    with pytest.raises(ValueError):
        app.register_blueprint(bp)  # same bp, same name, error

    app.register_blueprint(bp, name="again")  # same bp, different name, ok

    with pytest.raises(ValueError):
        app.register_blueprint(bp2)  # different bp, same name, error

    app.register_blueprint(bp2, name="alt")  # different bp, different name, ok


def test_self_registration(app, client) -> None:
    bp = flask.Blueprint("bp", __name__)
    with pytest.raises(ValueError):
        bp.register_blueprint(bp)


def test_blueprint_renaming(app, client) -> None:
    bp = flask.Blueprint("bp", __name__)
    bp2 = flask.Blueprint("bp2", __name__)

    @bp.get("/")
    def index():
        return flask.request.endpoint

    @bp.get("/error")
    def error():
        flask.abort(403)

    @bp.errorhandler(403)
    def forbidden(_: Exception):
        return "Error", 403

    @bp2.get("/")
    def index2():
        return flask.request.endpoint

    bp.register_blueprint(bp2, url_prefix="/a", name="sub")
    app.register_blueprint(bp, url_prefix="/a")
    app.register_blueprint(bp, url_prefix="/b", name="alt")

    assert client.get("/a/").data == b"bp.index"
    assert client.get("/b/").data == b"alt.index"
    assert client.get("/a/a/").data == b"bp.sub.index2"
    assert client.get("/b/a/").data == b"alt.sub.index2"
    assert client.get("/a/error").data == b"Error"
    assert client.get("/b/error").data == b"Error"
