use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{ AtomicU32, Ordering };

use ltk::{ ThemeDocument, ThemeMode, WallpaperFit };

/// Serialises tests that mutate the process-global active theme state
/// (`set_active_mode`, `theme_wallpaper`, …). `cargo test` runs tests
/// in parallel by default; without this guard a Light test can read
/// Dark state set by a concurrent test mid-flight.
static THEME_STATE_LOCK: Mutex<()> = Mutex::new( () );

// ── Per-test temp dir ─────────────────────────────────────────────────────────

/// Build a unique temp dir per test invocation. `process::id()` alone is not
/// enough — `cargo test` runs tests in the same process so a stale dir from a
/// previous test in the same run would collide. Add a process-local counter.
fn fresh_temp_dir( tag: &str ) -> PathBuf
{
	static COUNTER: AtomicU32 = AtomicU32::new( 0 );
	let n   = COUNTER.fetch_add( 1, Ordering::SeqCst );
	let dir = std::env::temp_dir()
		.join( format!( "ltk-theme-test-{}-{}-{}", std::process::id(), tag, n ) );
	let _ = std::fs::remove_dir_all( &dir );
	std::fs::create_dir_all( &dir ).unwrap();
	dir
}

const MINIMAL_THEME_JSON: &str = r##"
{
	"theme": { "id": "demo", "name": "Demo" },
	"fonts": {
		"sora": { "name": "Sora", "fallbacks": [], "sources": [] }
	},
	"modes": {
		"light": {
			"wallpaper":       { "path": "wp/light.jpg",      "fit": "cover"   },
			"lockscreen":      { "path": "wp/lock-light.jpg", "fit": "cover"   },
			"launcher":        { "background": "#FFFFFFE6", "border_radius": 24.0 },
			"window_controls": {
				"icon":           "#112233",
				"hover_bg":       "#11223322",
				"pressed_bg":     "#11223333",
				"close_hover_bg": "#AA0000",
				"close_icon":     "#FFFFFF",
				"focus_ring":     "#00CEB1"
			},
			"slots": {
				"bg-page":        { "type": "color", "value": "#FFFFFF"   },
				"surface":        { "type": "color", "value": "#FFFFFF99" },
				"surface-alt":    { "type": "color", "value": "#FFFFFFD9" },
				"text-primary":   { "type": "color", "value": "#102030"   },
				"text-secondary": { "type": "color", "value": "#10203080" },
				"accent":         { "type": "color", "value": "#00CEB1"   },
				"divider":        { "type": "color", "value": "#00000014" },
				"icon":           { "type": "color", "value": "#000000"   }
			}
		},
		"dark": {
			"wallpaper":       { "path": "wp/dark.jpg",      "fit": "contain" },
			"lockscreen":      { "path": "wp/lock-dark.jpg", "fit": "cover"   },
			"launcher":        { "background": "#221A5AE6", "border_radius": 24.0 },
			"window_controls": {
				"icon":           "#EEEEEE",
				"hover_bg":       "#FFFFFF18",
				"pressed_bg":     "#FFFFFF26",
				"close_hover_bg": "#E5484D",
				"close_icon":     "#FFFFFF",
				"focus_ring":     "#00CEB1"
			},
			"slots": {
				"bg-page":        { "type": "color", "value": "#000000"   },
				"surface":        { "type": "color", "value": "#18104599" },
				"surface-alt":    { "type": "color", "value": "#221A5AD9" },
				"text-primary":   { "type": "color", "value": "#FFFFFF"   },
				"text-secondary": { "type": "color", "value": "#FFFFFFB3" },
				"accent":         { "type": "color", "value": "#00CEB1"   },
				"divider":        { "type": "color", "value": "#FFFFFF14" },
				"icon":           { "type": "color", "value": "#FFFFFF"   }
			}
		}
	}
}
"##;

// ── Round-trip ────────────────────────────────────────────────────────────────

#[ test ]
fn parses_id_name_and_both_modes()
{
	let dir = fresh_temp_dir( "round-trip" );
	std::fs::write( dir.join( "theme.json" ), MINIMAL_THEME_JSON ).unwrap();

	let doc = ThemeDocument::load_from_dir( &dir ).expect( "minimal theme should parse" );
	assert_eq!( doc.id,   "demo" );
	assert_eq!( doc.name, "Demo" );

	// Wallpaper fit fields round-trip through the enum.
	assert_eq!( doc.light.wallpaper.as_ref().unwrap().fit, WallpaperFit::Cover );
	assert_eq!( doc.dark.wallpaper.as_ref().unwrap().fit,  WallpaperFit::Contain );

	let _ = std::fs::remove_dir_all( &dir );
}

