// Pixel-level "golden-like" coverage. We deliberately avoid storing reference
// PNG bytes: their value depends on fontdue / tiny-skia / theme versions, so a
// byte-exact reference would break on every dependency bump. Instead these
// tests assert *structural* properties of the rendered buffer (background
// colour, transparency, determinism, focus-state divergence) plus a few
// canonical scenes that exercise the partial-redraw and full-redraw paths.

use ltk::core::{ Canvas, RenderOptions, UiSurface };
use ltk::{ button, column, container, spacer, text, Color, Element };

// ── helpers ───────────────────────────────────────────────────────────────────

fn extract_pixels( surface: &UiSurface<()>, w: u32, h: u32 ) -> Vec<u8>
{
	let mut buf = vec![ 0u8; ( w * h * 4 ) as usize ];
	// Software canvas only — `Canvas::Gles::write_to_wayland_buf` is a no-op
	// (presentation goes through `eglSwapBuffers` elsewhere). UiSurface::new
	// returns a software surface.
	assert!( matches!( surface.canvas(), Canvas::Software( _ ) ) );
	surface.canvas().write_to_wayland_buf( &mut buf, false );
	buf
}

fn fnv1a_64( bytes: &[u8] ) -> u64
{
	let mut h: u64 = 0xcbf29ce484222325;
	for &b in bytes
	{
		h ^= b as u64;
		h  = h.wrapping_mul( 0x100000001b3 );
	}
	h
}

// Approximate equality on a single u8 channel — accounts for AA / rounding
// drift on the boundary of background-vs-content fills.
fn near( a: u8, b: u8, tol: u8 ) -> bool
{
	a.abs_diff( b ) <= tol
}

// ── background colour ─────────────────────────────────────────────────────────

#[ test ]
fn fully_transparent_background_yields_all_zero_alpha()
{
	let mut surface = UiSurface::<()>::new( 64, 64 );
	let view: Element<()> = column().into();
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 64, 64 ).background( Color::TRANSPARENT ),
	);
	let buf = extract_pixels( &surface, 64, 64 );
	for chunk in buf.chunks_exact( 4 )
	{
		// Every pixel ends up with alpha = 0; RGB after premultiplication is
		// also 0 because the background fill is the only paint.
		assert_eq!( chunk[ 3 ], 0, "transparent bg must produce zero alpha" );
	}
}

#[ test ]
fn opaque_background_paints_canonical_rgb_in_corner_pixel()
{
	let mut surface = UiSurface::<()>::new( 64, 64 );
	let view: Element<()> = column().into();
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 64, 64 ).background( Color::rgb( 1.0, 0.25, 0.0 ) ),
	);
	let buf = extract_pixels( &surface, 64, 64 );

	// Top-left pixel: nothing draws there beyond the background fill, so the
	// channels should match (255, ~64, 0, 255) with mild rounding tolerance.
	assert!( near( buf[ 0 ], 255, 1 ) );
	assert!( near( buf[ 1 ],  64, 2 ) );
	assert!( near( buf[ 2 ],   0, 1 ) );
	assert_eq!( buf[ 3 ], 255 );
}

#[ test ]
fn background_fills_every_pixel_when_view_is_empty()
{
	let mut surface = UiSurface::<()>::new( 32, 32 );
	let view: Element<()> = column().into();
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 32, 32 ).background( Color::rgb( 0.0, 0.5, 0.0 ) ),
	);
	let buf = extract_pixels( &surface, 32, 32 );
	for chunk in buf.chunks_exact( 4 )
	{
		assert!( near( chunk[ 0 ], 0,   1 ) );
		assert!( near( chunk[ 1 ], 128, 2 ) );
		assert!( near( chunk[ 2 ], 0,   1 ) );
		assert_eq!( chunk[ 3 ], 255 );
	}
}

// ── determinism ───────────────────────────────────────────────────────────────

fn counter_view( count: u32 ) -> Element<()>
{
	column::<()>()
		.padding( 16.0 )
		.spacing( 8.0 )
		.push( text( format!( "Count: {count}" ) ) )
		.push( button( "Increment" ) )
		.into()
}

