extends SceneTree

## Synthetic candidate, material, ownership and timing regression for animated
## M2 scene finalization.

const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_animated_scene_finalizer.gd")
const FINALIZER_PATH := "res://src/render/m2/m2_animated_scene_finalizer.gd"
const RESOURCE_FINALIZER_PATH := (
	"res://src/render/m2/m2_animation_resource_finalizer.gd"
)
const MATERIALIZER_PATH := "res://src/render/m2/m2_animated_instance_materializer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"


func _initialize() -> void:
	var failures: Array[String] = []
	_verify_candidate_instantiation_and_invalid_lifetime(failures)
	_verify_animation_validation_and_transfer(failures)
	_verify_material_mapping(failures)
	_verify_ownership_boundaries(failures)
	var elapsed_milliseconds := _verify_bounded_timing(failures)
	if not failures.is_empty():
		for failure in failures:
			push_error("M2_ANIMATED_SCENE_FINALIZER: %s" % failure)
		quit(1)
		return
	print(
		"M2_ANIMATED_SCENE_FINALIZER PASS cases=13 iterations=10000 elapsed_ms=%.3f"
		% elapsed_milliseconds
	)
	quit(0)


func _verify_candidate_instantiation_and_invalid_lifetime(failures: Array[String]) -> void:
	var finalizer: RefCounted = FINALIZER_SCRIPT.new()
	_expect_same(finalizer.call("instantiate_candidate", null), null, "null Resource rejected", failures)
	_expect_same(finalizer.call("instantiate_candidate", Resource.new()), null, "unsupported Resource rejected", failures)

	var invalid_source := Node.new()
	var invalid_scene := PackedScene.new()
	_expect_equal(invalid_scene.pack(invalid_source), OK, "non-Node3D scene packs", failures)
	invalid_source.free()
	var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
	_expect_same(finalizer.call("instantiate_candidate", invalid_scene), null, "non-Node3D root rejected", failures)
	var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
	_expect_equal(node_count_after, node_count_before, "invalid instantiated root freed", failures)

	var valid_source := Node3D.new()
	var valid_scene := PackedScene.new()
	_expect_equal(valid_scene.pack(valid_source), OK, "Node3D scene packs", failures)
	valid_source.free()
	var candidate: Node3D = finalizer.call("instantiate_candidate", valid_scene)
	_expect_true(candidate != null, "Node3D candidate returned", failures)
	if candidate != null:
		candidate.free()


func _verify_animation_validation_and_transfer(failures: Array[String]) -> void:
	var finalizer: RefCounted = FINALIZER_SCRIPT.new()
	var empty_candidate := Node3D.new()
	var empty_candidate_id := empty_candidate.get_instance_id()
	var rejected: Dictionary = finalizer.call("finalize_candidate", empty_candidate)
	_expect_true(rejected.is_empty(), "candidate without AnimationPlayer rejected", failures)
	_expect_false(is_instance_id_valid(empty_candidate_id), "rejected candidate freed", failures)

	var animated_candidate := Node3D.new()
	var branch := Node3D.new()
	var first_player := AnimationPlayer.new()
	var second_player := AnimationPlayer.new()
	animated_candidate.add_child(branch)
	branch.add_child(first_player)
	animated_candidate.add_child(second_player)
	var players: Array[AnimationPlayer] = finalizer.call(
		"animation_players_in_subtree",
		animated_candidate
	)
	_expect_equal(players.size(), 2, "nested players discovered", failures)
	_expect_same(players[0], first_player, "player depth-first order retained", failures)
	var accepted: Dictionary = finalizer.call("finalize_candidate", animated_candidate)
	_expect_same(accepted.get("prototype"), animated_candidate, "exact candidate transferred", failures)
	_expect_equal(int(accepted.get("animation_player_count", 0)), 2, "player count transferred", failures)
	animated_candidate.free()


