// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use super::TextEdit;

#[ derive( Clone, Debug, PartialEq, Eq ) ]
enum Msg
{
	Changed( String ),
	Submitted,
}

fn new_te( value: &str ) -> TextEdit<Msg>
{
	TextEdit::new( "placeholder".into(), value.into() )
}

// ── Construction ──────────────────────────────────────────────────────────

#[ test ]
fn new_places_cursor_at_end_of_initial_value()
{
	let t = new_te( "hello" );
	assert_eq!( t.cursor_pos, 5 );
	assert_eq!( t.value, "hello" );
}

#[ test ]
fn new_with_empty_value_starts_with_zero_cursor()
{
	let t = new_te( "" );
	assert_eq!( t.cursor_pos, 0 );
	assert!( t.value.is_empty() );
}

#[ test ]
fn new_with_multibyte_value_uses_byte_length_for_cursor()
{
	// "café" is 5 bytes (c-a-f-é where é is 2 bytes in UTF-8).
	let t = new_te( "café" );
	assert_eq!( t.cursor_pos, 5 );
	assert_eq!( t.value.len(), 5 );
}

// ── insert_str ────────────────────────────────────────────────────────────

#[ test ]
fn insert_str_at_end_appends_and_advances_cursor()
{
	let mut t = new_te( "ab" );
	assert_eq!( t.cursor_pos, 2 );
	let _ = t.insert_str( "c" );
	assert_eq!( t.value, "abc" );
	assert_eq!( t.cursor_pos, 3 );
}

#[ test ]
fn insert_str_at_middle_inserts_and_advances_cursor_by_byte_len()
{
	let mut t = new_te( "ab" );
	t.cursor_pos = 1;
	let _ = t.insert_str( "X" );
	assert_eq!( t.value, "aXb" );
	assert_eq!( t.cursor_pos, 2 );
}

#[ test ]
fn insert_str_multibyte_advances_cursor_by_utf8_byte_count()
{
	let mut t = new_te( "" );
	let _ = t.insert_str( "é" );  // 2 bytes
	assert_eq!( t.value, "é" );
	assert_eq!( t.cursor_pos, 2 );
	let _ = t.insert_str( "🦀" );  // 4 bytes
	assert_eq!( t.cursor_pos, 6 );
}

#[ test ]
fn insert_str_clamps_runaway_cursor()
{
	// If cursor_pos is somehow past value.len() (defensive), insert should
	// behave as "insert at end" rather than panic.
	let mut t = new_te( "ab" );
	t.cursor_pos = 99;
	let _ = t.insert_str( "c" );
	assert_eq!( t.value, "abc" );
	// Cursor is clamped to new value length.
	assert_eq!( t.cursor_pos, 3 );
}

#[ test ]
fn insert_str_returns_on_change_with_new_value()
{
	let mut t = new_te( "ab" )
		.on_change( |s| Msg::Changed( s ) );
	let msg = t.insert_str( "c" );
	assert_eq!( msg, Some( Msg::Changed( "abc".into() ) ) );
}

#[ test ]
fn insert_str_without_on_change_returns_none()
{
	let mut t = new_te( "ab" );
	assert_eq!( t.insert_str( "c" ), None );
}

// ── backspace ─────────────────────────────────────────────────────────────

#[ test ]
fn backspace_at_zero_cursor_is_noop()
{
	let mut t = new_te( "abc" );
	t.cursor_pos = 0;
	assert_eq!( t.backspace(), None );
	assert_eq!( t.value, "abc" );
	assert_eq!( t.cursor_pos, 0 );
}

#[ test ]
fn backspace_at_end_removes_last_char()
{
	let mut t = new_te( "abc" );
	let _ = t.backspace();
	assert_eq!( t.value, "ab" );
	assert_eq!( t.cursor_pos, 2 );
}

#[ test ]
fn backspace_in_middle_removes_char_before_cursor()
{
	let mut t = new_te( "abc" );
	t.cursor_pos = 2;
	let _ = t.backspace();
	assert_eq!( t.value, "ac" );
	assert_eq!( t.cursor_pos, 1 );
}

#[ test ]
fn backspace_handles_multibyte_acute()
{
	// "é" is two bytes; backspacing must remove the whole codepoint.
	let mut t = new_te( "café" );
	let _ = t.backspace();
	assert_eq!( t.value, "caf" );
	assert_eq!( t.cursor_pos, 3 );
}

