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

use super::*;
use super::raw::*;
use super::super::paint::{ GradientSpace, Paint };
use super::super::shadow::{ BlendMode, ShadowsRef };
use super::super::slots::Slot;
use super::super::text_style::LineHeight;

#[ test ]
fn color_parser_accepts_hex_and_functional()
{
	assert_eq!( color_serde::parse( "#04D9FE" ).unwrap(),     Color::hex( 0x04, 0xD9, 0xFE ) );
	assert_eq!( color_serde::parse( "04D9FE" ).unwrap(),      Color::hex( 0x04, 0xD9, 0xFE ) );
	let with_a = color_serde::parse( "#FFFFFF80" ).unwrap();
	assert!( ( with_a.a - 128.0 / 255.0 ).abs() < 1e-6 );
	let f = color_serde::parse( "rgba(255, 147, 169, 0.8)" ).unwrap();
	assert_eq!( f.r, 1.0 );
	assert!( ( f.g - 147.0 / 255.0 ).abs() < 1e-6 );
	assert!( ( f.a - 0.8 ).abs() < 1e-6 );
	let three = color_serde::parse( "rgb(0, 0, 0)" ).unwrap();
	assert_eq!( three, Color::BLACK );
}

#[ test ]
fn color_parser_rejects_bad_input()
{
	assert!( color_serde::parse( "" ).is_err() );
	assert!( color_serde::parse( "#FFF" ).is_err() );
	assert!( color_serde::parse( "rgba(1,2,3)" ).is_err() );
	assert!( color_serde::parse( "rgba(1,2,3,4,5)" ).is_err() );
	assert!( color_serde::parse( "rgba(300, 0, 0, 1)" ).is_err() );
}

#[ test ]
fn color_formatter_is_canonical()
{
	assert_eq!( color_serde::format( Color::hex( 0x04, 0xD9, 0xFE ) ), "#04D9FE" );
	let semi = Color { r: 1.0, g: 1.0, b: 1.0, a: 0.5 };
	assert_eq!( color_serde::format( semi ), "#FFFFFF80" );
}

#[ test ]
fn slot_color_parses_with_meta()
{
	let json = r##"
	{
		"type": "color",
		"value": "#04D9FE",
		"meta": { "semantic": "primary/500" }
	}
	"##;
	let raw: RawSlot = serde_json::from_str( json ).unwrap();
	let slot = Slot::from( raw );
	match slot
	{
		Slot::Color { value, meta } =>
		{
			assert_eq!( value, Color::hex( 0x04, 0xD9, 0xFE ) );
			assert_eq!( meta.semantic.as_deref(), Some( "primary/500" ) );
		}
		other => panic!( "expected color slot, got {:?}", other ),
	}
}

