use std::{error::Error, time::Duration};

use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use gpui::{Bounds, point, px, size};

use super::WebSurfaceStore;
use crate::shell::web_surface_state::WebSurfaceInputOutcome;

fn assert_applied(outcome: WebSurfaceInputOutcome) {
    assert_eq!(outcome, WebSurfaceInputOutcome::Applied);
}

#[test]
fn typed_text_enters_pending_input_after_clicked_viewport() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/form")?;

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_applied(store.record_click_point(
        tab.id(),
        tab.url().as_str(),
        point(px(160.0), px(120.0)),
        1.0,
    ));
    assert_applied(store.record_typed_text(tab.id(), tab.url().as_str(), "e"));
    assert_applied(store.record_typed_text(tab.id(), tab.url().as_str(), "l"));

    let input = store.take_pending_input(tab.id(), tab.url().as_str());

    assert_eq!(input.click_point.map(|point| (point.x(), point.y())), Some((160, 120)));
    assert_eq!(input.typed_text.as_deref(), Some("el"));
    Ok(())
}

#[test]
fn scroll_delta_enters_pending_input_after_wheel() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/list")?;

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_applied(store.record_scroll_delta(
        tab.id(),
        tab.url().as_str(),
        point(px(0.0), px(140.0)),
        point(px(320.0), px(240.0)),
        1.0,
    ));
    assert_applied(store.record_scroll_delta(
        tab.id(),
        tab.url().as_str(),
        point(px(0.0), px(60.0)),
        point(px(300.0), px(220.0)),
        1.0,
    ));

    let input = store.take_pending_input(tab.id(), tab.url().as_str());

    assert_eq!(input.scroll_offset.y(), 200);
    assert_eq!(input.scroll_delta.map(|delta| (delta.x(), delta.y())), Some((0, 200)));
    assert_eq!(input.scroll_point.map(|point| (point.x(), point.y())), Some((300, 220)));
    Ok(())
}

#[test]
fn viewport_size_changes_on_first_clean_measurement() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/resize")?;

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_applied(store.record_viewport_size(tab.id(), resized_once_bounds(), 1.0));
    Ok(())
}

/// A sidebar / window animation emits a new viewport bounds on every
/// GPUI paint — without this debounce the renderer would call Servo's
/// `rendering_context.resize` per frame, which destroys and reallocates
/// the framebuffer and flashes a blank surface for every animation
/// tick. `record_viewport_size` keeps the latest size in the store so
/// the cadence-driven retry will pick it up once the gesture settles,
/// but returns `Buffered` so the synchronous flush is skipped.
#[test]
fn rapid_viewport_changes_buffer_until_gesture_settles() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/animation")?;

    assert_eq!(
        store.record_viewport_size(tab.id(), web_bounds(), 1.0),
        WebSurfaceInputOutcome::Applied,
        "first measurement always fires immediately so the page can load",
    );
    assert_eq!(
        store.record_viewport_size(tab.id(), resized_once_bounds(), 1.0),
        WebSurfaceInputOutcome::Applied,
        "the first transition after an untimed measurement still fires once",
    );
    assert_eq!(
        store.record_viewport_size(tab.id(), web_bounds(), 1.0),
        WebSurfaceInputOutcome::Buffered,
        "a rapid second transition collapses into the trailing-edge resize",
    );
    Ok(())
}

