use std::time::Duration;

use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Constraint, Rect};
use ratatui_core::style::Color;
use ratatui_core::widgets::StatefulWidget;
use ratatui_widgets::block::Block;

use tui_overlay::{Anchor, Backdrop, Easing, Overlay, OverlayState, Slide};

const AREA: Rect = Rect {
    x: 0,
    y: 0,
    width: 80,
    height: 24,
};

fn render(overlay: Overlay, state: &mut OverlayState) -> Buffer {
    let mut buf = Buffer::empty(AREA);
    overlay.render(AREA, &mut buf, state);
    buf
}

fn open_state() -> OverlayState {
    let mut s = OverlayState::new();
    s.open();
    s
}

// ============================================================================
// Anchor placement (01_positioning.feature)
// ============================================================================

#[test]
fn anchor_placement_all_nine() {
    let cases: &[(Anchor, u16, u16)] = &[
        (Anchor::TopLeft, 0, 0),
        (Anchor::Top, 30, 0),
        (Anchor::TopRight, 60, 0),
        (Anchor::Left, 0, 7),
        (Anchor::Center, 30, 7),
        (Anchor::Right, 60, 7),
        (Anchor::BottomLeft, 0, 14),
        (Anchor::Bottom, 30, 14),
        (Anchor::BottomRight, 60, 14),
    ];

    for &(anchor, ex, ey) in cases {
        let mut state = open_state();

        let overlay = Overlay::new()
            .anchor(anchor)
            .width(Constraint::Length(20))
            .height(Constraint::Length(10));

        render(overlay, &mut state);

        let inner = state.inner_area().expect("should be open");
        assert_eq!(
            (inner.x, inner.y),
            (ex, ey),
            "anchor {anchor:?} expected ({ex}, {ey}), got ({}, {})",
            inner.x,
            inner.y
        );
        assert_eq!((inner.width, inner.height), (20, 10), "anchor {anchor:?}");
    }
}

// ============================================================================
// Sizing (01_positioning.feature)
// ============================================================================

#[test]
fn percentage_width_and_height() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Center)
        .width(Constraint::Percentage(50))
        .height(Constraint::Percentage(50));

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.width, inner.height), (40, 12));
    assert_eq!((inner.x, inner.y), (20, 6));
}

#[test]
fn default_width_height_fills_parent() {
    let mut state = open_state();
    render(Overlay::new().anchor(Anchor::Center), &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.width, inner.height), (80, 24));
}

#[test]
fn length_constraint_fixed_size() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::TopRight)
        .width(Constraint::Length(40))
        .height(Constraint::Length(3));

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.width, inner.height), (40, 3));
    assert_eq!((inner.x, inner.y), (40, 0));
}

#[test]
fn clamps_to_parent_area() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Center)
        .width(Constraint::Length(200))
        .height(Constraint::Length(100));

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.width, inner.height), (80, 24));
}

// ============================================================================
// Offset (01_positioning.feature)
// ============================================================================

#[test]
fn offset_nudges_from_anchor() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::TopRight)
        .width(Constraint::Length(40))
        .height(Constraint::Length(3))
        .offset(-2, 1);

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.x, inner.y), (38, 1));
}

#[test]
fn offset_clamps_to_parent() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::TopLeft)
        .width(Constraint::Length(20))
        .height(Constraint::Length(10))
        .offset(-5, -5);

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.x, inner.y), (0, 0));
}

// ============================================================================
// Independent axes (01_positioning.feature)
// ============================================================================

#[test]
fn independent_width_and_height() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::BottomLeft)
        .width(Constraint::Percentage(60))
        .height(Constraint::Length(5));

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.width, inner.height), (48, 5));
    assert_eq!((inner.x, inner.y), (0, 19));
}

// ============================================================================
// Rendering (04_rendering.feature)
// ============================================================================

#[test]
fn closed_overlay_does_not_modify_buffer() {
    let mut state = OverlayState::new();
    let before = Buffer::empty(AREA);

    let mut buf = Buffer::empty(AREA);
    Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .render(AREA, &mut buf, &mut state);

    assert_eq!(buf, before);
}

#[test]
fn inner_area_none_when_closed() {
    let mut state = OverlayState::new();
    render(Overlay::new(), &mut state);
    assert!(state.inner_area().is_none());
}

