# Reference test for WGLMakie HTML widgets
# This test verifies that all widgets render correctly with proper positioning when use_html_widgets=true

using ReferenceTests: RNG, @reference_test
using ReferenceTests, WGLMakie
using Test
import Electron, Bonito

struct EScreenshot
    display
    app::Bonito.App
    capture_full_page::Bool
    # Figures whose render we block on before screenshotting (via
    # `get_screen(fig).plot_initialized`). For `App`s that embed figures, pass them
    # explicitly since we can't recover them from the app.
    figures::Vector{Any}
end

EScreenshot(display, fig::Makie.FigureLike; capture_full_page = false) = EScreenshot(display, App(fig), capture_full_page, Any[fig])
EScreenshot(display, app::Bonito.App; figures = Any[]) = EScreenshot(display, app, true, figures)  # Default to full page for App tests

# Block until every figure has finished its initial render — i.e. `done_init`
# fired, which now means resized-to-parent and painted (see `three_plot.jl` /
# `WGLMakie.js`). This is the source of truth, replacing a fixed sleep. The screen
# is attached during rendering, so wait for it too.
function wait_for_plots(figures; timeout = 60)
    for fig in figures
        scene = Makie.get_scene(fig)
        ok = Bonito.wait_for(timeout = timeout) do
            screen = Makie.getscreen(scene)
            screen !== nothing && isready(screen.plot_initialized)
        end
        if ok != :success
            @warn "figure did not report plot_initialized in time"
            continue
        end
        value = fetch(Makie.getscreen(scene).plot_initialized)   # fetch peeks, leaves it in the channel
        value === true || @warn "figure reported an init error" value
    end
    return
end

function snapshot_figure(edisplay, app, path; capture_full_page = false, figures = Any[])
    rm(path; force = true)
    display(edisplay, app)
    win = edisplay.window.window
    Bonito.wait_for_ready(app)
    wait_for_plots(figures)   # block until every figure is resized-to-parent and painted
    win_size = run(
        win, """(()=>{
            document.body.style.margin = '0';
            document.body.style.padding = '0';
            document.documentElement.style.margin = '0';
            document.documentElement.style.padding = '0';
            document.body.style.overflow = 'hidden'; // Prevent scrollbars

            if ($capture_full_page) {
                // Capture the full body content for testing HTML layouts.
                // Measure the bounding box of the actual content, not viewport
                // dimensions.
                //
                // Bonito wraps the app in `display:contents` fragment divs
                // (`bonito-fragment`). Such elements generate no layout box, so
                // `getBoundingClientRect()` on them is all-zero - iterating
                // `body.children` directly would collapse the measured content
                // height to the body margins. Descend through any
                // `display:contents` wrapper to reach the real content boxes
                // (this reproduces the measurement from before Bonito inserted
                // the fragment wrappers).
                let maxRight = 0;
                let maxBottom = 0;
                const measure = (el) => {
                    for (const child of el.children) {
                        if (getComputedStyle(child).display === "contents") {
                            measure(child);
                        } else {
                            const rect = child.getBoundingClientRect();
                            maxRight = Math.max(maxRight, rect.right);
                            maxBottom = Math.max(maxBottom, rect.bottom);
                        }
                    }
                };
                measure(document.body);

                // If nothing measurable was found, fall back to scrollWidth/Height
                const width = maxRight > 0 ? maxRight : document.body.scrollWidth;
                const height = maxBottom > 0 ? maxBottom : document.body.scrollHeight;

                return [Math.ceil(width), Math.ceil(height)];
            } else {
                // Find the canvas element (Makie renders to canvas)
                const canvas = document.querySelector('canvas');
                const rect = canvas.getBoundingClientRect();
                return [
                    Math.ceil(rect.width),
                    Math.ceil(rect.height)
                ];
            }
        })();
        """
    )
    Electron.ElectronAPI.setContentSize(win, win_size...)
    sleep(1) # give the resize round-trip a chance to start
    wait_for_render(win)
    winid = win.id
    js_path = replace(path, '\\' => '/')
    run(
        edisplay.window.app,
        """
        const win = BrowserWindow.fromId($winid)
        win.webContents.capturePage().then(image => {
            require('fs').writeFileSync('$(js_path)', image.toPNG());
        }).catch(err => console.error('Screenshot error:', err));
        """,
    )
    Bonito.wait_for(() -> isfile(path); timeout = 30)
    return path
end