/// Regression — a wheel scroll between focusing a field and typing
/// must not erase keyboard focus or buffered keystrokes.
///
/// The `record_scroll_delta` path drops `click_point` on purpose
/// (its viewport coordinates are stale once the page scrolls) but
/// must keep `keyboard_focus` and `typed_text`, so a focused input
/// keeps receiving the user's keystrokes after they wheel-scroll.
/// Locks the fix from commit fcac326-era input-loss work.
#[test]
fn scroll_after_click_keeps_keyboard_focus_and_typed_text() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/form")?;
    let url = tab.url().as_str();

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_applied(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)), 1.0));
    assert_applied(store.record_typed_text(tab.id(), url, "h"));

    assert_applied(store.record_scroll_delta(
        tab.id(),
        url,
        point(px(0.0), px(140.0)),
        point(px(160.0), px(120.0)),
        1.0,
    ));

    assert_eq!(
        store.record_typed_text(tab.id(), url, "i"),
        WebSurfaceInputOutcome::Applied,
        "scroll must not erase keyboard focus — typing after a scroll should still buffer",
    );

    let input = store.take_pending_input(tab.id(), url);

    assert_eq!(
        input.scroll_delta.map(|delta| (delta.x(), delta.y())),
        Some((0, 140)),
        "scroll delta should reach the Servo runtime",
    );
    assert_eq!(input.scroll_offset.y(), 140);
    assert!(
        input.click_point.is_none(),
        "post-scroll click coordinates would land on the wrong DOM node — they must be dropped",
    );
    assert_eq!(
        input.typed_text.as_deref(),
        Some("hi"),
        "buffered keystrokes from before AND after the scroll must reach the Servo runtime",
    );
    Ok(())
}

/// Locks the Retina (2x) scale path: GPUI delivers logical pixels but
/// Servo expects device pixels, so every coordinate that crosses the
/// boundary must be multiplied by `window.scale_factor()`. Without
/// this regression a future refactor could silently revert to the
/// 1.0-only behavior that left every Retina click landing in the
/// upper-left quadrant of the page.
#[test]
fn retina_scale_factor_doubles_every_input_coordinate() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/form")?;
    let url = tab.url().as_str();

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 2.0));
    assert_applied(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)), 2.0));
    assert_applied(store.record_typed_text(tab.id(), url, "h"));
    assert_applied(store.record_scroll_delta(
        tab.id(),
        url,
        point(px(0.0), px(140.0)),
        point(px(160.0), px(120.0)),
        2.0,
    ));

    let input = store.take_pending_input(tab.id(), url);

    assert_eq!(
        input.scroll_delta.map(|delta| (delta.x(), delta.y())),
        Some((0, 280)),
        "wheel delta of 140 logical px must be 280 device px on Retina",
    );
    assert_eq!(input.scroll_offset.y(), 280, "scroll offset accumulates in device px");
    assert_eq!(
        input.click_point, None,
        "scroll drops the buffered click — its viewport coords are stale",
    );
    Ok(())
}

/// Locks the precondition that `record_typed_text` requires a prior
/// click to have established keyboard focus. Without this guard, a
/// future refactor could silently start buffering stray keystrokes
/// against an unfocused tab — and the user would see characters land
/// on whichever DOM node Servo last focused, with no visible cause.
#[test]
fn typing_without_a_prior_click_is_rejected() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/form")?;
    let url = tab.url().as_str();

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_eq!(
        store.record_typed_text(tab.id(), url, "x"),
        WebSurfaceInputOutcome::DroppedNoKeyboardFocus,
        "typing must fail until a click establishes keyboard focus on this tab and url",
    );
    Ok(())
}

/// Negative-path coverage for `DroppedNoViewportBounds`: a click that
/// arrives before the viewport has reported its bounds (race during
/// first paint) must surface as a typed outcome, not a silent `false`.
#[test]
fn click_before_viewport_measured_reports_no_viewport_bounds() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/form")?;

    assert_eq!(
        store.record_click_point(tab.id(), tab.url().as_str(), point(px(160.0), px(120.0)), 1.0,),
        WebSurfaceInputOutcome::DroppedNoViewportBounds,
    );
    Ok(())
}

/// Negative-path coverage for `DroppedZeroDelta`: a wheel event whose
/// device-pixel delta rounds to zero must surface as the explicit
/// outcome so the renderer skips an unnecessary repaint.
#[test]
fn zero_wheel_delta_reports_zero_delta() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/list")?;

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_eq!(
        store.record_scroll_delta(
            tab.id(),
            tab.url().as_str(),
            point(px(0.0), px(0.0)),
            point(px(160.0), px(120.0)),
            1.0,
        ),
        WebSurfaceInputOutcome::DroppedZeroDelta,
    );
    Ok(())
}

