extends SceneTree

## Synthetic phase, selection, playback, native-copy and boundary regression for
## animated M2 instance playback.

const CONTROLLER_SCRIPT := preload("res://src/render/m2/m2_animation_playback_controller.gd")
const NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
const CONTROLLER_PATH := "res://src/render/m2/m2_animation_playback_controller.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_phase_contract(failures)
	_verify_animation_selection_priority(failures)
	_verify_player_loop_play_and_seek(failures)
	_verify_native_copy_start_and_diagnostics(failures)
	_verify_ownership_boundaries(failures)
	var elapsed_milliseconds := _verify_bounded_timing(failures)
	if not failures.is_empty():
		for failure in failures:
			push_error("M2_ANIMATION_PLAYBACK_CONTROLLER: %s" % failure)
		quit(1)
		return
	print(
		"M2_ANIMATION_PLAYBACK_CONTROLLER PASS cases=15 iterations=20000 elapsed_ms=%.3f"
		% elapsed_milliseconds
	)
	quit(0)


func _verify_phase_contract(failures: Array[String]) -> void:
	var controller: RefCounted = CONTROLLER_SCRIPT.new()
	for fixture in [
		{"path": "creature/fish/fish.m2", "index": 0},
		{"path": "world/critter/eagle.m2", "index": 17},
		{"path": "World\\Critter\\Bird.m2", "index": -3},
	]:
		var path := String(fixture["path"])
		var index := int(fixture["index"])
		var expected := float(abs(("%s:%d" % [path, index]).hash()) % 1000) / 1000.0
		var actual := float(controller.call("phase_for_instance", path, index))
		_expect_float_equal(actual, expected, "exact phase formula", failures)
		_expect_true(actual >= 0.0 and actual < 1.0, "phase in unit interval", failures)


func _verify_animation_selection_priority(failures: Array[String]) -> void:
	var controller: RefCounted = CONTROLLER_SCRIPT.new()
	var ordinary := _make_player(["Idle", "Stand", "Run"])
	_expect_string_equal(String(controller.call("choose_default_animation", ordinary, "world/tree.m2")), "Stand", "ordinary priority", failures)
	var fish := _make_player(["Stand", "Swim", "Death"])
	_expect_string_equal(String(controller.call("choose_default_animation", fish, "creature/fish/a.m2")), "Swim", "fish priority", failures)
	var bird := _make_player(["Idle", "Walk", "Swim"])
	_expect_string_equal(String(controller.call("choose_default_animation", bird, "creature/eagle/a.m2")), "Walk", "bird priority", failures)
	var substring := _make_player(["Custom_Run_Loop"])
	_expect_string_equal(String(controller.call("choose_default_animation", substring, "world/a.m2")), "Custom_Run_Loop", "substring fallback", failures)
	var arbitrary := _make_player(["Zeta"])
	_expect_string_equal(String(controller.call("choose_default_animation", arbitrary, "world/a.m2")), "Zeta", "first-name fallback", failures)
	var empty_player := AnimationPlayer.new()
	_expect_string_equal(String(controller.call("choose_default_animation", empty_player, "")), "", "empty player rejected", failures)
	ordinary.free()
	fish.free()
	bird.free()
	substring.free()
	arbitrary.free()
	empty_player.free()


func _verify_player_loop_play_and_seek(failures: Array[String]) -> void:
	var controller: RefCounted = CONTROLLER_SCRIPT.new()
	var fixture_root := Node3D.new()
	var player := _make_player(["Stand", "Other"])
	var stand := player.get_animation("Stand")
	var other := player.get_animation("Other")
	stand.length = 2.0
	other.length = 1.0
	stand.loop_mode = Animation.LOOP_NONE
	other.loop_mode = Animation.LOOP_NONE
	fixture_root.add_child(player)
	get_root().add_child(fixture_root)
	var phase := float(controller.call("phase_for_instance", "world/test.m2", 5))
	var diagnostics: Array[Dictionary] = controller.call(
		"start_instance_playback",
		fixture_root,
		"world/test.m2",
		5,
		NATIVE_ANIMATOR_SCRIPT,
		[player] as Array[AnimationPlayer],
		false
	)
	_expect_true(diagnostics.is_empty(), "no native diagnostics without animators", failures)
	_expect_equal(stand.loop_mode, Animation.LOOP_LINEAR, "selected loop linear", failures)
	_expect_equal(other.loop_mode, Animation.LOOP_LINEAR, "all loops linear", failures)
	_expect_string_equal(player.current_animation, "Stand", "selected animation plays", failures)
	_expect_float_equal(player.current_animation_position, stand.length * phase, "selected animation seeks to phase", failures)
	fixture_root.free()