# A `resize_to` canvas is resized only when the Julia `viewport(scene)` round-trip
# lands (WGLMakie.js `canvas_width` -> `renderer.setSize`), and it re-renders right
# after. So wait (via rAF, no busy Julia loop) until every canvas size holds steady
# for a few frames, then one more frame to paint — robust even when a loaded CI
# runner drags the round-trip out well past a fixed sleep.
function wait_for_render(win; stable = 5, max_frames = 1800)
    run(
        win, """
        new Promise((resolve) => {
            let prev = "", steady = 0, frames = 0;
            const dims = () => [...document.querySelectorAll('canvas')].map(c => c.width + "x" + c.height).join("|");
            const tick = () => {
                const d = dims();
                steady = d === prev ? steady + 1 : 0;
                prev = d;
                (steady >= $stable || ++frames > $max_frames) ?
                    requestAnimationFrame(() => resolve(true)) : requestAnimationFrame(tick);
            };
            requestAnimationFrame(tick);
        })
        """
    )
    return
end

# YAY we can just overload save_results for our own EScreenshot type :)
function ReferenceTests.save_result(path::String, es::EScreenshot)
    isfile(path * ".png") && rm(path * ".png"; force = true)
    snapshot_figure(es.display, es.app, path * ".png"; capture_full_page = es.capture_full_page, figures = es.figures)
    return true
end

# Simple ResizableCard replacement for testing (mimics Splots.ResizableCard behavior)
function TestResizableCard(content; style = Bonito.Styles())
    card_style = Bonito.Styles(
        style,
        Bonito.CSS(
            "position" => "relative",
            "overflow" => "hidden",
            "background-color" => "rgba(1, 1, 1, 0.2)",
            "box-shadow" => "0 4px 8px rgba(0, 0, 0, 0.2)",
            "padding" => "7px",
            "margin" => "2px",
            "border-radius" => "10px"
        )
    )
    return DOM.div(content; style = card_style)
end

