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

// End-to-end coverage for the runtime contract: `Msg → App::update → next
// view → render`. The Wayland event loop in `ltk::run` is the integration
// point that ties widget-level handler snapshots, focus traversal and keysym
// dispatch together; these tests exercise the same wiring against `UiSurface`
// (the runtime-free embedding of that loop).

use ltk::core::{ RenderOptions, UiSurface };
use ltk::test_support::next_focusable_index;
use ltk::{
	button, column, text, App, Color, Element, Keysym,
};

// ── A small counter app ───────────────────────────────────────────────────────

#[ derive( Clone, Debug, PartialEq, Eq ) ]
enum Msg
{
	Inc,
	Dec,
	Reset,
	Quit,
}

struct Counter
{
	value:   i32,
	pending: Vec<Msg>,
	quit:    bool,
}

impl Counter
{
	fn new() -> Self
	{
		Self { value: 0, pending: vec![], quit: false }
	}
}

impl App for Counter
{
	type Message = Msg;

	fn view( &self ) -> Element<Msg>
	{
		column::<Msg>()
			.padding( 16.0 )
			.spacing( 8.0 )
			.push( text( format!( "{}", self.value ) ) )
			.push( button( "+" ).on_press( Msg::Inc ) )
			.push( button( "−" ).on_press( Msg::Dec ) )
			.push( button( "reset" ).on_press( Msg::Reset ) )
			.into()
	}

	fn update( &mut self, msg: Msg )
	{
		match msg
		{
			Msg::Inc   => self.value += 1,
			Msg::Dec   => self.value -= 1,
			Msg::Reset => self.value  = 0,
			Msg::Quit  => self.quit   = true,
		}
	}

	fn poll_external( &mut self ) -> Vec<Msg>
	{
		std::mem::take( &mut self.pending )
	}

	fn on_key( &mut self, keysym: Keysym ) -> Option<Msg>
	{
		match keysym
		{
			Keysym::Escape => Some( Msg::Quit ),
			_              => None,
		}
	}
}

fn render( surface: &mut UiSurface<Msg>, app: &Counter ) -> ltk::core::RenderOutput
{
	surface.render(
		&app.view(),
		RenderOptions::full_canvas( 320, 240 ).background( Color::rgb( 0.1, 0.1, 0.1 ) ),
	)
}

// ── Msg → update → re-render ──────────────────────────────────────────────────

#[ test ]
fn pressing_increment_button_advances_counter_state()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let mut app     = Counter::new();
	let _ = render( &mut surface, &app );

	// Locate the "+" button. Layout pushes the text widget first (non-
	// interactive), then the three buttons in declaration order.
	let plus_idx = surface.widget_rects()[ 0 ].flat_idx;
	let msg = surface.handlers( plus_idx )
		.and_then( |h| h.press_msg() )
		.expect( "button must carry on_press" );
	assert_eq!( msg, Msg::Inc );

	app.update( msg );
	assert_eq!( app.value, 1 );

	let _ = render( &mut surface, &app );
	// Three buttons remain laid out — view shape did not change.
	assert_eq!( surface.widget_rects().len(), 3 );
}

#[ test ]
fn multiple_dispatch_cycles_accumulate_state()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let mut app     = Counter::new();

	for _ in 0..5
	{
		let _ = render( &mut surface, &app );
		let inc_idx = surface.widget_rects()[ 0 ].flat_idx;
		let msg = surface.handlers( inc_idx ).and_then( |h| h.press_msg() ).unwrap();
		app.update( msg );
	}
	assert_eq!( app.value, 5 );
}

#[ test ]
fn dec_then_reset_returns_state_to_zero()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let mut app     = Counter::new();
	let _ = render( &mut surface, &app );

	// Buttons appear in declaration order: 0 = "+", 1 = "−", 2 = "reset".
	let dec_idx   = surface.widget_rects()[ 1 ].flat_idx;
	let reset_idx = surface.widget_rects()[ 2 ].flat_idx;

	let dec_msg = surface.handlers( dec_idx ).and_then( |h| h.press_msg() ).unwrap();
	app.update( dec_msg );
	assert_eq!( app.value, -1 );

	let _ = render( &mut surface, &app );
	let reset_msg = surface.handlers( reset_idx ).and_then( |h| h.press_msg() ).unwrap();
	app.update( reset_msg );
	assert_eq!( app.value, 0 );
}