#[ test ]
fn palette_hex_strings_round_trip_through_slots()
{
	let dir = fresh_temp_dir( "palette" );
	std::fs::write( dir.join( "theme.json" ), MINIMAL_THEME_JSON ).unwrap();
	let doc = ThemeDocument::load_from_dir( &dir ).unwrap();

	// "#102030" → R=0x10/255, G=0x20/255, B=0x30/255, A=1.0
	let p = doc.light.slots.color( "text-primary" ).expect( "text-primary slot" );
	let expected = ( 0x10 as f32 / 255.0, 0x20 as f32 / 255.0, 0x30 as f32 / 255.0 );
	assert!( ( p.r - expected.0 ).abs() < 1e-3, "r={}, expected {}", p.r, expected.0 );
	assert!( ( p.g - expected.1 ).abs() < 1e-3, "g={}, expected {}", p.g, expected.1 );
	assert!( ( p.b - expected.2 ).abs() < 1e-3, "b={}, expected {}", p.b, expected.2 );
	assert!( ( p.a - 1.0 ).abs() < 1e-3,        "a={}, expected 1.0", p.a );

	// 8-digit hex with alpha: "#10203080" → A ≈ 0x80/255
	let s = doc.light.slots.color( "text-secondary" ).unwrap();
	assert!( ( s.a - ( 0x80 as f32 / 255.0 ) ).abs() < 1e-3 );

	let _ = std::fs::remove_dir_all( &dir );
}

#[ test ]
fn window_control_tokens_round_trip()
{
	let dir = fresh_temp_dir( "window-controls" );
	std::fs::write( dir.join( "theme.json" ), MINIMAL_THEME_JSON ).unwrap();
	let doc = ThemeDocument::load_from_dir( &dir ).unwrap();

	let wc = doc.light.window_controls.as_ref().expect( "window_controls" );
	assert!( ( wc.icon.r - ( 0x11 as f32 / 255.0 ) ).abs() < 1e-3 );
	assert!( ( wc.icon.g - ( 0x22 as f32 / 255.0 ) ).abs() < 1e-3 );
	assert!( ( wc.icon.b - ( 0x33 as f32 / 255.0 ) ).abs() < 1e-3 );
	assert!( ( wc.icon.a - 1.0 ).abs() < 1e-3 );

	assert!( ( wc.hover_bg.a - ( 0x22 as f32 / 255.0 ) ).abs() < 1e-3 );

	let _ = std::fs::remove_dir_all( &dir );
}

#[ test ]
fn relative_asset_paths_resolve_against_theme_dir()
{
	let dir = fresh_temp_dir( "paths" );
	std::fs::write( dir.join( "theme.json" ), MINIMAL_THEME_JSON ).unwrap();
	let doc = ThemeDocument::load_from_dir( &dir ).unwrap();

	let wp = doc.light.wallpaper.as_ref().unwrap().path.as_ref().expect( "wallpaper path" );
	assert_eq!( wp, &dir.join( "wp/light.jpg" ) );

	let lock = doc.dark.lockscreen.as_ref().unwrap().path.as_ref().expect( "lockscreen path" );
	assert_eq!( lock, &dir.join( "wp/lock-dark.jpg" ) );

	let _ = std::fs::remove_dir_all( &dir );
}

// ── Errors ────────────────────────────────────────────────────────────────────

#[ test ]
fn missing_theme_json_returns_error()
{
	// Empty dir — load_from_dir should fail rather than panic.
	let dir = fresh_temp_dir( "missing" );
	let result = ThemeDocument::load_from_dir( &dir );
	assert!( result.is_err(), "expected Err for empty dir" );
	let _ = std::fs::remove_dir_all( &dir );
}

// ── Mode resolution ───────────────────────────────────────────────────────────