function create_test_figure()
    fig = Figure(size = (1200, 900))

    # Top row - Plots with vertical slider next to ax2
    ax1 = Axis(fig[1, 1], title = "Sine Wave (Slider controlled)")
    ax2 = Axis(fig[1, 2], title = "Scatter (Button controlled)")
    sl_vertical = Makie.Slider(fig[1, 3], range = 0:0.1:10, startvalue = 5, horizontal = false)
    ax3 = Axis(fig[1, 4], title = "Heatmap (Menu controlled)")

    # Row 2 - Horizontal sliders with labels
    Label(fig[2, 1], "Frequency:", halign = :right, tellwidth = false)
    sl_freq = Makie.Slider(fig[2, 2:3], range = 0.5:0.1:5, startvalue = 2, tellwidth = false)
    freq_label = Label(fig[2, 4], lift(x -> "$(round(x, digits = 1))", sl_freq.value), halign = :left, tellwidth = false)

    Label(fig[3, 1], "Amplitude:", halign = :right, tellwidth = false)
    sl_amp = Makie.Slider(fig[3, 2:3], range = 0.5:0.1:3, startvalue = 1.5, tellwidth = false)
    amp_label = Label(fig[3, 4], lift(x -> "$(round(x, digits = 1))", sl_amp.value), halign = :left, tellwidth = false)

    Label(fig[4, 1], "Phase:", halign = :right, tellwidth = false)
    sl_phase = Makie.Slider(fig[4, 2:3], range = 0:0.1:2π, startvalue = 0, tellwidth = false)
    phase_label = Label(fig[4, 4], lift(x -> "$(round(x, digits = 2))", sl_phase.value), halign = :left, tellwidth = false)

    # Row 5 - Buttons
    btn_add = Makie.Button(fig[5, 2:3], label = "Add Point", tellwidth = false)
    click_counter = Label(fig[5, 4], lift(n -> "Total: $n clicks", btn_add.clicks), halign = :left, tellwidth = false)

    # Row 6 - Menus
    Label(fig[6, 1], "Colormap:", halign = :right, tellwidth = false)
    menu_cmap = Makie.Menu(fig[6, 2], options = ["viridis", "plasma", "inferno", "magma"], default = "viridis", tellwidth = false)

    num_func = GridLayout(fig[6:7, 3:4])
    Label(num_func[1, 1], "Function:"; halign = :right, tellwidth = false)
    funcs = [sin, cos, tan]
    menu_func = Makie.Menu(
        num_func[1, 2];
        options = zip(["Sine", "Cosine", "Tangent"], funcs),
        default = "Sine", tellwidth = false
    )

    # Row 7 - Textboxes
    Label(fig[7, 1], "Text input:", halign = :right, tellwidth = false)
    textbox_text = Makie.Textbox(fig[7, 2], placeholder = "Type here...", stored_string = "Hello!", tellwidth = false)

    Label(num_func[2, 1], "Number:"; halign = :right, tellwidth = false)
    textbox_num = Makie.Textbox(num_func[2, 2]; validator = Float64, stored_string = "3.14", tellwidth = false)

    # Row 8 - Checkboxes
    Label(fig[8, 1], "Enable feature:", halign = :right, tellwidth = false)
    checkbox_feature = Makie.Checkbox(fig[8, 2], checked = true, tellwidth = false)

    Label(fig[8, 3], "Show grid:", halign = :right, tellwidth = false)
    checkbox_grid = Makie.Checkbox(fig[8, 4], checked = false, tellwidth = false)

    # Row 9 - Toggles
    Label(fig[9, 1], "Dark mode:", halign = :right, tellwidth = false)
    toggle_dark = Makie.Toggle(fig[9, 2], active = false, tellwidth = false)

    Label(fig[9, 3], "Auto-update:", halign = :right, tellwidth = false)
    toggle_update = Makie.Toggle(fig[9, 4], active = true, tellwidth = false)

    # Plot 1: Sine wave controlled by sliders
    xs = 0:0.01:10
    ys = lift(sl_freq.value, sl_amp.value, sl_phase.value) do freq, amp, phase
        amp .* sin.(freq .* xs .+ phase)
    end
    sine_line = lines!(ax1, xs, ys, linewidth = 3, color = :blue)

    # Connect checkbox to control sine wave visibility
    on(checkbox_feature.checked) do checked
        sine_line.visible[] = checked
    end

    # Plot 2: Scatter plot controlled by buttons and vertical slider
    points = Observable(Point2f[])
    scatter!(ax2, points, color = :red, markersize = 15)
    on(btn_add.clicks) do n
        new_point = Point2f(RNG.rand() * 10, sl_vertical.value[])
        push!(points[], new_point)
        notify(points)
    end
    # Current position indicator
    current_marker = lift(sl_vertical.value) do y
        [Point2f(5, y)]
    end
    scatter!(ax2, current_marker, color = :green, markersize = 25, marker = :cross)
    limits!(ax2, 0, 10, 0, 10)

    # Connect checkbox to control grid visibility
    on(checkbox_grid.checked) do checked
        ax2.xgridvisible[] = checked
        ax2.ygridvisible[] = checked
    end

    # Plot 3: Heatmap controlled by menu
    data = RNG.randn(25, 25)
    hmap = heatmap!(ax3, data, colormap = menu_cmap.selection)

    # Connect toggles to control plot features
    on(toggle_dark.active) do active
        # Toggle background color
        color = to_color(active ? :gray20 : :white)
        foreach((ax1, ax2, ax3)) do ax
            ax.backgroundcolor[] = color
        end
    end

    # Bottom row - Live text preview
    preview_text = lift(textbox_text.displayed_string, textbox_num.displayed_string, toggle_update.active) do text, num, auto
        status = auto ? "Auto-update: ON" : "Auto-update: OFF"
        "Preview: \"$text\" | Number: $num | $status"
    end
    Label(fig[10, 1:4], preview_text, fontsize = 18, color = :gray50, tellwidth = false)

    # Show modified function plot
    modified_xs = 0:0.1:10
    modified_ys = lift(menu_func.selection, textbox_num.stored_string) do f, numstr
        multiplier = something(tryparse(Float64, numstr), 1.0)
        multiplier .* f.(modified_xs) .+ 5
    end
    lines!(ax2, modified_xs, modified_ys, color = :orange, linewidth = 4)

    # Manipulate all widgets to test that they update correctly in HTML
    # This verifies bidirectional sync between Julia and HTML widgets
    for i in 1:3
        btn_add.clicks[] = btn_add.clicks[] + 1
    end
    set_close_to!(sl_freq, 3.5)
    set_close_to!(sl_amp, 2.0)
    set_close_to!(sl_phase, π)
    set_close_to!(sl_vertical, 7.5)
    menu_cmap.i_selected[] = 3  # Select "inferno"
    menu_func.i_selected[] = 2  # Select "Cosine"
    Makie.set!(textbox_text, "Updated!")
    Makie.set!(textbox_num, "2.71")
    checkbox_grid.checked[] = false
    toggle_dark.active[] = true
    # Simulate 3 clicks
    return fig
end

# Makie.inline!(Makie.automatic)
edisplay = Bonito.use_electron_display(; devtools = true)
# ReferenceTests.RECORDING_DIR[] = joinpath(pwd(), "WidgetTests")
# ReferenceTests.REGISTERED_TESTS |> empty!
function close_devtools(w)
    return run(w.app, "electron.BrowserWindow.fromId($(w.id)).webContents.closeDevTools()")
end