#[ test ]
fn re_render_after_state_change_preserves_widget_count()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let mut app     = Counter::new();

	let first = render( &mut surface, &app );
	app.update( Msg::Inc );
	// A real runtime would call `mark_content_dirty` here so the next render
	// repaints the value text; UiSurface leaves that to the embedder. We
	// just assert that the layout shape is stable across the message
	// dispatch.
	let _second = render( &mut surface, &app );

	assert!( first.full_redraw, "first render is always a full redraw" );
	assert_eq!( surface.widget_rects().len(), 3 );
}

// ── on_key / poll_external ────────────────────────────────────────────────────

#[ test ]
fn on_key_escape_emits_quit_message()
{
	let mut app = Counter::new();
	let msg = app.on_key( Keysym::Escape );
	assert_eq!( msg, Some( Msg::Quit ) );
}

#[ test ]
fn on_key_unknown_keysym_returns_none()
{
	let mut app = Counter::new();
	assert!( app.on_key( Keysym::Tab ).is_none() );
}

#[ test ]
fn poll_external_drains_pending_messages_in_order()
{
	let mut app = Counter::new();
	app.pending.extend( [ Msg::Inc, Msg::Inc, Msg::Reset ] );

	let drained = app.poll_external();
	assert_eq!( drained, vec![ Msg::Inc, Msg::Inc, Msg::Reset ] );

	// Consuming the queue empties it.
	let again = app.poll_external();
	assert!( again.is_empty() );
}

#[ test ]
fn updating_with_drained_messages_reflects_on_render()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let mut app     = Counter::new();
	app.pending.extend( [ Msg::Inc, Msg::Inc, Msg::Inc ] );

	for msg in app.poll_external()
	{
		app.update( msg );
	}
	assert_eq!( app.value, 3 );
	let _ = render( &mut surface, &app );
}

#[ test ]
fn defaults_for_unset_app_hooks_are_inert()
{
	let app = Counter::new();
	// is_animating defaults to false — confirms the runtime sleeps on idle.
	assert!( !app.is_animating() );
	// poll_interval defaults to None — pure event-driven scheduling.
	assert!( app.poll_interval().is_none() );
}

// ── Tab navigation through UiSurface widget rects ─────────────────────────────

#[ test ]
fn tab_navigation_advances_through_focusable_widgets()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let app         = Counter::new();
	let _ = render( &mut surface, &app );

	let widgets = surface.widget_rects();
	assert_eq!( widgets.len(), 3 );

	// Forward from None lands on the first focusable (button "+").
	let first  = next_focusable_index( widgets, None, false ).unwrap();
	let second = next_focusable_index( widgets, Some( first ),  false ).unwrap();
	let third  = next_focusable_index( widgets, Some( second ), false ).unwrap();
	let wrap   = next_focusable_index( widgets, Some( third ),  false ).unwrap();

	assert_ne!( first,  second );
	assert_ne!( second, third );
	assert_eq!( wrap,   first, "focus wraps to the head after the tail" );
}

#[ test ]
fn tab_navigation_reverse_walks_backward()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let app         = Counter::new();
	let _ = render( &mut surface, &app );

	let widgets = surface.widget_rects();
	let last  = next_focusable_index( widgets, None, true ).unwrap();
	let prev  = next_focusable_index( widgets, Some( last ), true ).unwrap();
	let wrap  = next_focusable_index( widgets, Some( prev ), true ).unwrap();
	let wrap2 = next_focusable_index( widgets, Some( wrap ), true ).unwrap();

	assert_ne!( last, prev );
	assert_ne!( prev, wrap );
	assert_eq!( wrap2, last, "reverse traversal also wraps" );
}

// ── External invalidation ─────────────────────────────────────────────────────

#[ test ]
fn mark_content_dirty_triggers_full_redraw_after_external_state_mutation()
{
	let mut surface = UiSurface::<Msg>::new( 320, 240 );
	let mut app     = Counter::new();
	let _ = render( &mut surface, &app );

	// Application state mutates via a path not derived from a Msg dispatch
	// (e.g. a clock tick stored in a RefCell during view()). The runtime
	// signals "content changed without interaction transition" via
	// `mark_content_dirty`; the next render must come back as full redraw.
	app.value = 42;
	surface.mark_content_dirty();
	let out = render( &mut surface, &app );
	assert!( out.full_redraw );
}