#[test]
fn inner_area_some_when_open() {
    let mut state = open_state();

    render(
        Overlay::new()
            .anchor(Anchor::Right)
            .width(Constraint::Percentage(30)),
        &mut state,
    );

    assert!(state.inner_area().is_some());
}

#[test]
fn inner_area_inset_by_block_borders() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .block(Block::bordered());

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();

    // 80 * 30% = 24 wide, anchored right → x=56
    // With borders: inner inset by 1 on each side
    assert_eq!(inner.x, 57);
    assert_eq!(inner.y, 1);
    assert_eq!(inner.width, 22);
    assert_eq!(inner.height, 22);
}

#[test]
fn no_block_inner_equals_overlay_rect() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Center)
        .width(Constraint::Percentage(60))
        .height(Constraint::Percentage(50));

    render(overlay, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.width, inner.height), (48, 12));
    assert_eq!((inner.x, inner.y), (16, 6));
}

#[test]
fn block_renders_border_characters() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Center)
        .width(Constraint::Percentage(60))
        .height(Constraint::Percentage(50))
        .block(Block::bordered().title("Details"));

    let buf = render(overlay, &mut state);

    // Overlay rect: 48 wide, 12 tall, origin (16, 6)
    assert_eq!(buf[(16, 6)].symbol(), "┌");
    assert_eq!(buf[(63, 6)].symbol(), "┐");
    assert_eq!(buf[(16, 17)].symbol(), "└");
    assert_eq!(buf[(63, 17)].symbol(), "┘");
}

#[test]
fn block_renders_title() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Center)
        .width(Constraint::Percentage(60))
        .height(Constraint::Percentage(50))
        .block(Block::bordered().title("Details"));

    let buf = render(overlay, &mut state);

    // Title renders after the top-left corner
    let title: String = (17..24).map(|x| buf[(x, 6)].symbol().to_string()).collect();

    assert!(title.contains("Details"), "title not found: {title:?}");
}

// ============================================================================
// Hit testing — overlay_rect (04_rendering.feature)
// ============================================================================

#[test]
fn overlay_rect_returns_full_bounds_when_open() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .block(Block::bordered());

    render(overlay, &mut state);

    let outer = state.overlay_rect().unwrap();
    let inner = state.inner_area().unwrap();

    // overlay_rect is the full 24-wide rect; inner_area is inset by borders
    assert_eq!(outer.width, 24);
    assert_eq!(outer.height, 24);
    assert!(inner.x > outer.x);
    assert!(inner.y > outer.y);
    assert!(inner.width < outer.width);
    assert!(inner.height < outer.height);
}

#[test]
fn overlay_rect_none_when_closed() {
    let mut state = OverlayState::new();
    render(Overlay::new(), &mut state);

    assert!(state.overlay_rect().is_none());
    assert!(state.inner_area().is_none());
}

#[test]
fn overlay_rect_equals_inner_without_block() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100));

    render(overlay, &mut state);

    assert_eq!(state.overlay_rect(), state.inner_area());
}

// ============================================================================
// State machine basics
// ============================================================================

#[test]
fn new_state_is_closed() {
    let state = OverlayState::new();
    assert!(state.is_closed());
    assert!(!state.is_open());
    assert!(state.inner_area().is_none());
}

#[test]
fn toggle_opens_closed() {
    let mut state = OverlayState::new();
    state.toggle();
    assert!(state.is_open());
}

#[test]
fn toggle_closes_open() {
    let mut state = open_state();
    state.toggle();
    assert!(state.is_closed());
}

// ============================================================================
// Backdrop dimming (02_backdrop.feature)
// ============================================================================

/// Build a buffer with pre-rendered content: every cell gets a known fg/bg/symbol.
fn prefilled_buffer() -> Buffer {
    let mut buf = Buffer::empty(AREA);

    for y in 0..AREA.height {
        for x in 0..AREA.width {
            let cell = &mut buf[(x, y)];
            cell.set_char('X');
            cell.fg = Color::Rgb(200, 200, 200);
            cell.bg = Color::Rgb(40, 40, 40);
        }
    }

    buf
}