#[ test ]
fn backspace_handles_emoji_codepoint()
{
	// Crab emoji is 4 bytes in UTF-8; cursor must back up by 4.
	let mut t = new_te( "x🦀" );
	assert_eq!( t.cursor_pos, 5 );
	let _ = t.backspace();
	assert_eq!( t.value, "x" );
	assert_eq!( t.cursor_pos, 1 );
}

#[ test ]
fn backspace_returns_on_change_with_new_value()
{
	let mut t = new_te( "ab" )
		.on_change( |s| Msg::Changed( s ) );
	let msg = t.backspace();
	assert_eq!( msg, Some( Msg::Changed( "a".into() ) ) );
}

// ── display_text + secure ─────────────────────────────────────────────────

#[ test ]
fn display_text_plain_returns_value_verbatim()
{
	let t = new_te( "hello" );
	assert_eq!( t.display_text(), "hello" );
}

#[ test ]
fn display_text_secure_returns_bullets_one_per_codepoint()
{
	// One bullet per codepoint, NOT per byte. "café" has 4 codepoints,
	// so the masked text must be 4 bullets — not 5 (the byte length).
	let t = new_te( "café" ).secure( true );
	let masked = t.display_text();
	assert_eq!( masked.chars().count(), 4 );
	assert!( masked.chars().all( |c| c == '\u{2022}' ) );
}

#[ test ]
fn display_text_secure_with_emoji_one_bullet_per_codepoint()
{
	// Crab emoji is one codepoint — exactly one bullet, even though it
	// occupies four bytes in UTF-8.
	let t = new_te( "🦀" ).secure( true );
	assert_eq!( t.display_text().chars().count(), 1 );
}

#[ test ]
fn secure_mode_does_not_touch_underlying_value()
{
	let t = new_te( "secret" ).secure( true );
	assert_eq!( t.value, "secret" );
}

#[ test ]
fn secure_toggles_via_builder()
{
	let t = new_te( "x" ).secure( true ).secure( false );
	assert_eq!( t.display_text(), "x" );
}

// ── secure-mode credential zeroize ────────────────────────────────────────