#[ test ]
fn rendering_the_same_view_twice_produces_identical_pixel_buffers()
{
	let mut surface = UiSurface::<()>::new( 240, 120 );
	let opts = RenderOptions::full_canvas( 240, 120 ).background( Color::rgb( 0.05, 0.05, 0.05 ) );

	let _ = surface.render( &counter_view( 0 ), opts );
	let buf_a = extract_pixels( &surface, 240, 120 );

	// Force a clean re-paint via mark_content_dirty so both passes hit the
	// same code path (full redraw rather than partial-damage clip).
	surface.mark_content_dirty();
	let _ = surface.render( &counter_view( 0 ), opts );
	let buf_b = extract_pixels( &surface, 240, 120 );

	assert_eq!( buf_a, buf_b, "two identical renders must produce identical pixels" );
}

#[ test ]
fn render_hash_is_stable_across_two_back_to_back_passes()
{
	let mut surface = UiSurface::<()>::new( 320, 200 );
	let opts = RenderOptions::full_canvas( 320, 200 ).background( Color::rgb( 0.1, 0.1, 0.1 ) );

	let _ = surface.render( &counter_view( 7 ), opts );
	let h1 = fnv1a_64( &extract_pixels( &surface, 320, 200 ) );

	surface.mark_content_dirty();
	let _ = surface.render( &counter_view( 7 ), opts );
	let h2 = fnv1a_64( &extract_pixels( &surface, 320, 200 ) );

	assert_eq!( h1, h2, "a deterministic render pass must produce a stable hash" );
}

// ── content presence ──────────────────────────────────────────────────────────

#[ test ]
fn rendering_a_button_produces_pixels_that_differ_from_the_background()
{
	// Render on a WHITE canvas, not black: with no theme dir configured the
	// active theme is the embedded B/W fallback, whose Light mode defines
	// `accent` (the default button fill) and `text-primary` as pure BLACK on
	// a white page. A black canvas would therefore hide a fallback button
	// entirely (black-on-black) and this structural "button paints content"
	// check would spuriously fail. White matches the fallback's page colour,
	// so the black button stands out — and a real theme's saturated accent
	// (e.g. teal) differs from white too, keeping the test theme-robust.
	let mut surface = UiSurface::<()>::new( 240, 120 );
	let bg = Color::WHITE;
	let view: Element<()> = column::<()>().push( button( "tap" ) ).into();
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 240, 120 ).background( bg ),
	);
	let buf = extract_pixels( &surface, 240, 120 );

	let differs = buf.chunks_exact( 4 ).any( |chunk|
		!( chunk[ 0 ] == 255 && chunk[ 1 ] == 255 && chunk[ 2 ] == 255 && chunk[ 3 ] == 255 )
	);
	assert!( differs, "a button must paint at least one non-background pixel" );
}

#[ test ]
fn rendering_text_produces_pixels_that_differ_from_the_background()
{
	let mut surface = UiSurface::<()>::new( 240, 120 );
	let bg = Color::rgb( 0.0, 0.0, 0.0 );
	let view: Element<()> = column::<()>().push( text( "ltk" ).size( 32.0 ) ).into();
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 240, 120 ).background( bg ),
	);
	let buf = extract_pixels( &surface, 240, 120 );
	let glyph_pixels = buf.chunks_exact( 4 )
		.filter( |chunk| !( chunk[ 0 ] == 0 && chunk[ 1 ] == 0 && chunk[ 2 ] == 0 ) )
		.count();
	assert!( glyph_pixels > 0, "text rasterisation must produce non-bg pixels" );
}

#[ test ]
fn container_with_explicit_color_fill_paints_inside_padding()
{
	// Fill a sized container with a known colour, render on a black
	// background, and confirm a measurable region of red pixels appears.
	let mut surface = UiSurface::<()>::new( 100, 100 );
	let view: Element<()> = column::<()>()
		.padding( 0.0 )
		.push(
			container::<()>( spacer().width( 40.0 ).height( 40.0 ) )
				.background( Color::rgb( 1.0, 0.0, 0.0 ) ),
		)
		.into();
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 100, 100 ).background( Color::BLACK ),
	);
	let bytes = extract_pixels( &surface, 100, 100 );

	let red_pixels = bytes.chunks_exact( 4 )
		.filter( |c| c[ 0 ] > 200 && c[ 1 ] < 30 && c[ 2 ] < 30 && c[ 3 ] == 255 )
		.count();
	// A 40×40 fill produces 1600 fully-saturated red pixels; allow a wide
	// floor to absorb anti-aliased edge pixels that mix with the background.
	assert!(
		red_pixels > 500,
		"container with red fill must produce a substantial red region (got {red_pixels})",
	);
}