fn render_into(buf: &mut Buffer, overlay: Overlay, state: &mut OverlayState) {
    overlay.render(AREA, buf, state);
}

#[test]
fn backdrop_sets_bg_to_base_outside_overlay() {
    let mut state = open_state();
    let mut buf = prefilled_buffer();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    // 80 * 30% = 24 → overlay x=56..80
    // Cell at (5, 5) is outside overlay
    assert_eq!(buf[(5, 5)].bg, Color::Rgb(0, 0, 0));
}

#[test]
fn backdrop_sets_derived_fg_outside_overlay() {
    let mut state = open_state();
    let mut buf = prefilled_buffer();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    // Derived fg from Rgb(0,0,0): 0/2+64 = 64 → Rgb(64,64,64)
    assert_eq!(buf[(5, 5)].fg, Color::Rgb(64, 64, 64));
}

#[test]
fn backdrop_preserves_symbols() {
    let mut state = open_state();
    let mut buf = prefilled_buffer();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    assert_eq!(buf[(5, 5)].symbol(), "X");
}

#[test]
fn backdrop_does_not_dim_inside_overlay() {
    let mut state = open_state();
    let mut buf = prefilled_buffer();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    // Overlay rect starts at x=56. Cells inside are cleared (not dimmed).
    // The clear_region resets cells, so they won't have the backdrop bg.
    let inner = state.inner_area().unwrap();
    let mid_x = inner.x + inner.width / 2;
    let mid_y = inner.y + inner.height / 2;

    // Inside cells were reset, not given backdrop colors
    assert_ne!(buf[(mid_x, mid_y)].bg, Color::Rgb(0, 0, 0));
}

#[test]
fn backdrop_reset_bg_resolves_to_base() {
    let mut state = open_state();
    let mut buf = Buffer::empty(AREA);

    // Cell has Reset bg (default)
    assert_eq!(buf[(5, 5)].bg, Color::Reset);

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(20, 20, 30)));

    render_into(&mut buf, overlay, &mut state);

    assert_eq!(buf[(5, 5)].bg, Color::Rgb(20, 20, 30));
}

#[test]
fn backdrop_dims_rgb_fg() {
    let mut state = open_state();
    let mut buf = prefilled_buffer();

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    // Original fg was Rgb(200,200,200), now should be derived Rgb(64,64,64)
    let fg = buf[(5, 5)].fg;
    if let Color::Rgb(r, g, b) = fg {
        assert!(r < 200 && g < 200 && b < 200, "channels should be reduced");
    } else {
        panic!("expected Rgb, got {fg:?}");
    }
}

#[test]
fn backdrop_dims_indexed_cube_color() {
    let mut state = open_state();
    let mut buf = Buffer::empty(AREA);
    buf[(5, 5)].fg = Color::Indexed(196);

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    // Indexed(196) was replaced with derived fg
    assert_ne!(buf[(5, 5)].fg, Color::Indexed(196));
    assert_eq!(buf[(5, 5)].fg, Color::Rgb(64, 64, 64));
}

#[test]
fn backdrop_dims_grayscale_indexed() {
    let mut state = open_state();
    let mut buf = Buffer::empty(AREA);
    buf[(5, 5)].fg = Color::Indexed(250);

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    assert_ne!(buf[(5, 5)].fg, Color::Indexed(250));
    assert_eq!(buf[(5, 5)].fg, Color::Rgb(64, 64, 64));
}

#[test]
fn backdrop_dark_base_derives_light_fg() {
    let mut state = open_state();
    let mut buf = Buffer::empty(AREA);

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)));

    render_into(&mut buf, overlay, &mut state);

    assert_eq!(buf[(5, 5)].fg, Color::Rgb(64, 64, 64));
}

#[test]
fn backdrop_light_base_derives_darker_fg() {
    let mut state = open_state();
    let mut buf = Buffer::empty(AREA);

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(240, 240, 240)));

    render_into(&mut buf, overlay, &mut state);

    assert_eq!(buf[(5, 5)].fg, Color::Rgb(184, 184, 184));
}

