#![ cfg( feature = "test-support" ) ]

// Animation lifecycle coverage. The runtime polls `App::is_animating()` once
// per frame and, while it returns `true`, redraws at ~60 Hz reading
// `Instant::now()` against a stored start time. These tests exercise that
// lifecycle, the swipe-progress callbacks and the `take_focus_request` /
// `on_text_input_focused` hooks against an `App` impl — without the Wayland
// loop, but with the same trait the loop drives.

use std::time::{ Duration, Instant };

use ltk::test_support::WidgetHandlers;
use ltk::core::{ RenderOptions, UiSurface };
use ltk::{
	column, text, App, Color, Element, WidgetId,
};

const TWEEN: Duration = Duration::from_millis( 200 );

#[ derive( Clone, Debug, PartialEq ) ]
enum Msg
{
	StartFade,
	FadeDone,
}

struct Animator
{
	tween_start:    Option<Instant>,
	swipe_progress: f32,
	swipe_h:        f32,
	tap_count:      u32,
	drag_xy:        Option<( f32, f32 )>,
	drop_xy:        Option<( f32, f32 )>,
	focus_request:  Option<WidgetId>,
	ime_active:     bool,
}

impl Animator
{
	fn new() -> Self
	{
		Self
		{
			tween_start:    None,
			swipe_progress: 0.0,
			swipe_h:        0.0,
			tap_count:      0,
			drag_xy:        None,
			drop_xy:        None,
			focus_request:  None,
			ime_active:     false,
		}
	}

	fn tween_progress( &self ) -> f32
	{
		match self.tween_start
		{
			Some( t ) => ( t.elapsed().as_secs_f32() / TWEEN.as_secs_f32() ).clamp( 0.0, 1.0 ),
			None      => 1.0,
		}
	}
}

impl App for Animator
{
	type Message = Msg;

	fn view( &self ) -> Element<Msg>
	{
		column::<Msg>()
			.padding( 16.0 )
			.push( text( format!( "progress = {:.2}", self.tween_progress() ) ) )
			.into()
	}

	fn update( &mut self, msg: Msg )
	{
		match msg
		{
			Msg::StartFade => self.tween_start = Some( Instant::now() ),
			Msg::FadeDone  => self.tween_start = None,
		}
	}

	fn is_animating( &self ) -> bool
	{
		self.tween_start.is_some()
	}

	fn poll_external( &mut self ) -> Vec<Msg>
	{
		// End-of-animation cleanup belongs here per the architecture doc:
		// when the tween completes, clear the start instant so is_animating
		// goes back to false and the runtime sleeps again.
		if let Some( t ) = self.tween_start
		{
			if t.elapsed() >= TWEEN
			{
				self.tween_start = None;
			}
		}
		vec![]
	}

	fn on_swipe_progress( &mut self, progress: f32 )
	{
		self.swipe_progress = progress;
	}

	fn on_swipe_horizontal_progress( &mut self, progress: f32 )
	{
		self.swipe_h = progress;
	}

	fn on_tap( &mut self ) -> Option<Msg>
	{
		self.tap_count += 1;
		None
	}

	fn on_drag_move( &mut self, x: f32, y: f32 )
	{
		self.drag_xy = Some( ( x, y ) );
	}

	fn on_drop( &mut self, x: f32, y: f32 ) -> Option<Msg>
	{
		self.drop_xy = Some( ( x, y ) );
		None
	}

	fn take_focus_request( &mut self ) -> Option<WidgetId>
	{
		self.focus_request.take()
	}

	fn on_text_input_focused( &mut self, active: bool )
	{
		self.ime_active = active;
	}
}

// ── is_animating lifecycle ────────────────────────────────────────────────────

#[ test ]
fn fresh_animator_is_not_animating()
{
	let app = Animator::new();
	assert!( !app.is_animating() );
}

#[ test ]
fn starting_a_tween_flips_is_animating_to_true()
{
	let mut app = Animator::new();
	app.update( Msg::StartFade );
	assert!( app.is_animating() );
}

#[ test ]
fn explicit_fade_done_message_settles_animation()
{
	let mut app = Animator::new();
	app.update( Msg::StartFade );
	app.update( Msg::FadeDone );
	assert!( !app.is_animating() );
}

#[ test ]
fn poll_external_clears_tween_after_duration_elapsed()
{
	let mut app = Animator::new();
	// Set tween_start to a moment far in the past so the elapsed > TWEEN
	// branch in poll_external runs immediately. Avoids `thread::sleep`.
	app.tween_start = Some( Instant::now() - Duration::from_secs( 10 ) );
	assert!( app.is_animating() );

	let drained = app.poll_external();
	assert!( drained.is_empty() );
	assert!( !app.is_animating(), "elapsed > TWEEN must clear the tween via poll_external" );
}

#[ test ]
fn poll_external_keeps_tween_alive_within_duration()
{
	let mut app = Animator::new();
	app.tween_start = Some( Instant::now() );
	let _ = app.poll_external();
	assert!( app.is_animating(), "fresh tween must not be cleared by poll_external" );
}

// ── tween progress reading ────────────────────────────────────────────────────

