extends SceneTree

## Asset-free contract, dependency and timing regression for ADT M2 grouping.

const GROUPER_SCRIPT := preload("res://src/render/m2/m2_placement_grouper.gd")
const TRANSFORM_RESOLVER_SCRIPT := preload(
	"res://src/render/m2/m2_placement_transform_resolver.gd"
)
const GROUPER_PATH := "res://src/render/m2/m2_placement_grouper.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"


func _initialize() -> void:
	var failures: Array[String] = []
	_verify_empty_and_invalid_inputs(failures)
	_verify_historical_path_normalization(failures)
	_verify_group_order(failures)
	_verify_tile_local_defaults(failures)
	_verify_regular_transform(failures)
	_verify_waterfall_transform(failures)
	_verify_scale_clamp(failures)
	_verify_fresh_output(failures)
	_verify_source_boundaries(failures)
	var elapsed_milliseconds := _verify_bounded_timing(failures)
	if not failures.is_empty():
		for failure in failures:
			push_error("M2_PLACEMENT_GROUPER: %s" % failure)
		quit(1)
		return
	print(
		"M2_PLACEMENT_GROUPER PASS cases=9 iterations=100 placements=256 elapsed_ms=%.3f"
		% elapsed_milliseconds
	)
	quit(0)


func _verify_empty_and_invalid_inputs(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	_expect_equal(
		grouper.group_placements(Vector3.ZERO, PackedStringArray(), []).size(),
		0,
		"empty",
		failures
	)
	var placements: Array = [
		"invalid",
		{"name_id": -1},
		{"name_id": 2},
		{"name_id": 0},
	]
	var groups: Dictionary = grouper.group_placements(
		Vector3.ZERO,
		PackedStringArray([""]),
		placements
	)
	_expect_equal(groups.size(), 0, "invalid placements", failures)


func _verify_historical_path_normalization(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	var names := PackedStringArray([
		"World\\Generic\\Tree.mdx",
		"World/Generic/Rock.mdl",
		"World/Generic/Upper.MDX",
	])
	var placements: Array = [{"name_id": 0}, {"name_id": 1}, {"name_id": 2}]
	var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
	_expect_true(groups.has("World/Generic/Tree.m2"), "backslash and mdx normalization", failures)
	_expect_true(groups.has("World/Generic/Rock.m2"), "mdl normalization", failures)
	_expect_true(groups.has("World/Generic/Upper.MDX"), "uppercase suffix preservation", failures)


func _verify_group_order(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	var placements: Array = [
		{"name_id": 0, "pos": Vector3(3.0, 0.0, 0.0)},
		{"name_id": 1, "pos": Vector3(8.0, 0.0, 0.0)},
		{"name_id": 0, "pos": Vector3(5.0, 0.0, 0.0)},
	]
	var groups: Dictionary = grouper.group_placements(
		Vector3.ZERO,
		PackedStringArray(["Tree.m2", "Rock.m2"]),
		placements
	)
	_expect_equal(groups.size(), 2, "group count", failures)
	var tree_transforms: Array = groups.get("Tree.m2", [])
	_expect_equal(tree_transforms.size(), 2, "tree transform count", failures)
	if tree_transforms.size() == 2:
		_expect_vector_near(tree_transforms[0].origin, Vector3(3.0, 0.0, 0.0), "first tree", failures)
		_expect_vector_near(tree_transforms[1].origin, Vector3(5.0, 0.0, 0.0), "second tree", failures)


func _verify_tile_local_defaults(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	var groups: Dictionary = grouper.group_placements(
		Vector3(10.0, 20.0, 30.0),
		PackedStringArray(["Tree.m2"]),
		[{"name_id": 0}]
	)
	var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
	_expect_vector_near(transform.origin, Vector3(-10.0, -20.0, -30.0), "default local position", failures)
	_expect_basis_near(transform.basis, Basis.IDENTITY, "default basis and scale", failures)


func _verify_regular_transform(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	var rotation := Vector3(0.2, -0.4, 0.6)
	var groups: Dictionary = grouper.group_placements(
		Vector3(1.0, 2.0, 3.0),
		PackedStringArray(["Tree.m2"]),
		[{
			"name_id": 0,
			"pos": Vector3(5.0, 8.0, 13.0),
			"rot": rotation,
			"scale": 2.0,
		}]
	)
	var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
	_expect_basis_near(transform.basis, Basis.from_euler(rotation).scaled(Vector3.ONE * 2.0), "regular basis", failures)
	_expect_vector_near(transform.origin, Vector3(4.0, 6.0, 10.0), "regular origin", failures)


func _verify_waterfall_transform(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	var resolver := TRANSFORM_RESOLVER_SCRIPT.new()
	var path := "World/X/ElwynnTallWaterfall01.m2"
	var rotation := Vector3(0.05, -0.2, 0.15)
	var scale_value := 1.5
	var position := Vector3(20.0, 30.0, 40.0)
	var tile_origin := Vector3(2.0, 3.0, 4.0)
	var groups: Dictionary = grouper.group_placements(
		tile_origin,
		PackedStringArray([path]),
		[{
			"name_id": 0,
			"pos": position,
			"rot": rotation,
			"scale": scale_value,
		}]
	)
	var transform: Transform3D = (groups[path] as Array)[0]
	var expected_basis: Basis = resolver.resolve_basis(rotation, path).scaled(
		Vector3.ONE * scale_value
	)
	var expected_origin: Vector3 = (
		position
		- tile_origin
		+ resolver.resolve_origin_offset(rotation, path, scale_value)
	)
	_expect_basis_near(transform.basis, expected_basis, "waterfall basis", failures)
	_expect_vector_near(transform.origin, expected_origin, "waterfall origin", failures)


func _verify_scale_clamp(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	var groups: Dictionary = grouper.group_placements(
		Vector3.ZERO,
		PackedStringArray(["Tree.m2"]),
		[{"name_id": 0, "scale": -5.0}]
	)
	var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
	_expect_basis_near(
		transform.basis,
		Basis.IDENTITY.scaled(Vector3.ONE * 0.0001),
		"negative scale clamp",
		failures
	)


func _verify_fresh_output(failures: Array[String]) -> void:
	var grouper := GROUPER_SCRIPT.new()
	var names := PackedStringArray(["Tree.m2"])
	var placements: Array = [{"name_id": 0}]
	var first: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
	(first["Tree.m2"] as Array).clear()
	var second: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
	_expect_equal((second["Tree.m2"] as Array).size(), 1, "fresh output", failures)


func _verify_source_boundaries(failures: Array[String]) -> void:
	var loader_source := _read_text(LOADER_PATH, failures)
	var grouper_source := _read_text(GROUPER_PATH, failures)
	var grouping_task_source := _source_between(
		loader_source,
		"func _group_tile_m2_task(",
		"func _drain_m2_group_results()",
		failures
	)
	_expect_equal(
		loader_source.count("_m2_placement_grouper.group_placements("),
		1,
		"one loader grouping adapter",
		failures
	)
	_expect_true(
		not grouping_task_source.contains("for placement_variant in m2_placements"),
		"loader omits grouping loop",
		failures
	)
	_expect_equal(
		grouper_source.count("placement_transform_resolver.resolve_basis("),
		1,
		"one grouper basis adapter",
		failures
	)
	_expect_equal(
		grouper_source.count("placement_transform_resolver.resolve_origin_offset("),
		1,
		"one grouper offset adapter",
		failures
	)
	for forbidden_dependency in ["WorkerThreadPool", "Mutex", "RenderingServer", "ResourceLoader"]:
		_expect_true(
			not grouper_source.contains(forbidden_dependency),
			"grouper omits %s" % forbidden_dependency,
			failures
		)


func _verify_bounded_timing(failures: Array[String]) -> float:
	var grouper := GROUPER_SCRIPT.new()
	var names := PackedStringArray(["Tree.m2", "Rock.mdx"])
	var placements: Array = []
	for placement_index in range(256):
		placements.append({
			"name_id": placement_index % 2,
			"pos": Vector3(float(placement_index), 0.0, float(-placement_index)),
			"rot": Vector3(0.1, 0.2, 0.3),
			"scale": 1.0,
		})
	var started_microseconds := Time.get_ticks_usec()
	for iteration in range(100):
		var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
		if groups.size() != 2:
			failures.append("timing iteration %d produced wrong group count" % iteration)
			break
	var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
	_expect_true(elapsed_milliseconds < 1000.0, "100 x 256 grouping bounded", failures)
	return elapsed_milliseconds


func _read_text(path: String, failures: Array[String]) -> String:
	var file := FileAccess.open(path, FileAccess.READ)
	if file == null:
		failures.append("cannot open %s" % path)
		return ""
	return file.get_as_text()


func _source_between(
		source: String,
		start_marker: String,
		end_marker: String,
		failures: Array[String]
) -> String:
	var start_offset := source.find(start_marker)
	var end_offset := source.find(end_marker, start_offset + start_marker.length())
	if start_offset < 0 or end_offset <= start_offset:
		failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
		return ""
	return source.substr(start_offset, end_offset - start_offset)


func _expect_basis_near(
		actual_basis: Basis,
		expected_basis: Basis,
		label: String,
		failures: Array[String]
) -> void:
	if not actual_basis.is_equal_approx(expected_basis):
		failures.append("%s basis mismatch" % label)


func _expect_vector_near(
		actual_value: Vector3,
		expected_value: Vector3,
		label: String,
		failures: Array[String]
) -> void:
	if not actual_value.is_equal_approx(expected_value):
		failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])


func _expect_equal(
		actual_value: int,
		expected_value: int,
		label: String,
		failures: Array[String]
) -> void:
	if actual_value != expected_value:
		failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])


func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
	if not actual_value:
		failures.append("%s expected true" % label)