#[ test ]
fn secure_flag_propagates_to_widget_handler_snapshot()
{
	use crate::widget::{ Element, WidgetHandlers };

	// The runtime takes a `WidgetHandlers` snapshot of every interactive
	// widget once per frame. The `secure` flag must travel with the
	// snapshot so the handler's own `Drop` (which mirrors `TextEdit`'s
	// wipe-on-drop) knows whether to scrub on release.
	let widget: Element<()> = TextEdit::new( "p".into(), "secret".into() )
		.secure( true )
		.into_element();
	let h = widget.handlers();
	match h
	{
		WidgetHandlers::TextEdit { secure, .. } => assert!( secure, "secure flag must propagate" ),
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn multiline_flag_propagates_to_widget_handler_snapshot()
{
	use crate::widget::{ Element, WidgetHandlers };

	let widget: Element<()> = TextEdit::new( "p".into(), "x".into() )
		.multiline( true )
		.into_element();
	match widget.handlers()
	{
		WidgetHandlers::TextEdit { multiline, .. } => assert!( multiline, "multiline flag must propagate" ),
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn secure_overrides_multiline_in_widget_handlers()
{
	use crate::widget::{ Element, WidgetHandlers };

	// `is_multiline()` requires `!secure` — a secure password field
	// must remain single-line even if the caller also asked for
	// multiline. The handler snapshot reads `is_multiline()`, so
	// `multiline` here must be `false`.
	let widget: Element<()> = TextEdit::new( "p".into(), "x".into() )
		.multiline( true )
		.secure( true )
		.into_element();
	match widget.handlers()
	{
		WidgetHandlers::TextEdit { multiline, secure, .. } =>
		{
			assert!( secure, "secure must propagate" );
			assert!( !multiline, "secure must override multiline" );
		}
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn non_secure_flag_propagates_to_widget_handler_snapshot()
{
	use crate::widget::{ Element, WidgetHandlers };

	let widget: Element<()> = TextEdit::new( "p".into(), "x".into() )
		.into_element();
	match widget.handlers()
	{
		WidgetHandlers::TextEdit { secure, .. } => assert!( !secure, "default is non-secure" ),
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn secure_drop_zeros_underlying_string_buffer()
{
	// Reach into the value's byte buffer right before the drop, run the
	// drop logic by replacing the binding, and confirm the bytes that
	// had been the credential are now zero. We cannot inspect the
	// allocation after the actual drop (UB), so this test exercises the
	// same path that `Drop::drop` runs internally.
	let mut t: TextEdit<()> = TextEdit::new( "p".into(), "secret".into() ).secure( true );
	assert_eq!( t.value, "secret" );

	// Manually run the wipe the way Drop will; the next assertion checks
	// the buffer is zeroed before the auto-drop releases it.
	// SAFETY: same as the production wipe path above — overwriting
	// every byte with zero leaves the String holding a sequence of NUL
	// codepoints, which is valid UTF-8.
	let bytes = unsafe { t.value.as_mut_vec() };
	crate::secure_mem::secure_zero( bytes );
	assert!( bytes.iter().all( |&b| b == 0 ) );
	assert_eq!( bytes.len(), 6 );
}

#[ test ]
fn non_secure_text_edit_drop_is_a_noop_for_value()
{
	// A non-secure widget must not pay the wipe cost. We cannot observe
	// the absence of writes directly, but we can confirm that the
	// underlying bytes still match the original content right up to
	// the moment of drop.
	let t: TextEdit<()> = TextEdit::new( "p".into(), "value".into() );
	assert_eq!( t.value.as_bytes(), b"value" );
	// Drop runs at end of scope without touching the bytes.
}

// ── on_submit / on_change wiring ──────────────────────────────────────────

#[ test ]
fn on_submit_stores_message_unchanged()
{
	let t = new_te( "" ).on_submit( Msg::Submitted );
	assert_eq!( t.on_submit, Some( Msg::Submitted ) );
}

// ── full edit roundtrip ───────────────────────────────────────────────────

#[ test ]
fn type_word_then_clear_with_backspace()
{
	let mut t = new_te( "" );
	for ch in "hola".chars()
	{
		let _ = t.insert_str( &ch.to_string() );
	}
	assert_eq!( t.value, "hola" );
	assert_eq!( t.cursor_pos, 4 );
	while t.cursor_pos > 0
	{
		let _ = t.backspace();
	}
	assert!( t.value.is_empty() );
	assert_eq!( t.cursor_pos, 0 );
}

// ── multiline mode ───────────────────────────────────────────────────────

#[ test ]
fn multiline_default_off()
{
	let t = new_te( "" );
	assert!( !t.multiline );
	assert!( !t.is_multiline() );
}

#[ test ]
fn multiline_builder_enables()
{
	let t = new_te( "" ).multiline( true );
	assert!( t.multiline );
	assert!( t.is_multiline() );
}

#[ test ]
fn multiline_grows_preferred_height_with_rows()
{
	use crate::render::Canvas;
	let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
	let canvas = Canvas::new( 800, 600 );
	let single = new_te( "" );
	let ( _, h_single ) = single.preferred_size( 200.0, &canvas );
	// The single-line default routes through the widget-scaling mode.
	assert_eq!( h_single, canvas.geom_px( super::theme::HEIGHT ) );

	let multi = new_te( "" ).multiline( true ).rows( 5 );
	let ( _, h_multi ) = multi.preferred_size( 200.0, &canvas );
	assert!( h_multi > h_single, "multiline must claim more vertical space" );

	let bigger = new_te( "" ).multiline( true ).rows( 10 );
	let ( _, h_bigger ) = bigger.preferred_size( 200.0, &canvas );
	assert!( h_bigger > h_multi, "more rows → taller box" );
}

#[ test ]
fn height_builder_scales_single_line_box_with_surface()
{
	use crate::render::Canvas;
	use crate::types::Length;
	let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
	// An explicit height bypasses the mode: vmin on a 400×800 surface →
	// 10 % of the smaller side (400) = 40 px, resolved in physical space.
	let t      = new_te( "" ).height( Length::vmin( 10.0 ) );
	let canvas = Canvas::new( 400, 800 );
	assert_eq!( t.preferred_size( 1000.0, &canvas ).1, 40.0 );

	// The default single-line height follows the widget-scaling mode.
	let d = new_te( "" );
	assert_eq!( d.preferred_size( 1000.0, &canvas ).1, canvas.geom_px( super::theme::HEIGHT ) );
}

#[ test ]
fn rows_clamps_zero_to_one()
{
	let t = new_te( "" ).multiline( true ).rows( 0 );
	assert_eq!( t.rows, 1 );
}

#[ test ]
fn secure_overrides_multiline_in_is_multiline_query()
{
	// A `multiline + secure` configuration must remain single-line:
	// passwords carrying a literal `\n` would defeat the password
	// mode rendering and turn the credential boundary fuzzy.
	let t: TextEdit<()> = TextEdit::new( "p".into(), "x".into() )
		.multiline( true )
		.secure( true );
	assert!( t.multiline );  // raw flag preserved
	assert!( !t.is_multiline() );  // effective mode is single-line
}

#[ test ]
fn insert_str_can_carry_newline_in_multiline_mode()
{
	// The widget itself does not gate `\n` — that decision lives at
	// dispatch time. We just confirm `\n` round-trips through
	// insert_str + the value buffer untouched.
	let mut t = new_te( "ab" ).multiline( true );
	let _ = t.insert_str( "\n" );
	assert_eq!( t.value, "ab\n" );
	assert_eq!( t.cursor_pos, 3 );
	let _ = t.insert_str( "c" );
	assert_eq!( t.value, "ab\nc" );
}

// ── builders for the inline / picker variants ────────────────────────────

#[ test ]
fn defaults_for_new_inline_builders()
{
	let t: TextEdit<()> = TextEdit::new( "p".into(), "v".into() );
	assert_eq!( t.align, super::super::text::TextAlign::Left );
	assert!( !t.borderless );
	assert!( t.fixed_width.is_none() );
	assert_eq!( t.font_size, None ); // follows the widget-scaling mode
	assert!( !t.select_on_focus );
}

#[ test ]
fn align_builder_sets_alignment()
{
	use super::super::text::TextAlign;
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).align( TextAlign::Center );
	assert_eq!( t.align, TextAlign::Center );
	let t = t.align( TextAlign::Right );
	assert_eq!( t.align, TextAlign::Right );
}

#[ test ]
fn borderless_builder_toggles_flag()
{
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).borderless( true );
	assert!( t.borderless );
}

#[ test ]
fn fixed_width_builder_stores_value()
{
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).fixed_width( 72.0 );
	assert_eq!( t.fixed_width, Some( crate::types::Length::px( 72.0 ) ) );
}

#[ test ]
fn font_size_builder_stores_length()
{
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).font_size( 28.0 );
	assert_eq!( t.font_size, Some( crate::types::Length::px( 28.0 ) ) );
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).font_size( crate::types::Length::vmin( 4.5 ) );
	assert_eq!( t.font_size, Some( crate::types::Length::vmin( 4.5 ) ) );
}

#[ test ]
fn font_size_fluid_follows_the_mode_at_the_design_px()
{
	use crate::render::Canvas;
	let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
	crate::set_widget_scaling( crate::WidgetScaling::Fluid );
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).font_size_fluid( 28.0 );
	assert_eq!( t.font_size, Some( crate::types::Length::fluid( 28.0 ) ) );
	// The fluid design px resolves the same as the mode's font_px( 28 ).
	let canvas = Canvas::new( 412, 900 );
	assert_eq!( t.effective_font_size( &canvas ), canvas.font_px( 28.0 ) );
	// The default follows the theme default under the mode.
	let d: TextEdit<()> = TextEdit::new( "".into(), "".into() );
	assert_eq!( d.effective_font_size( &canvas ), canvas.font_px( super::theme::FONT_SIZE ) );
}

#[ test ]
fn select_on_focus_builder_toggles_flag()
{
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).select_on_focus( true );
	assert!( t.select_on_focus );
}

#[ test ]
fn fixed_width_overrides_preferred_size_width()
{
	let canvas = crate::render::Canvas::new( 1, 1 );
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).fixed_width( 64.0 );
	let ( w, _h ) = t.preferred_size( 1000.0, &canvas );
	assert_eq!( w, 64.0 );
}