# Devtools cut off the screenshot!
close_devtools(edisplay.window.window)

@reference_test "Widgets layout" begin
    WGLMakie.activate!(; use_html_widgets = false, px_per_unit = Makie.automatic, scalefactor = Makie.automatic)
    fig = create_test_figure()
    EScreenshot(edisplay, fig)
end

@reference_test "HTML Widgets layout" begin
    WGLMakie.activate!(; use_html_widgets = true, px_per_unit = Makie.automatic, scalefactor = Makie.automatic)
    fig = create_test_figure()
    EScreenshot(edisplay, fig)
end

@reference_test "HTML Widgets layout px_per_unit=1" begin
    WGLMakie.activate!(; use_html_widgets = true, px_per_unit = 1, scalefactor = 1)
    fig = create_test_figure()
    EScreenshot(edisplay, fig)
end

@reference_test "HTML Widgets layout px_per_unit=2" begin
    WGLMakie.activate!(; use_html_widgets = true, px_per_unit = 2, scalefactor = 2)
    fig = create_test_figure()
    EScreenshot(edisplay, fig)
end

@reference_test "resize_to parent with fixed size div" begin
    fig = create_test_figure()
    app = App() do
        DOM.div(
            DOM.h2("resize_to=:parent Test"; style = "text-align: center; color: #666;"),
            DOM.div(
                WGLMakie.WithConfig(fig; use_html_widgets = true, resize_to = :parent);
                style = "width: 1000px; height: 800px; border: 2px solid blue; margin: 10px;",
            ),
        )
    end
    EScreenshot(edisplay, app; figures = [fig])
end

@reference_test "resize_to parent with ResizableCard" begin
    fig = create_test_figure()
    app = App() do
        card = TestResizableCard(WGLMakie.WithConfig(fig; use_html_widgets = true, resize_to = :parent))
        DOM.div(
            DOM.h2("ResizableCard Test"; style = "text-align: center; color: #666;"),
            DOM.div(card; style = "width: 900px; height: 700px; margin: 10px;"),
        )
    end
    EScreenshot(edisplay, app; figures = [fig])
end

@reference_test "resize_to parent nested in styled container" begin
    fig = create_test_figure()
    app = App() do
        DOM.div(
            DOM.h2("Nested Container Test"; style = "text-align: center; color: #666;"),
            DOM.div(
                DOM.div(
                    WGLMakie.WithConfig(fig; use_html_widgets = true, resize_to = :parent);
                    style = "width: 100%; height: 100%;",
                );
                style = "width: 1100px; height: 850px; padding: 20px; background-color: #f0f0f0; border-radius: 8px; margin: 10px;",
            ),
        )
    end
    EScreenshot(edisplay, app; figures = [fig])
end

@reference_test "resize_to parent with multiple figures side by side" begin
    fig1 = Figure(; size = (600, 400))
    ax1 = Axis(fig1[1, 1]; title = "Left Plot")
    sl1 = Makie.Slider(fig1[2, 1]; range = 0:0.1:10, startvalue = 5)
    lines!(ax1, 0:0.1:10, lift(v -> sin.((0:0.1:10) .+ v), sl1.value))

    fig2 = Figure(; size = (600, 400))
    ax2 = Axis(fig2[1, 1]; title = "Right Plot")
    sl2 = Makie.Slider(fig2[2, 1]; range = 0:0.1:10, startvalue = 3)
    lines!(ax2, 0:0.1:10, lift(v -> cos.((0:0.1:10) .+ v), sl2.value))

    app = App() do
        DOM.div(
            DOM.h2("Side by Side Test"; style = "text-align: center; color: #666;"),
            DOM.div(
                DOM.div(
                    WGLMakie.WithConfig(fig1; use_html_widgets = true, resize_to = :parent);
                    style = "width: 100%; height: 100%;",
                ),
                DOM.div(
                    WGLMakie.WithConfig(fig2; use_html_widgets = true, resize_to = :parent);
                    style = "width: 100%; height: 100%;",
                );
                style = "display: flex; gap: 20px; padding: 10px; flex-wrap: wrap;",
            ),
        )
    end
    EScreenshot(edisplay, app; figures = [fig1, fig2])
end

@reference_test "resize_to body baseline" begin
    fig = create_test_figure()
    app = Bonito.App() do
        DOM.div(
            WGLMakie.WithConfig(fig; use_html_widgets = true, resize_to = :body);
            style = "margin: 0; padding: 0;",
        )
    end
    EScreenshot(edisplay, app; figures = [fig])
end

# Reset to default
WGLMakie.activate!(; use_html_widgets = false, px_per_unit = Makie.automatic, scalefactor = Makie.automatic)