/// Point `LTK_THEMES_DIR` at the in-tree `themes/` so any test that
/// triggers `ensure_active` finds the bundled `default` theme without
/// requiring a system install. Sets the env var unconditionally — the
/// global `ACTIVE` is process-wide so the first test that calls
/// `ensure_active` wins, and a stale env var from a prior run would
/// just point at the same directory.
fn point_at_in_tree_themes()
{
	std::env::set_var
	(
		"LTK_THEMES_DIR",
		env!( "CARGO_MANIFEST_DIR" ).to_string() + "/themes",
	);
}

#[ test ]
fn theme_mode_round_trip()
{
	// Switching the active mode at runtime is observable via
	// `ltk::active_mode()`. This triggers `ensure_active()` which loads
	// the in-tree default theme.
	let _guard = THEME_STATE_LOCK.lock().unwrap_or_else( |p| p.into_inner() );
	point_at_in_tree_themes();

	ltk::set_active_mode( ThemeMode::Light );
	assert_eq!( ltk::active_mode(), ThemeMode::Light );
	ltk::set_active_mode( ThemeMode::Dark );
	assert_eq!( ltk::active_mode(), ThemeMode::Dark );
}

#[ test ]
fn theme_wallpaper_resolves_for_active_mode()
{
	// `theme_wallpaper()` resolves via the convention
	// `branding/{mode}/wallpaper.svg` (the default theme no longer
	// declares an explicit `wallpaper.path` in theme.json), with the
	// standard mode → opposite-mode → no-mode fallback chain.
	let _guard = THEME_STATE_LOCK.lock().unwrap_or_else( |p| p.into_inner() );
	point_at_in_tree_themes();

	ltk::set_active_mode( ThemeMode::Light );
	let wp = ltk::theme_wallpaper().expect( "Light resolves via branding/light/" );
	let path = wp.path.expect( "convention resolution returns a concrete path" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/light/wallpaper.svg" ),
		"unexpected Light wallpaper path: {}", path.display(),
	);

	ltk::set_active_mode( ThemeMode::Dark );
	let wp = ltk::theme_wallpaper().expect( "Dark resolves via branding/dark/" );
	let path = wp.path.expect( "convention resolution returns a concrete path" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/dark/wallpaper.svg" ),
		"unexpected Dark wallpaper path: {}", path.display(),
	);
}

#[ test ]
fn theme_lockscreen_resolves_for_active_mode()
{
	// `theme_lockscreen()` mirrors `theme_wallpaper()` against the
	// convention `branding/{mode}/lockscreen.svg`. The default theme
	// ships both Light and Dark variants, so each mode resolves to its
	// own asset without needing the fallback chain.
	let _guard = THEME_STATE_LOCK.lock().unwrap_or_else( |p| p.into_inner() );
	point_at_in_tree_themes();

	ltk::set_active_mode( ThemeMode::Light );
	let ls = ltk::theme_lockscreen().expect( "Light resolves via branding/light/" );
	let path = ls.path.expect( "convention resolution returns a concrete path" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/light/lockscreen.svg" ),
		"unexpected Light lockscreen path: {}", path.display(),
	);

	ltk::set_active_mode( ThemeMode::Dark );
	let ls = ltk::theme_lockscreen().expect( "Dark resolves via branding/dark/" );
	let path = ls.path.expect( "convention resolution returns a concrete path" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/dark/lockscreen.svg" ),
		"unexpected Dark lockscreen path: {}", path.display(),
	);
}

#[ test ]
fn theme_logo_variants_resolve_for_active_mode()
{
	// `theme_logo`, `theme_logo_square`, `theme_logo_horizontal` are
	// thin wrappers over `branding_asset( "logo/<variant>", "svg" )`.
	// The default theme ships all three SVG variants for both modes
	// under `branding/{mode}/logo/`, so each call resolves directly
	// to its own-mode file without the cross-mode fallback.
	let _guard = THEME_STATE_LOCK.lock().unwrap_or_else( |p| p.into_inner() );
	point_at_in_tree_themes();

	for mode in [ ThemeMode::Light, ThemeMode::Dark ]
	{
		ltk::set_active_mode( mode );
		let mode_dir = match mode
		{
			ThemeMode::Light => "branding/light/logo/",
			ThemeMode::Dark  => "branding/dark/logo/",
		};

		let primary = ltk::theme_logo()
			.unwrap_or_else( || panic!( "theme_logo missing for {mode:?}" ) );
		assert!
		(
			primary.to_string_lossy().ends_with( &format!( "{mode_dir}logo.svg" ) ),
			"unexpected {mode:?} logo path: {}", primary.display(),
		);

		let square = ltk::theme_logo_square()
			.unwrap_or_else( || panic!( "theme_logo_square missing for {mode:?}" ) );
		assert!
		(
			square.to_string_lossy().ends_with( &format!( "{mode_dir}square.svg" ) ),
			"unexpected {mode:?} square logo path: {}", square.display(),
		);

		let horizontal = ltk::theme_logo_horizontal()
			.unwrap_or_else( || panic!( "theme_logo_horizontal missing for {mode:?}" ) );
		assert!
		(
			horizontal.to_string_lossy().ends_with( &format!( "{mode_dir}horizontal.svg" ) ),
			"unexpected {mode:?} horizontal logo path: {}", horizontal.display(),
		);
	}
}