#[test]
fn hover_input_is_rate_limited() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/hover")?;
    let start = std::time::Instant::now();

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_applied(store.record_hover_point_at(tab.id(), point(px(10.0), px(10.0)), 1.0, start));
    assert_eq!(
        store.record_hover_point_at(
            tab.id(),
            point(px(12.0), px(12.0)),
            1.0,
            start + Duration::from_millis(8),
        ),
        WebSurfaceInputOutcome::NoChange,
    );
    assert_applied(store.record_hover_point_at(
        tab.id(),
        point(px(44.0), px(45.0)),
        1.0,
        start + Duration::from_millis(33),
    ));

    let input = store.take_pending_input(tab.id(), tab.url().as_str());

    assert_eq!(input.hover_point.map(|point| (point.x(), point.y())), Some((44, 45)));
    Ok(())
}

/// Pinning the per-tab isolation invariant. A click recorded against
/// tab A must not be drained by, dropped by, or overwritten by any
/// state mutation routed to tab B. The store keys every click on its
/// owning `TabId` via `PerTabSurface`, but the singleton
/// `keyboard_focus` cross-cuts tabs — so a regression that
/// accidentally entangled them (e.g. dropping A's `click_point` when
/// B took focus) would surface here.
#[test]
fn click_on_tab_a_survives_click_on_tab_b() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab_a = web_tab("https://example.com/a")?;
    let tab_b = web_tab("https://example.com/b")?;

    assert_applied(store.record_viewport_size(tab_a.id(), web_bounds(), 1.0));
    assert_applied(store.record_viewport_size(tab_b.id(), web_bounds(), 1.0));

    assert_applied(store.record_click_point(
        tab_a.id(),
        tab_a.url().as_str(),
        point(px(40.0), px(40.0)),
        1.0,
    ));
    assert_applied(store.record_click_point(
        tab_b.id(),
        tab_b.url().as_str(),
        point(px(200.0), px(200.0)),
        1.0,
    ));

    let input_a = store.take_pending_input(tab_a.id(), tab_a.url().as_str());
    assert_eq!(
        input_a.click_point.map(|p| (p.x(), p.y())),
        Some((40, 40)),
        "tab A's click must survive a subsequent click on tab B — \
         per-tab surfaces are independent owners of `click_point`",
    );

    let input_b = store.take_pending_input(tab_b.id(), tab_b.url().as_str());
    assert_eq!(
        input_b.click_point.map(|p| (p.x(), p.y())),
        Some((200, 200)),
        "tab B's click must drain into B's pending input, not A's",
    );
    Ok(())
}

/// Pinning the zero-delta short-circuit's non-effect on a buffered
/// click. `record_scroll_delta` wipes `click_point` (post-scroll
/// coords would target the wrong DOM node), but the early `None`
/// return for `DroppedZeroDelta` must short-circuit *before* the wipe.
/// A future refactor that moved the wipe above the delta check would
/// silently eat clicks whenever a precision-mouse wheel reported a
/// sub-device-pixel delta.
#[test]
fn zero_wheel_delta_must_not_erase_buffered_click() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/form")?;
    let url = tab.url().as_str();

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_applied(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)), 1.0));

    assert_eq!(
        store.record_scroll_delta(
            tab.id(),
            url,
            point(px(0.0), px(0.0)),
            point(px(160.0), px(120.0)),
            1.0,
        ),
        WebSurfaceInputOutcome::DroppedZeroDelta,
    );

    let input = store.take_pending_input(tab.id(), url);
    assert_eq!(
        input.click_point.map(|p| (p.x(), p.y())),
        Some((160, 120)),
        "a zero-delta wheel event reports DroppedZeroDelta and must not \
         take the `click_point` wipe path — the early return guards it",
    );
    Ok(())
}