#[test]
fn backdrop_explicit_fg_overrides_derived() {
    let mut state = open_state();
    let mut buf = Buffer::empty(AREA);

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100))
        .backdrop(Backdrop::new(Color::Rgb(0, 0, 0)).fg(Color::Rgb(80, 80, 80)));

    render_into(&mut buf, overlay, &mut state);

    assert_eq!(buf[(5, 5)].fg, Color::Rgb(80, 80, 80));
}

#[test]
fn no_backdrop_leaves_cells_unchanged() {
    let mut state = open_state();
    let mut buf = prefilled_buffer();
    let original_fg = buf[(5, 5)].fg;
    let original_bg = buf[(5, 5)].bg;

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100));

    render_into(&mut buf, overlay, &mut state);

    // Cell outside overlay should be untouched
    assert_eq!(buf[(5, 5)].fg, original_fg);
    assert_eq!(buf[(5, 5)].bg, original_bg);
}

// ============================================================================
// State machine (03_animation.feature)
// ============================================================================

const DUR_150: Duration = Duration::from_millis(150);
const DUR_200: Duration = Duration::from_millis(200);

fn animated_state() -> OverlayState {
    OverlayState::new()
        .with_duration(DUR_150)
        .with_easing(Easing::EaseOut)
}

#[test]
fn animation_starts_closed() {
    let state = animated_state();
    assert!(state.is_closed());
    assert!(!state.is_animating());
    assert!(state.inner_area().is_none());
}

#[test]
fn open_transitions_to_opening() {
    let mut state = animated_state();
    state.open();
    assert!(state.is_animating());
    assert!(!state.is_open());
    assert!(!state.is_closed());
}

#[test]
fn tick_advances_opening_to_open() {
    let mut state = animated_state();
    state.open();
    state.tick(DUR_150);
    assert!(state.is_open());
    assert!(!state.is_animating());
}

#[test]
fn close_transitions_to_closing() {
    let mut state = animated_state();
    state.open();
    state.tick(DUR_150);
    state.close();
    assert!(state.is_animating());
    assert!(!state.is_open());
}

#[test]
fn tick_advances_closing_to_closed() {
    let mut state = animated_state();
    state.open();
    state.tick(DUR_150);
    state.close();
    state.tick(DUR_150);
    assert!(state.is_closed());
    assert!(state.inner_area().is_none());
}

#[test]
fn animated_toggle_opens_closed() {
    let mut state = animated_state();
    state.toggle();
    assert!(state.is_animating());
}

#[test]
fn animated_toggle_closes_open() {
    let mut state = animated_state();
    state.open();
    state.tick(DUR_150);
    state.toggle();
    assert!(state.is_animating());
}

// ============================================================================
// Interruption (03_animation.feature)
// ============================================================================

#[test]
fn close_during_opening_reverses_from_progress() {
    let mut state = OverlayState::new()
        .with_duration(DUR_200)
        .with_easing(Easing::Linear);

    state.open();
    state.tick(Duration::from_millis(100));

    // 50% progress toward open
    state.close();
    assert!(state.is_animating());

    // Remaining close time ≈ 100ms (50% of 200ms)
    state.tick(Duration::from_millis(100));
    assert!(state.is_closed());
}

#[test]
fn open_during_closing_reverses_from_progress() {
    let mut state = OverlayState::new()
        .with_duration(DUR_200)
        .with_easing(Easing::Linear);

    state.open();
    state.tick(DUR_200);
    state.close();
    state.tick(Duration::from_millis(50));

    // 75% visible (25% through closing)
    state.open();
    assert!(state.is_animating());

    // Remaining open time ≈ 50ms (25% of 200ms)
    state.tick(Duration::from_millis(50));
    assert!(state.is_open());
}

// ============================================================================
// Slide directions (03_animation.feature)
// ============================================================================

#[test]
fn slide_right_clips_horizontally() {
    let mut state = OverlayState::new()
        .with_duration(DUR_200)
        .with_easing(Easing::Linear);

    state.open();
    state.tick(Duration::from_millis(100));

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .slide(Slide::Right)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100));

    let mut buf = Buffer::empty(AREA);
    overlay.render(AREA, &mut buf, &mut state);

    // 80 * 30% = 24 wide. At 50% linear: 12 columns visible on right edge
    let inner = state.inner_area().unwrap();
    assert_eq!(inner.width, 12);
    assert_eq!(inner.x, 68);
}

