from flask import jsonify
import requests
import pytest

from dash import Dash, Input, Output, html, hooks, set_props, ctx, get_app


@pytest.fixture
def hook_cleanup():
    yield
    hooks._ns["layout"] = []
    hooks._ns["setup"] = []
    hooks._ns["route"] = []
    hooks._ns["error"] = []
    hooks._ns["callback"] = []
    hooks._ns["index"] = []
    hooks._ns["custom_data"] = []
    hooks._ns["dev_tools"] = []
    hooks._css_dist = []
    hooks._js_dist = []
    hooks._finals = {}
    hooks._clientside_callbacks = []


def test_hook001_layout(hook_cleanup, dash_duo):
    @hooks.layout()
    def on_layout(layout):
        return [html.Div("Header", id="header")] + layout

    app = Dash()
    app.layout = [html.Div("Body", id="body")]

    dash_duo.start_server(app)

    dash_duo.wait_for_text_to_equal("#header", "Header")
    dash_duo.wait_for_text_to_equal("#body", "Body")


def test_hook002_setup(hook_cleanup):
    setup_title = None

    @hooks.setup()
    def on_setup(app: Dash):
        nonlocal setup_title
        setup_title = app.title

    app = Dash(title="setup-test")
    app.layout = html.Div("setup")

    assert setup_title == "setup-test"


def test_hook003_route(hook_cleanup, dash_duo):
    @hooks.route(methods=("POST",))
    def hook_route():
        return jsonify({"success": True})

    app = Dash()
    app.layout = html.Div("hook route")

    dash_duo.start_server(app)
    response = requests.post(f"{dash_duo.server_url}/hook_route")
    data = response.json()
    assert data["success"]


def test_hook004_error(hook_cleanup, dash_duo):
    @hooks.error()
    def on_error(error):
        set_props("error", {"children": str(error)})

    app = Dash()
    app.layout = [html.Button("start", id="start"), html.Div(id="error")]

    @app.callback(Input("start", "n_clicks"), prevent_initial_call=True)
    def on_click(_):
        raise Exception("hook error")

    dash_duo.start_server(app)
    dash_duo.wait_for_element("#start").click()
    dash_duo.wait_for_text_to_equal("#error", "hook error")


def test_hook005_callback(hook_cleanup, dash_duo):
    @hooks.callback(
        Output("output", "children"),
        Input("start", "n_clicks"),
        prevent_initial_call=True,
    )
    def on_hook_cb(n_clicks):
        return f"clicked {n_clicks}"

    app = Dash()
    app.layout = [
        html.Button("start", id="start"),
        html.Div(id="output"),
    ]

    dash_duo.start_server(app)
    dash_duo.wait_for_element("#start").click()
    dash_duo.wait_for_text_to_equal("#output", "clicked 1")


def test_hook006_priority_final(hook_cleanup, dash_duo):
    @hooks.layout(final=True)
    def hook_final(layout):
        return html.Div([html.Div("final")] + [layout], id="final-wrapper")

    @hooks.layout()
    def hook1(layout):
        layout.children.append(html.Div("first"))
        return layout

    @hooks.layout()
    def hook2(layout):
        layout.children.append(html.Div("second"))
        return layout

    # This appears after the layout
    @hooks.layout(priority=12)
    def hook4(layout):
        layout.children.append(html.Div("Prime"))
        return layout

    # Should still be last after setting a new max.
    @hooks.layout()
    def hook3(layout):
        layout.children.append(html.Div("third"))
        return layout

    app = Dash()

    app.layout = html.Div([html.Div("layout")], id="body")

    dash_duo.start_server(app)
    dash_duo.wait_for_text_to_equal("#final-wrapper > div:first-child", "final")
    dash_duo.wait_for_text_to_equal("#body > div:first-child", "layout")
    dash_duo.wait_for_text_to_equal("#body > div:nth-child(2)", "Prime")
    dash_duo.wait_for_text_to_equal("#body > div:nth-child(3)", "first")
    dash_duo.wait_for_text_to_equal("#body > div:nth-child(4)", "second")
    dash_duo.wait_for_text_to_equal("#body > div:nth-child(5)", "third")


def test_hook007_hook_index(hook_cleanup, dash_duo):
    @hooks.index()
    def hook_index(index: str):
        body = "<body>"
        ib = index.find(body) + len(body)
        injected = '<div id="hooked">Hooked</div>'
        new_index = index[ib:] + injected + index[: ib + 1]
        return new_index

    app = Dash()
    app.layout = html.Div(["index"])

    dash_duo.start_server(app)
    dash_duo.wait_for_text_to_equal("#hooked", "Hooked")


def test_hook008_hook_distributions(hook_cleanup, dash_duo):
    js_uri = "https://example.com/none.js"
    css_uri = "https://example.com/none.css"
    hooks.script([{"external_url": js_uri, "external_only": True}])
    hooks.stylesheet([{"external_url": css_uri, "external_only": True}])

    app = Dash()
    app.layout = html.Div("distribute")

    dash_duo.start_server(app)

    assert dash_duo.find_element(f'script[src="{js_uri}"]')
    assert dash_duo.find_element(f'link[href="{css_uri}"]')