#[ test ]
fn fixed_width_capped_by_max_width()
{
	let canvas = crate::render::Canvas::new( 1, 1 );
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() ).fixed_width( 500.0 );
	let ( w, _h ) = t.preferred_size( 200.0, &canvas );
	// fixed_width is capped by the parent's available width to
	// avoid overflowing tight rows.
	assert_eq!( w, 200.0 );
}

#[ test ]
fn no_fixed_width_falls_back_to_max_width()
{
	let canvas = crate::render::Canvas::new( 1, 1 );
	let t: TextEdit<()> = TextEdit::new( "".into(), "".into() );
	let ( w, _h ) = t.preferred_size( 320.0, &canvas );
	assert_eq!( w, 320.0 );
}

#[ test ]
fn align_propagates_to_widget_handler_snapshot()
{
	use crate::widget::{ Element, WidgetHandlers };
	use super::super::text::TextAlign;
	let widget: Element<()> = TextEdit::new( "".into(), "".into() )
		.align( TextAlign::Center )
		.into_element();
	match widget.handlers()
	{
		WidgetHandlers::TextEdit { align, .. } => assert_eq!( align, TextAlign::Center ),
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn font_size_propagates_to_widget_handler_snapshot()
{
	use crate::widget::{ Element, WidgetHandlers };
	let widget: Element<()> = TextEdit::new( "".into(), "".into() )
		.font_size( 28.0 )
		.into_element();
	match widget.handlers()
	{
		WidgetHandlers::TextEdit { font_size, .. } => assert_eq!( font_size, Some( crate::types::Length::px( 28.0 ) ) ),
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn select_on_focus_propagates_to_widget_handler_snapshot()
{
	use crate::widget::{ Element, WidgetHandlers };
	let widget: Element<()> = TextEdit::new( "".into(), "".into() )
		.select_on_focus( true )
		.into_element();
	match widget.handlers()
	{
		WidgetHandlers::TextEdit { select_on_focus, .. } => assert!( select_on_focus ),
		_ => panic!( "expected TextEdit handler" ),
	}
}

// ── password_toggle ───────────────────────────────────────────────────────

#[ derive( Clone, Debug, PartialEq, Eq ) ] enum ToggleMsg { Flip }

#[ test ]
fn password_toggle_default_is_none()
{
	let t = TextEdit::<ToggleMsg>::new( "".into(), "".into() );
	assert!( t.password_toggle.is_none() );
}

#[ test ]
fn password_toggle_builder_records_visible_and_msg()
{
	let t = TextEdit::<ToggleMsg>::new( "".into(), "".into() )
		.password_toggle( true, ToggleMsg::Flip );
	assert_eq!( t.password_toggle, Some( ( true, ToggleMsg::Flip ) ) );
}

#[ test ]
fn effective_secure_falls_back_to_secure_field_when_no_toggle()
{
	let plain  = TextEdit::<ToggleMsg>::new( "".into(), "".into() );
	let secret = TextEdit::<ToggleMsg>::new( "".into(), "".into() ).secure( true );
	assert!( !plain.effective_secure() );
	assert!(   secret.effective_secure() );
}

#[ test ]
fn password_toggle_overrides_secure_field()
{
	// When the toggle is set, its `visible` controls bullets
	// regardless of the explicit `secure` flag.
	let visible_with_secure = TextEdit::<ToggleMsg>::new( "".into(), "".into() )
		.secure( true )
		.password_toggle( true, ToggleMsg::Flip );
	let hidden_no_secure = TextEdit::<ToggleMsg>::new( "".into(), "".into() )
		.password_toggle( false, ToggleMsg::Flip );
	assert!( !visible_with_secure.effective_secure() );
	assert!(  hidden_no_secure.effective_secure() );
}

#[ test ]
fn password_toggle_hidden_substitutes_bullets_in_display_text()
{
	let t = TextEdit::<ToggleMsg>::new( "".into(), "abc".into() )
		.password_toggle( false, ToggleMsg::Flip );
	assert_eq!( t.display_text(), "•••" );
}

#[ test ]
fn password_toggle_visible_returns_value_verbatim()
{
	let t = TextEdit::<ToggleMsg>::new( "".into(), "abc".into() )
		.password_toggle( true, ToggleMsg::Flip );
	assert_eq!( t.display_text(), "abc" );
}

#[ test ]
fn password_toggle_collapses_multiline_to_single_line()
{
	// `is_multiline()` follows `effective_secure` — a hidden
	// password collapses to single-line even if `multiline(true)`
	// was set, matching the behaviour of an explicit `secure`.
	let t = TextEdit::<ToggleMsg>::new( "".into(), "".into() )
		.multiline( true )
		.password_toggle( false, ToggleMsg::Flip );
	assert!( !t.is_multiline() );
}

#[ test ]
fn password_toggle_msg_propagates_to_widget_handler_snapshot()
{
	use crate::widget::{ Element, WidgetHandlers };
	let widget: Element<ToggleMsg> = TextEdit::new( "".into(), "".into() )
		.password_toggle( false, ToggleMsg::Flip )
		.into_element();
	// `handlers()` returns a value of `WidgetHandlers<Msg>`, which
	// implements `Drop` (wipe-on-drop for secure fields), so we
	// cannot destructure it by value-move. Bind first, then match
	// on a reference.
	let h = widget.handlers();
	match &h
	{
		WidgetHandlers::TextEdit { password_toggle_msg, secure, .. } =>
		{
			assert_eq!( password_toggle_msg.as_ref(), Some( &ToggleMsg::Flip ) );
			// Snapshot's `secure` is true even though `visible=false`
			// because the field carries a password regardless.
			assert!( *secure );
		}
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn password_toggle_msg_none_when_no_toggle_configured()
{
	use crate::widget::{ Element, WidgetHandlers };
	let widget: Element<ToggleMsg> = TextEdit::new( "".into(), "".into() )
		.into_element();
	let h = widget.handlers();
	match &h
	{
		WidgetHandlers::TextEdit { password_toggle_msg, .. } =>
		{
			assert!( password_toggle_msg.is_none() );
		}
		_ => panic!( "expected TextEdit handler" ),
	}
}

#[ test ]
fn password_toggle_hit_zone_sits_at_right_edge()
{
	use crate::types::{ Point, Rect };
	let rect = Rect { x: 100.0, y: 50.0, width: 240.0, height: 48.0 };
	let zone = super::password_toggle_hit_zone( rect );
	// Right edge of the zone matches the field's right edge.
	assert!( ( ( zone.x + zone.width ) - ( rect.x + rect.width ) ).abs() < 0.01 );
	// Wide enough to cover the icon + slop on both sides + half pad.
	assert!( zone.width >= super::theme::PASSWORD_TOGGLE_SIZE );
	// A point on the centre-right hits the zone.
	let p = Point { x: rect.x + rect.width - 8.0, y: rect.y + rect.height / 2.0 };
	assert!( zone.contains( p ) );
	// A point well to the left of the zone misses it.
	let p = Point { x: rect.x + 10.0, y: rect.y + rect.height / 2.0 };
	assert!( !zone.contains( p ) );
}

#[ test ]
fn password_toggle_hit_zone_clamped_when_field_narrower_than_icon()
{
	use crate::types::Rect;
	let rect = Rect { x: 0.0, y: 0.0, width: 4.0, height: 24.0 };
	let zone = super::password_toggle_hit_zone( rect );
	// Zone never extends past the field's right edge.
	assert!( zone.x + zone.width <= rect.x + rect.width + f32::EPSILON );
}

#[ test ]
fn password_toggle_drop_zeros_underlying_string_buffer()
{
	// Same wipe-on-drop guarantee `secure( true )` gives — a
	// `password_toggle` field is sensitive even when currently
	// visible, so the buffer must be scrubbed on drop.
	let t = TextEdit::<ToggleMsg>::new( "".into(), "shhh".into() )
		.password_toggle( true, ToggleMsg::Flip );
	let ptr = t.value.as_ptr();
	let len = t.value.len();
	drop( t );
	// SAFETY: `String` deallocates the buffer in its `Drop`, but
	// the bytes at `ptr..ptr+len` were zeroed *before* the
	// allocator was notified — reading them now is a use-after-
	// free, so we don't. Instead we simply assert that the test
	// reaches this line without panicking; the `secure_zero`
	// path runs unconditionally for password_toggle fields.
	let _ = ( ptr, len );
}