#[ test ]
fn branding_raster_picks_smallest_covering_at_each_target_size()
{
	// The default theme ships `1280x720`, `1920x1080`, `3840x2160` WebPs
	// for both wallpaper and lockscreen in both modes. The lookup must
	// pick the smallest entry whose two dimensions cover the surface.
	let _guard = THEME_STATE_LOCK.lock().unwrap_or_else( |p| p.into_inner() );
	point_at_in_tree_themes();

	ltk::set_active_mode( ThemeMode::Light );

	// Surface ≤ 720p → 1280x720 covers it.
	let path = ltk::theme_branding_raster( "wallpaper", 800, 600 )
		.expect( "Light raster covers 800x600" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/light/wallpaper/1280x720.webp" ),
		"unexpected raster for 800x600: {}", path.display(),
	);

	// Surface 1080p exact → 1920x1080 covers it.
	let path = ltk::theme_branding_raster( "wallpaper", 1920, 1080 )
		.expect( "Light raster covers 1920x1080" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/light/wallpaper/1920x1080.webp" ),
		"unexpected raster for 1920x1080: {}", path.display(),
	);

	// 1920x1200 (16:10 laptop) → 1080 < 1200, so 1080p is too short. Next
	// up that covers both axes is 4K.
	let path = ltk::theme_branding_raster( "wallpaper", 1920, 1200 )
		.expect( "Light raster covers 1920x1200" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/light/wallpaper/3840x2160.webp" ),
		"unexpected raster for 1920x1200: {}", path.display(),
	);
}

#[ test ]
fn branding_raster_falls_back_to_largest_when_none_covers()
{
	// 5000-wide surface exceeds the largest shipped raster (3840). No
	// covering candidate, so the lookup falls back to the largest
	// available raster — a fast-decoding upscaled WebP beats paying the
	// SVG rasterisation cost.
	let _guard = THEME_STATE_LOCK.lock().unwrap_or_else( |p| p.into_inner() );
	point_at_in_tree_themes();

	ltk::set_active_mode( ThemeMode::Dark );

	let path = ltk::theme_branding_raster( "wallpaper", 5000, 3000 )
		.expect( "no covering raster but the largest is returned" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/dark/wallpaper/3840x2160.webp" ),
		"unexpected fallback raster for 5000x3000: {}", path.display(),
	);

	// `branding_image` returns the same — it only falls to the SVG when
	// no raster files exist at all.
	let img = ltk::theme_branding_image( "wallpaper", 5000, 3000 )
		.expect( "branding_image returns the same largest raster" );
	assert_eq!( img, path );
}

#[ test ]
fn branding_raster_with_zero_size_returns_smallest_available()
{
	// (0, 0) means "surface size unknown — give me the smallest variant".
	// Every entry trivially covers a 0×0 surface so the smallest by area
	// wins. Used at startup so the first frame paints a fast-decoded
	// lightweight raster instead of paying the SVG rasterisation cost.
	let _guard = THEME_STATE_LOCK.lock().unwrap_or_else( |p| p.into_inner() );
	point_at_in_tree_themes();

	ltk::set_active_mode( ThemeMode::Light );

	let path = ltk::theme_branding_raster( "wallpaper", 0, 0 )
		.expect( "(0, 0) returns the smallest raster" );
	assert!
	(
		path.to_string_lossy().ends_with( "branding/light/wallpaper/1280x720.webp" ),
		"unexpected raster for (0, 0): {}", path.display(),
	);

	let img = ltk::theme_branding_image( "wallpaper", 0, 0 )
		.expect( "branding_image returns the same smallest raster" );
	assert_eq!( img, path );
}