def test_hook009_hook_clientside_callback(hook_cleanup, dash_duo):
    hooks.clientside_callback(
        "(n) => `Called ${n}`",
        Output("hook-output", "children"),
        Input("hook-start", "n_clicks"),
        prevent_initial_call=True,
    )

    app = Dash()
    app.layout = [
        html.Button("start", id="hook-start"),
        html.Div(id="hook-output"),
    ]

    dash_duo.start_server(app)

    dash_duo.wait_for_element("#hook-start").click()
    dash_duo.wait_for_text_to_equal("#hook-output", "Called 1")


def test_hook010_hook_custom_data(hook_cleanup, dash_duo):
    @hooks.custom_data("custom")
    def custom_data(_):
        return "custom-data"

    app = Dash()
    app.layout = [html.Button("insert", id="btn"), html.Div(id="output")]

    @app.callback(
        Output("output", "children"),
        Input("btn", "n_clicks"),
        prevent_initial_call=True,
    )
    def cb(_):
        return ctx.custom_data.custom

    dash_duo.start_server(app)
    dash_duo.wait_for_element("#btn").click()
    dash_duo.wait_for_text_to_equal("#output", "custom-data")


def test_hook011_devtool_hook(hook_cleanup, dash_duo):
    hooks.devtool(
        "dash_html_components", "Button", {"children": "devtool", "id": "devtool"}
    )

    app = Dash()
    app.layout = html.Div(["hooked", html.Div(id="output")])

    @app.callback(
        Output("output", "children"),
        Input("devtool", "n_clicks", allow_optional=True),
        prevent_initial_call=True,
    )
    def cb(_):
        return "hooked from devtools"

    dash_duo.start_server(
        app,
        debug=True,
        use_reloader=False,
        use_debugger=True,
        dev_tools_hot_reload=False,
        dev_tools_props_check=False,
        dev_tools_disable_version_check=True,
    )
    dash_duo.wait_for_element("#devtool").click()
    dash_duo.wait_for_text_to_equal("#output", "hooked from devtools")


def test_hook012_get_app_available_in_hooks_on_routes(hook_cleanup, dash_duo):
    """Test that get_app() is available during hooks when @with_app_context decorated routes are called."""

    # Track which hooks were able to access get_app()
    hook_access_results = {
        "layout_hook": False,
        "error_hook": False,
        "callback_hook": False,
    }

    @hooks.layout()
    def layout_hook(layout):
        try:
            retrieved_app = get_app()
            hook_access_results["layout_hook"] = retrieved_app is not None
        except Exception:
            hook_access_results["layout_hook"] = False
        return layout

    @hooks.error()
    def error_hook(error):
        try:
            retrieved_app = get_app()
            hook_access_results["error_hook"] = retrieved_app is not None
        except Exception:
            hook_access_results["error_hook"] = False

    @hooks.callback(
        Output("hook-output", "children"),
        Input("hook-button", "n_clicks"),
        prevent_initial_call=True,
    )
    def callback_hook(n_clicks):
        try:
            retrieved_app = get_app()
            hook_access_results["callback_hook"] = retrieved_app is not None
            return f"Hook callback: {n_clicks}"
        except Exception as err:
            hook_access_results["callback_hook"] = False
            return f"Error in hook callback: {err}"

    app = Dash(__name__)
    app.layout = [
        html.Div("Test get_app in hooks", id="main"),
        html.Button("Click for callback", id="button"),
        html.Div(id="output"),
        html.Button("Hook callback", id="hook-button"),
        html.Div(id="hook-output"),
        html.Button("Error", id="error-btn"),
        html.Div(id="error-output"),
    ]

    @app.callback(
        Output("output", "children"),
        Input("button", "n_clicks"),
        prevent_initial_call=True,
    )
    def test_callback(n_clicks):
        return f"Clicked {n_clicks} times"

    @app.callback(
        Output("error-output", "children"),
        Input("error-btn", "n_clicks"),
        prevent_initial_call=True,
    )
    def error_callback(n_clicks):
        if n_clicks:
            raise ValueError("Test error for hook")
        return ""

    dash_duo.start_server(app)

    # Test the @with_app_context decorated routes

    # 2. Test layout hook via index route (GET /)
    dash_duo.wait_for_text_to_equal("#main", "Test get_app in hooks")

    # 3. Test callback hook via dispatch route (POST /_dash-update-component)
    dash_duo.wait_for_element("#hook-button").click()
    dash_duo.wait_for_text_to_equal("#hook-output", "Hook callback: 1")

    # 4. Test error hook via dispatch route when error occurs
    dash_duo.wait_for_element("#error-btn").click()
    # Give error hook time to execute
    import time

    time.sleep(0.5)

    # Verify that get_app() worked in hooks during route calls with @with_app_context
    assert hook_access_results[
        "layout_hook"
    ], "get_app() should be accessible in layout hook when serve_layout/index routes have @with_app_context"
    assert hook_access_results[
        "callback_hook"
    ], "get_app() should be accessible in callback hook when dispatch route has @with_app_context"
    assert hook_access_results[
        "error_hook"
    ], "get_app() should be accessible in error hook when dispatch route has @with_app_context"