// ── focus-state divergence ────────────────────────────────────────────────────

#[ test ]
fn focusing_a_button_changes_pixels_inside_its_paint_rect()
{
	// The focus ring is one of the cheapest ways to verify the partial-
	// redraw path actually paints something different. Render with no focus,
	// snapshot the pixels, set focus, render again, and confirm the buffer
	// has changed somewhere.
	let mut surface = UiSurface::<()>::new( 200, 80 );
	let opts = RenderOptions::full_canvas( 200, 80 ).background( Color::rgb( 0.05, 0.05, 0.05 ) );
	let view: Element<()> = column::<()>().push( button( "tap" ) ).into();

	let _ = surface.render( &view, opts );
	let unfocused = extract_pixels( &surface, 200, 80 );

	let idx = surface.widget_rects()[ 0 ].flat_idx;
	surface.set_focused( Some( idx ) );
	let _ = surface.render( &view, opts );
	let focused = extract_pixels( &surface, 200, 80 );

	assert_ne!(
		unfocused, focused,
		"focus state change must produce visibly different pixels",
	);
}

// ── invariants ────────────────────────────────────────────────────────────────

#[ test ]
fn pixel_buffer_length_matches_canvas_size()
{
	let surface = UiSurface::<()>::new( 100, 50 );
	let mut buf = vec![ 0u8; 100 * 50 * 4 ];
	surface.canvas().write_to_wayland_buf( &mut buf, false );
	// All pixels are zero before render — confirms the buffer is correctly
	// sized and write_to_wayland_buf doesn't overwrite past the end.
	assert!( buf.iter().all( |&b| b == 0 ) );
}

#[ test ]
fn resize_changes_the_required_buffer_length()
{
	let mut surface = UiSurface::<()>::new( 64, 64 );
	let view: Element<()> = column().into();
	let opts = RenderOptions::full_canvas( 64, 64 );
	let _ = surface.render( &view, opts );

	surface.resize( 128, 96 );
	assert_eq!( surface.size(), ( 128, 96 ) );

	// Old buffer would be too short; the new size dictates a 49152-byte
	// buffer. Confirm `write_to_wayland_buf` accepts and fills it without
	// panicking.
	let mut new_buf = vec![ 0u8; 128 * 96 * 4 ];
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 128, 96 ).background( Color::rgb( 0.0, 0.0, 0.5 ) ),
	);
	surface.canvas().write_to_wayland_buf( &mut new_buf, false );

	// Last pixel should carry the navy bg.
	let last = &new_buf[ new_buf.len() - 4 .. ];
	assert!( near( last[ 2 ], 128, 2 ), "last pixel blue channel ≈ 128" );
	assert_eq!( last[ 3 ], 255 );
}

// ── canonical scene byte equality ─────────────────────────────────────────────

#[ test ]
fn canonical_solid_red_scene_has_exact_byte_layout()
{
	// A scene with NO text (so no font-version drift) and a colour with NO
	// rounding ambiguity (1.0 → 255, 0.0 → 0). The buffer is portable
	// across machines because the path is plain rectangle clear → tiny-skia
	// premul → byte copy, and the values land exactly. If this assertion
	// drifts, either the renderer or the buffer format changed; investigate
	// before regenerating.
	let mut surface = UiSurface::<()>::new( 16, 16 );
	let view: Element<()> = column().into();
	let _ = surface.render(
		&view,
		RenderOptions::full_canvas( 16, 16 ).background( Color::rgb( 1.0, 0.0, 0.0 ) ),
	);
	let actual = extract_pixels( &surface, 16, 16 );
	let expected: Vec<u8> = ( 0..16 * 16 ).flat_map( |_| [ 255u8, 0, 0, 255 ] ).collect();
	assert_eq!( actual, expected, "16×16 solid red must serialise to exact RGBA bytes" );
}

#[ test ]
fn fnv1a_hash_changes_when_a_single_byte_flips()
{
	// Sanity check on the in-test hash function itself — guards the rest of
	// the determinism tests against a regression that breaks the hash but
	// leaves the assertions trivially passing.
	let a = [ 0u8, 0, 0, 0 ];
	let mut b = a;
	b[ 2 ] = 1;
	assert_ne!( fnv1a_64( &a ), fnv1a_64( &b ) );
}