#[ test ]
fn gradient_under_cap_is_passed_through_verbatim()
{
	let raw_stops: Vec<RawColorStop> = ( 0..16 )
		.map( |i| serde_json::from_str(
			&format!( r##"{{ "pos": {}, "color": "#FFFFFFFF" }}"##, i as f32 / 15.0 ),
		).unwrap() )
		.collect();
	let result = collect_capped_stops( raw_stops );
	assert_eq!( result.len(), 16, "stops below the soft cap pass through unchanged" );
}

#[ test ]
fn gradient_at_exact_cap_is_passed_through()
{
	let raw_stops: Vec<RawColorStop> = ( 0..MAX_GRADIENT_STOPS )
		.map( |i| serde_json::from_str(
			&format!( r##"{{ "pos": {}, "color": "#FFFFFFFF" }}"##, i as f32 / 63.0 ),
		).unwrap() )
		.collect();
	let result = collect_capped_stops( raw_stops );
	assert_eq!( result.len(), MAX_GRADIENT_STOPS );
}

#[ test ]
fn gradient_over_cap_is_truncated_to_cap()
{
	let raw_stops: Vec<RawColorStop> = ( 0..200 )
		.map( |i| serde_json::from_str(
			&format!( r##"{{ "pos": {}, "color": "#000000FF" }}"##, i as f32 ),
		).unwrap() )
		.collect();
	let result = collect_capped_stops( raw_stops );
	assert_eq!( result.len(), MAX_GRADIENT_STOPS, "tail past the cap must be dropped" );
}

#[ test ]
fn slot_linear_gradient_accepts_extrapolated_stops()
{
	let json = r##"
	{
		"type": "linear",
		"angle_deg": 152.77,
		"space": "linear-rgb",
		"stops":
		[
			{ "pos": -1.1654, "color": "rgba(255, 147, 169, 0.8)" },
			{ "pos":  1.2332, "color": "rgba(255, 255, 255, 0.8)" }
		]
	}
	"##;
	let raw: RawSlot = serde_json::from_str( json ).unwrap();
	match Slot::from( raw )
	{
		Slot::Paint { value: Paint::Linear( grad ), .. } =>
		{
			assert_eq!( grad.stops.len(), 2 );
			assert!( grad.stops[0].position < 0.0 );
			assert!( grad.stops[1].position > 1.0 );
			assert_eq!( grad.space, GradientSpace::LinearRgb );
		}
		_ => panic!( "expected linear paint" ),
	}
}

#[ test ]
fn slot_shadows_parses_stack()
{
	let json = r##"
	{
		"type": "shadows",
		"shadows":
		[
			{ "offset": [0, 4], "blur": 10, "color": "#00000014" },
			{ "offset": [0, 1], "blur": 4,  "color": "#0000000A" }
		]
	}
	"##;
	let raw: RawSlot = serde_json::from_str( json ).unwrap();
	match Slot::from( raw )
	{
		Slot::Shadows { value, .. } =>
		{
			assert_eq!( value.len(), 2 );
			assert_eq!( value[0].offset, [ 0.0, 4.0 ] );
			assert_eq!( value[0].blur,   10.0 );
			assert_eq!( value[0].blend,  BlendMode::Normal );
		}
		_ => panic!( "expected shadows slot" ),
	}
}

#[ test ]
fn slot_surface_reproduces_glass_accent()
{
	let json = r##"
	{
		"type": "surface",
		"fill": { "type": "solid", "color": "#04D9FE" },
		"shadows": "shadows-glass",
		"inset_shadows":
		[
			{ "offset": [-3.6, -3.6], "blur": 13.5, "color": "#555555", "blend": "plus-lighter" },
			{ "offset": [ 1.8,  1.8], "blur":  1.8, "color": "#555555", "blend": "plus-lighter" },
			{ "offset": [ 0.45, 0.45], "blur": 0.9, "color": "#000000", "blend": "overlay"     },
			{ "offset": [ 1.8,  1.8], "blur":  7.2, "color": "#00000026", "blend": "normal"    }
		],
		"backdrop": { "blur_px": 22.5 }
	}
	"##;
	let raw: RawSlot = serde_json::from_str( json ).unwrap();
	match Slot::from( raw )
	{
		Slot::Surface { value, .. } =>
		{
			assert!( matches!( value.fill, Paint::Solid( _ ) ) );
			assert_eq!( value.inset_shadows.len(), 4 );
			assert_eq!( value.inset_shadows[0].blend, BlendMode::PlusLighter );
			assert_eq!( value.inset_shadows[2].blend, BlendMode::Overlay );
			match value.shadows.as_ref().unwrap()
			{
				ShadowsRef::Named( id ) => assert_eq!( id, "shadows-glass" ),
				other                   => panic!( "expected named shadow ref, got {:?}", other ),
			}
		}
		_ => panic!( "expected surface slot" ),
	}
}

#[ test ]
fn slot_typography_parses_body_m()
{
	let json = r##"
	{
		"type": "typography",
		"family": { "ref": "sora" },
		"weight": 400,
		"size": 16,
		"line_height": { "px": 24 },
		"letter_spacing": 0,
		"meta": { "semantic": "body/m" }
	}
	"##;
	let raw: RawSlot = serde_json::from_str( json ).unwrap();
	match Slot::from( raw )
	{
		Slot::TextStyle { value, meta } =>
		{
			assert_eq!( value.weight, 400 );
			assert_eq!( value.size, 16.0 );
			assert_eq!( value.line_height, LineHeight::Px( 24.0 ) );
			assert_eq!( meta.semantic.as_deref(), Some( "body/m" ) );
		}
		_ => panic!( "expected typography slot" ),
	}
}

#[ test ]
fn document_parses_minimal_two_modes()
{
	let json = r##"
	{
		"theme": { "id": "demo", "name": "Demo" },
		"fonts":
		{
			"sora":
			{
				"name": "Sora",
				"fallbacks": ["system-ui", "sans-serif"],
				"sources":
				[
					{ "weight": 400, "path": "fonts/Sora-Regular.ttf" }
				]
			}
		},
		"modes":
		{
			"light":
			{
				"slots":
				{
					"primary-500": { "type": "color", "value": "#04D9FE" },
					"body-m":      { "type": "typography", "family": { "ref": "sora" }, "weight": 400, "size": 16, "line_height": { "px": 24 } }
				}
			},
			"dark":
			{
				"slots":
				{
					"primary-500": { "type": "color", "value": "#04D9FE" }
				}
			}
		}
	}
	"##;
	let doc = parse_document_json( json, None ).unwrap();
	assert_eq!( doc.id,   "demo" );
	assert_eq!( doc.name, "Demo" );
	assert_eq!( doc.fonts.len(), 1 );
	assert_eq!( doc.fonts["sora"].sources[0].weight, 400 );
	assert_eq!( doc.light.slots.len(), 2 );
	assert_eq!( doc.dark.slots.len(),  1 );
	assert!( doc.light.slots.color( "primary-500" ).is_some() );
	assert!( doc.light.slots.text_style( "body-m" ).is_some() );
}

#[ test ]
fn unknown_fields_in_stop_are_rejected()
{
	// `deny_unknown_fields` doesn't cooperate well with
	// `#[ serde( flatten ) ]`, so the slot-level check would be unreliable
	// on variants that flatten a body struct. The safety net lives on
	// concrete nested types like `RawColorStop`, `RawShadow`, and
	// `RawWallpaper`, which reject typos outright.
	let bad = r##"
	{
		"type": "linear",
		"angle_deg": 90,
		"stops":
		[
			{ "pos": 0, "color": "#FFFFFF", "extraneous": true }
		]
	}
	"##;
	assert!( serde_json::from_str::<RawSlot>( bad ).is_err() );
}