/// Pinning the bounds-vs-drain decoupling. A viewport resize between
/// click and drain leaves `viewport_bounds` mutated but does not
/// touch `click_point`, `scroll_offset`, or `requested_url` — the
/// three keys the drain filter checks. The click's stored device-px
/// coordinates remain valid against the new bounds because GPUI
/// re-renders before any new click can arrive.
///
/// If a future refactor stored raw window-relative coords on
/// `click_point` and converted them at drain time, a resize between
/// record and drain would shift the result; this test would still
/// drain "something" but the coordinates would change, surfacing the
/// drift.
#[test]
fn click_survives_viewport_bounds_change_before_drain() -> Result<(), Box<dyn Error>> {
    let mut store = WebSurfaceStore::new();
    let tab = web_tab("https://example.com/resize")?;
    let url = tab.url().as_str();

    assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
    assert_applied(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)), 1.0));

    assert_applied(store.record_viewport_size(tab.id(), resized_once_bounds(), 1.0));

    let input = store.take_pending_input(tab.id(), url);
    assert_eq!(
        input.click_point.map(|p| (p.x(), p.y())),
        Some((160, 120)),
        "a resize-in-progress must not steal the buffered click — \
         the drain filter checks url+scroll_offset, not bounds",
    );
    Ok(())
}

/// Servo's `read_pixels(gl::RGBA, gl::UNSIGNED_BYTE)` writes R-G-B-A
/// in memory order. GPUI's `RenderImage` is documented as "in BGRA
/// format" and the macOS Metal atlas uploads bytes as
/// `MTLPixelFormat::BGRA8Unorm` (B-G-R-A in memory). If the bytes
/// crossed the boundary unchanged, the Metal sampler would read the R
/// byte as B and vice versa — every coloured pixel would render with
/// R and B swapped. `WebSurfaceFrame::from_live_frame` is responsible
/// for swapping the two channels before handing the buffer to GPUI.
///
/// This test fed `(R=255, G=0, B=0, A=255)` — one red RGBA pixel —
/// through the live-frame conversion and asserts the resulting
/// `RenderImage` carries `(B=0, G=0, R=255, A=255)` byte-for-byte.
#[test]
fn live_frame_swaps_red_and_blue_bytes_for_gpui_bgra() -> Result<(), Box<dyn Error>> {
    use crate::services::servo_live::ServoLiveFrame;
    use crate::shell::web_surface_frame::WebSurfaceFrame;
    use crate::shell::web_surface_geometry::WebSurfaceScrollOffset;

    let rgba_red_pixel = vec![255u8, 0, 0, 255];
    let live = ServoLiveFrame::for_test(1, 1, rgba_red_pixel);
    let frame = WebSurfaceFrame::from_live_frame(
        "https://example.com/".to_string(),
        WebSurfaceScrollOffset::default(),
        100,
        live,
    )?;

    let image = frame.image.as_ref().ok_or("software frame must produce a RenderImage")?;
    let bytes = image.as_bytes(0).ok_or("RenderImage must expose its first frame's bytes")?;
    assert_eq!(
        bytes,
        &[0u8, 0, 255, 255],
        "Servo's RGBA red pixel must be swapped to BGRA (B=0, G=0, R=255, A=255) \
         before GPUI uploads it as BGRA8Unorm. If this assert says \
         `[255, 0, 0, 255]`, the swap is missing and every coloured \
         pixel on the page renders with R and B exchanged.",
    );
    Ok(())
}

#[test]
fn empty_live_frame_payload_is_rejected() -> Result<(), String> {
    use crate::services::servo_live::ServoLiveFrame;
    use crate::shell::web_surface_frame::WebSurfaceFrame;
    use crate::shell::web_surface_geometry::WebSurfaceScrollOffset;

    let result = WebSurfaceFrame::from_live_frame(
        "https://example.com/".to_string(),
        WebSurfaceScrollOffset::default(),
        100,
        ServoLiveFrame::for_test(1, 1, Vec::new()),
    );

    let error = match result {
        Ok(_) => return Err("empty Servo frame payload reached Ready state".to_string()),
        Err(error) => error,
    };
    assert_eq!(error.to_string(), "servo live frame did not include renderable pixels",);
    Ok(())
}

fn web_bounds() -> Bounds<gpui::Pixels> {
    Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)))
}

fn resized_once_bounds() -> Bounds<gpui::Pixels> {
    Bounds::new(point(px(0.0), px(0.0)), size(px(934.0), px(657.0)))
}

fn web_tab(url: &str) -> Result<BrowserTab, Box<dyn Error>> {
    Ok(BrowserTab::new(TabId::new(), SpaceId::new(), ProfileId::new(), "Web", UrlText::parse(url)?))
}