func _verify_material_mapping(failures: Array[String]) -> void:
	var finalizer: RefCounted = FINALIZER_SCRIPT.new()
	var material_source_root := Node3D.new()
	var source_first := MeshInstance3D.new()
	var source_second := MeshInstance3D.new()
	source_first.mesh = _make_triangle_mesh(2)
	source_second.mesh = _make_triangle_mesh(1)
	var first_override := StandardMaterial3D.new()
	var second_surface_material := StandardMaterial3D.new()
	source_first.set_surface_override_material(0, first_override)
	source_second.mesh.surface_set_material(0, second_surface_material)
	material_source_root.add_child(source_first)
	material_source_root.add_child(source_second)

	var animated_root := Node3D.new()
	var target_first := MeshInstance3D.new()
	var target_second := MeshInstance3D.new()
	target_first.mesh = _make_triangle_mesh(2)
	target_second.mesh = _make_triangle_mesh(2)
	animated_root.add_child(target_first)
	animated_root.add_child(target_second)
	var target_meshes: Array[MeshInstance3D] = finalizer.call(
		"mesh_instances_in_subtree",
		animated_root
	)
	_expect_same(target_meshes[0], target_first, "mesh depth-first order retained", failures)
	finalizer.call("repair_materials", animated_root, material_source_root)
	_expect_same(target_first.get_surface_override_material(0), first_override, "source override has priority", failures)
	_expect_same(target_first.get_surface_override_material(1), first_override, "missing source surface uses first fallback", failures)
	_expect_same(target_second.get_surface_override_material(0), second_surface_material, "source index maps to target index", failures)
	_expect_same(target_second.get_surface_override_material(1), first_override, "clamped source missing surface uses fallback", failures)
	animated_root.free()
	material_source_root.free()


func _verify_ownership_boundaries(failures: Array[String]) -> void:
	var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
	var resource_finalizer_source := FileAccess.get_file_as_string(
		RESOURCE_FINALIZER_PATH
	)
	var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
	var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
	_expect_true(loader_source.contains("M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()"), "loader composes finalizer", failures)
	for removed_loader_function in [
		"func _repair_m2_animated_materials(",
		"func _find_animation_players_recursive(",
		"func _find_mesh_instances_recursive(",
	]:
		_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
	for delegated_call in [
		"\"instantiate_candidate\"",
		"\"repair_materials\"",
		"\"finalize_candidate\"",
	]:
		_expect_equal(
			resource_finalizer_source.count(delegated_call),
			1,
			"single resource-finalizer delegation: %s" % delegated_call,
			failures
		)
	_expect_equal(
		loader_source.count("_m2_animated_scene_finalizer.mesh_instances_in_subtree(root)"),
		1,
		"single loader mesh traversal delegation",
		failures
	)
	_expect_equal(
		materializer_source.count("_animated_scene_finalizer.animation_players_in_subtree("),
		1,
		"single materializer player-inventory delegation",
		failures
	)
	for retained_renderer_rule in [
		"ResourceLoader.load_threaded_get(resource_path)",
		"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
		"_get_or_load_m2_material_prototype(normalized_rel)",
		"\"adopt_animated_prototype\"",
		"\"mark_animation_static\"",
		"M2_ANIM_CACHE path=%s cache=%s players=%d",
	]:
		_expect_true(
			(loader_source + resource_finalizer_source).contains(retained_renderer_rule),
			"renderer retains %s" % retained_renderer_rule,
			failures
		)
	for forbidden_dependency in [
		"ResourceLoader.",
		"FileAccess.",
		"WorkerThreadPool.",
		"_m2_prototype_cache_state",
		"_render_budget_scheduler",
		"M2Builder",
		"M2RawModelRepository",
	]:
		_expect_false(finalizer_source.contains(forbidden_dependency), "finalizer omits %s ownership" % forbidden_dependency, failures)


func _verify_bounded_timing(failures: Array[String]) -> float:
	var finalizer: RefCounted = FINALIZER_SCRIPT.new()
	var root := Node3D.new()
	var branch := root
	for _depth_index in range(8):
		var child_branch := Node3D.new()
		branch.add_child(child_branch)
		branch = child_branch
	branch.add_child(AnimationPlayer.new())
	var started_microseconds := Time.get_ticks_usec()
	for _iteration in range(10000):
		finalizer.call("animation_players_in_subtree", root)
	var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
	root.free()
	_expect_true(elapsed_milliseconds < 1000.0, "10000 subtree queries under 1 second", failures)
	return elapsed_milliseconds


func _make_triangle_mesh(surface_count: int) -> ArrayMesh:
	var mesh := ArrayMesh.new()
	for _surface_index in range(surface_count):
		var arrays := []
		arrays.resize(Mesh.ARRAY_MAX)
		arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
			Vector3.ZERO,
			Vector3.RIGHT,
			Vector3.UP,
		])
		arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
		mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
	return mesh


func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
	if not condition:
		failures.append(label)


func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
	_expect_true(not condition, label, failures)


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


func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
	if not is_same(actual, expected):
		failures.append(label)