#[test]
fn slide_left_clips_horizontally() {
    let mut state = OverlayState::new()
        .with_duration(DUR_200)
        .with_easing(Easing::Linear);

    state.open();
    state.tick(Duration::from_millis(100));

    let overlay = Overlay::new()
        .anchor(Anchor::Left)
        .slide(Slide::Left)
        .width(Constraint::Percentage(30))
        .height(Constraint::Percentage(100));

    let mut buf = Buffer::empty(AREA);
    overlay.render(AREA, &mut buf, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!(inner.width, 12);
    assert_eq!(inner.x, 0);
}

#[test]
fn slide_bottom_clips_vertically() {
    let mut state = OverlayState::new()
        .with_duration(DUR_200)
        .with_easing(Easing::Linear);

    state.open();
    state.tick(Duration::from_millis(100));

    let overlay = Overlay::new()
        .anchor(Anchor::Bottom)
        .slide(Slide::Bottom)
        .width(Constraint::Percentage(100))
        .height(Constraint::Percentage(50));

    let mut buf = Buffer::empty(AREA);
    overlay.render(AREA, &mut buf, &mut state);

    // 24 * 50% = 12 tall. At 50% linear: 6 rows visible at bottom
    let inner = state.inner_area().unwrap();
    assert_eq!(inner.height, 6);
    assert_eq!(inner.y, 18);
}

#[test]
fn slide_top_clips_vertically() {
    let mut state = OverlayState::new()
        .with_duration(DUR_200)
        .with_easing(Easing::Linear);

    state.open();
    state.tick(Duration::from_millis(100));

    let overlay = Overlay::new()
        .anchor(Anchor::Top)
        .slide(Slide::Top)
        .width(Constraint::Percentage(100))
        .height(Constraint::Percentage(50));

    let mut buf = Buffer::empty(AREA);
    overlay.render(AREA, &mut buf, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!(inner.height, 6);
    assert_eq!(inner.y, 0);
}

#[test]
fn no_slide_instant_appear() {
    let mut state = open_state();

    let overlay = Overlay::new()
        .anchor(Anchor::Center)
        .width(Constraint::Percentage(60))
        .height(Constraint::Percentage(50));

    let mut buf = Buffer::empty(AREA);
    overlay.render(AREA, &mut buf, &mut state);

    let inner = state.inner_area().unwrap();
    assert_eq!((inner.width, inner.height), (48, 12));
}

// ============================================================================
// Easing curves (03_animation.feature)
// ============================================================================

#[test]
fn easing_at_midpoint() {
    let cases: &[(Easing, f32)] = &[
        (Easing::Linear, 0.50),
        (Easing::EaseIn, 0.25),
        (Easing::EaseOut, 0.75),
        (Easing::EaseInOut, 0.50),
    ];

    for &(easing, expected) in cases {
        let result = easing.apply(0.5);
        assert!(
            (result - expected).abs() < 0.01,
            "{easing:?} at 0.5: expected {expected}, got {result}"
        );
    }
}

#[test]
fn easing_at_boundaries() {
    for easing in [
        Easing::Linear,
        Easing::EaseIn,
        Easing::EaseOut,
        Easing::EaseInOut,
    ] {
        assert_eq!(easing.apply(0.0), 0.0, "{easing:?} at 0.0");
        assert_eq!(easing.apply(1.0), 1.0, "{easing:?} at 1.0");
    }
}

#[test]
fn ease_out_reveals_more_early() {
    let mut state = OverlayState::new()
        .with_duration(DUR_200)
        .with_easing(Easing::EaseOut);

    state.open();
    state.tick(Duration::from_millis(50));

    let overlay = Overlay::new()
        .anchor(Anchor::Right)
        .slide(Slide::Right)
        .width(Constraint::Length(30))
        .height(Constraint::Percentage(100));

    let mut buf = Buffer::empty(AREA);
    overlay.render(AREA, &mut buf, &mut state);

    // 25% time with EaseOut: eased ≈ 0.4375, visible ≈ 13 cols
    let inner = state.inner_area().unwrap();
    assert!(
        inner.width > 7,
        "EaseOut at 25% should show >7 cols, got {}",
        inner.width
    );
}