func _verify_native_copy_start_and_diagnostics(failures: Array[String]) -> void:
	var controller: RefCounted = CONTROLLER_SCRIPT.new()
	var source_root := Node3D.new()
	var source_branch := Node.new()
	var source_animator: Node = NATIVE_ANIMATOR_SCRIPT.new()
	source_root.add_child(source_branch)
	source_branch.add_child(source_animator)
	source_animator.mesh_instance_path = NodePath("../SourceMesh")
	source_animator.bones = [{"parent": -1}]
	source_animator.surfaces = [{"vertices": PackedVector3Array()}]
	source_animator.animation_length = 8.0
	source_animator.playback_speed = 1.5

	var target_root := Node3D.new()
	var target_branch := Node.new()
	var target_animator: Node = NATIVE_ANIMATOR_SCRIPT.new()
	target_root.add_child(target_branch)
	target_branch.add_child(target_animator)
	controller.call("copy_native_animator_data", source_root, target_root, NATIVE_ANIMATOR_SCRIPT)
	_expect_true(target_animator.mesh_instance_path == source_animator.mesh_instance_path, "native path copied", failures)
	_expect_same(target_animator.bones, source_animator.bones, "native bones reference copied", failures)
	_expect_same(target_animator.surfaces, source_animator.surfaces, "native surfaces reference copied", failures)
	_expect_float_equal(target_animator.animation_length, 8.0, "native length copied", failures)
	_expect_float_equal(target_animator.playback_speed, 1.5, "native speed copied", failures)

	var native_inventory: Array[Node] = controller.call("native_animators_in_subtree", target_root, NATIVE_ANIMATOR_SCRIPT)
	_expect_equal(native_inventory.size(), 1, "exact-script native animator discovered", failures)
	var phase := float(controller.call("phase_for_instance", "world/native.m2", 9))
	var diagnostics: Array[Dictionary] = controller.call(
		"start_instance_playback",
		target_root,
		"world/native.m2",
		9,
		NATIVE_ANIMATOR_SCRIPT,
		[] as Array[AnimationPlayer],
		true
	)
	_expect_equal(diagnostics.size(), 1, "requested native diagnostic returned", failures)
	_expect_float_equal(float(target_animator.get("_time")), 8.0 * phase, "native phase applied", failures)
	diagnostics[0]["bones"] = 999
	_expect_equal(int(target_animator.runtime_debug_state()["bones"]), 1, "native diagnostic detached", failures)
	source_root.free()
	target_root.free()


func _verify_ownership_boundaries(failures: Array[String]) -> void:
	var controller_source := FileAccess.get_file_as_string(CONTROLLER_PATH)
	var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
	var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
	_expect_true(materializer_source.contains("M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new()"), "materializer composes playback controller", failures)
	for removed_loader_function in [
		"func _start_m2_animations(",
		"func _copy_m2_native_animator_data(",
		"func _find_m2_native_animators_recursive(",
		"func _choose_default_m2_animation(",
	]:
		_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
	_expect_equal(materializer_source.count("_animation_playback_controller.copy_native_animator_data("), 1, "native copy delegates once", failures)
	_expect_equal(materializer_source.count("_animation_playback_controller.start_instance_playback("), 1, "playback delegates once", failures)
	for retained_materializer_rule in [
		"Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS",
		"batch_root.add_child(instance)",
		"_apply_shadow_cast_recursive(instance, cast_shadows)",
	]:
		_expect_true(materializer_source.contains(retained_materializer_rule), "materializer retains %s" % retained_materializer_rule, failures)
	for retained_loader_rule in [
		"_set_editor_owner_recursive(batch_root)",
		"M2_NATIVE_ANIMATOR path=%s instance=%d",
	]:
		_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
	for forbidden_dependency in [
		"ResourceLoader.",
		"FileAccess.",
		"WorkerThreadPool.",
		"MultiMesh",
		"add_child(",
		"_m2_prototype_cache_state",
		"_render_budget_scheduler",
	]:
		_expect_false(controller_source.contains(forbidden_dependency), "controller omits %s ownership" % forbidden_dependency, failures)


func _verify_bounded_timing(failures: Array[String]) -> float:
	var controller: RefCounted = CONTROLLER_SCRIPT.new()
	var player := _make_player(["Stand", "Custom_Run"])
	var started_microseconds := Time.get_ticks_usec()
	for iteration in range(20000):
		controller.call("phase_for_instance", "world/model.m2", iteration)
		controller.call("choose_default_animation", player, "world/model.m2")
	var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
	player.free()
	_expect_true(elapsed_milliseconds < 1000.0, "20000 phase/selection pairs under 1 second", failures)
	return elapsed_milliseconds


func _make_player(animation_names: Array[String]) -> AnimationPlayer:
	var player := AnimationPlayer.new()
	var library := AnimationLibrary.new()
	for animation_name in animation_names:
		library.add_animation(animation_name, Animation.new())
	player.add_animation_library("", library)
	return player


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_float_equal(actual: float, expected: float, label: String, failures: Array[String]) -> void:
	if not is_equal_approx(actual, expected):
		failures.append("%s expected=%.6f actual=%.6f" % [label, expected, actual])


func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
	if actual != expected:
		failures.append("%s expected=%s actual=%s" % [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)