#[ test ]
fn tween_progress_reads_one_when_no_animation_in_flight()
{
	let app = Animator::new();
	assert_eq!( app.tween_progress(), 1.0 );
}

#[ test ]
fn tween_progress_clamps_to_unit_interval()
{
	let mut app = Animator::new();
	app.tween_start = Some( Instant::now() - Duration::from_secs( 60 ) );
	let p = app.tween_progress();
	assert!( ( 0.0..=1.0 ).contains( &p ), "progress {p} should clamp to [0,1]" );
	assert!( ( p - 1.0 ).abs() < f32::EPSILON );
}

#[ test ]
fn tween_progress_starts_close_to_zero_just_after_start()
{
	let mut app = Animator::new();
	app.tween_start = Some( Instant::now() );
	let p = app.tween_progress();
	assert!( p < 0.1, "progress {p} should be near 0.0 at the start of the tween" );
}

// ── swipe / tap / drag / drop callbacks ───────────────────────────────────────

#[ test ]
fn on_swipe_progress_records_value_per_axis()
{
	let mut app = Animator::new();
	app.on_swipe_progress( 0.7 );
	app.on_swipe_horizontal_progress( -0.4 );
	assert_eq!( app.swipe_progress, 0.7  );
	assert_eq!( app.swipe_h,       -0.4 );
}

#[ test ]
fn on_tap_increments_counter()
{
	let mut app = Animator::new();
	app.on_tap();
	app.on_tap();
	app.on_tap();
	assert_eq!( app.tap_count, 3 );
}

#[ test ]
fn on_drag_move_and_on_drop_record_coordinates()
{
	let mut app = Animator::new();
	app.on_drag_move( 12.0, 34.0 );
	app.on_drop( 56.0, 78.0 );
	assert_eq!( app.drag_xy, Some( ( 12.0, 34.0 ) ) );
	assert_eq!( app.drop_xy, Some( ( 56.0, 78.0 ) ) );
}

// ── focus / IME hooks ─────────────────────────────────────────────────────────

#[ test ]
fn take_focus_request_returns_some_once_then_none()
{
	let mut app = Animator::new();
	app.focus_request = Some( WidgetId( "username" ) );

	let first  = app.take_focus_request();
	let second = app.take_focus_request();

	assert_eq!( first,  Some( WidgetId( "username" ) ) );
	assert_eq!( second, None, "take_focus_request must consume the slot" );
}

#[ test ]
fn on_text_input_focused_records_active_state()
{
	let mut app = Animator::new();
	assert!( !app.ime_active );
	app.on_text_input_focused( true );
	assert!( app.ime_active );
	app.on_text_input_focused( false );
	assert!( !app.ime_active );
}

// ── swipe_threshold / long_press_duration defaults ────────────────────────────

#[ test ]
fn swipe_and_long_press_defaults_match_documented_constants()
{
	let app = Animator::new();
	// `0.6` (60 % of surface height for a vertical swipe) and 500 ms are
	// the documented defaults — apps override only when they need a hair
	// trigger or a longer press.
	assert!( ( app.swipe_threshold() - 0.6 ).abs() < 1e-6 );
	assert_eq!( app.long_press_duration(), Duration::from_millis( 500 ) );
}

// ── animation + UiSurface interplay ───────────────────────────────────────────

#[ test ]
fn animator_renders_inside_uisurface_without_wayland_loop()
{
	// Prove that an App with an active animation can drive UiSurface.render
	// directly — important for compositor embedders that don't run
	// `ltk::run` but still want the same animation contract.
	let mut surface = UiSurface::<Msg>::new( 320, 80 );
	let mut app     = Animator::new();
	app.update( Msg::StartFade );
	assert!( app.is_animating() );

	let opts = RenderOptions::full_canvas( 320, 80 ).background( Color::rgb( 0.0, 0.0, 0.0 ) );
	let _ = surface.render( &app.view(), opts );
	// Snapshot the text widget — it isn't focusable / interactive, so the
	// surface's widget_rects should be empty.
	assert!( surface.widget_rects().is_empty() );

	// Pretend a frame went by; force re-layout so the new tween_progress
	// percolates into the view.
	surface.mark_content_dirty();
	let _ = surface.render( &app.view(), opts );

	// Animation eventually settles via poll_external.
	app.tween_start = Some( Instant::now() - Duration::from_secs( 5 ) );
	let _ = app.poll_external();
	assert!( !app.is_animating() );
}

#[ test ]
fn handlers_match_default_for_text_only_animator_view()
{
	// Defensive: the animator's view is text-only, so widget_rects is empty
	// and any handlers lookup returns None. Regression guard against the
	// runtime accidentally treating animation-driven views as interactive.
	let mut surface = UiSurface::<Msg>::new( 320, 80 );
	let app         = Animator::new();
	let _ = surface.render(
		&app.view(),
		RenderOptions::full_canvas( 320, 80 ),
	);
	assert!( surface.widget_rects().is_empty() );
	assert!( surface.handlers( 0 ).is_none() );
	let _: Option<&WidgetHandlers<Msg>> = surface.handlers( 999 );
}
