extends SceneTree

## Asset-free M2 build dispatch priority, transition, source-boundary and
## bounded-timing regression.

const PLANNER_SCRIPT := preload(
	"res://src/render/m2/m2_build_dispatch_planner.gd"
)
const SNAPSHOT_SCRIPT := preload(
	"res://src/render/m2/m2_build_resource_snapshot.gd"
)
const PLANNER_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"


func _initialize() -> void:
	var failures: Array[String] = []
	_verify_wait_and_empty_actions(failures)
	_verify_materialization_priority(failures)
	_verify_static_retry_and_missing_actions(failures)
	_verify_fresh_results(failures)
	_verify_loader_and_dependency_boundaries(failures)
	var elapsed_milliseconds := _verify_bounded_timing(failures)
	if not failures.is_empty():
		for failure in failures:
			push_error("M2_BUILD_DISPATCH_PLANNER: %s" % failure)
		quit(1)
		return
	print(
		"M2_BUILD_DISPATCH_PLANNER PASS cases=10 iterations=20000 elapsed_ms=%.3f"
		% elapsed_milliseconds
	)
	quit(0)


func _verify_wait_and_empty_actions(failures: Array[String]) -> void:
	var planner: RefCounted = PLANNER_SCRIPT.new()
	var prototype := Node3D.new()
	var snapshot := _make_snapshot(prototype, true, ArrayMesh.new(), true)
	var animation_wait: Dictionary = planner.call(
		"plan_step", 4, snapshot
	)
	_expect_plan(
		animation_wait,
		PLANNER_SCRIPT.ACTION_WAIT_FOR_ANIMATION,
		true,
		false,
		"animation wait has absolute priority",
		failures
	)
	var empty_batch: Dictionary = planner.call(
		"plan_step", 0, _make_snapshot(prototype, false, ArrayMesh.new(), true)
	)
	_expect_plan(
		empty_batch,
		PLANNER_SCRIPT.ACTION_ADVANCE_WITHOUT_MATERIALIZATION,
		false,
		false,
		"zero batch advances without serial",
		failures
	)
	var negative_batch: Dictionary = planner.call(
		"plan_step", -3, _make_snapshot(null, false, null, false)
	)
	_expect_plan(
		negative_batch,
		PLANNER_SCRIPT.ACTION_ADVANCE_WITHOUT_MATERIALIZATION,
		false,
		false,
		"negative batch advances without serial",
		failures
	)
	prototype.free()


func _verify_materialization_priority(failures: Array[String]) -> void:
	var planner: RefCounted = PLANNER_SCRIPT.new()
	var prototype := Node3D.new()
	var animated: Dictionary = planner.call(
		"plan_step", 3, _make_snapshot(prototype, false, ArrayMesh.new(), true)
	)
	_expect_plan(
		animated,
		PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED,
		false,
		true,
		"animated prototype wins over static state",
		failures
	)
	var static_mesh: Dictionary = planner.call(
		"plan_step", 3, _make_snapshot(null, false, ArrayMesh.new(), true)
	)
	_expect_plan(
		static_mesh,
		PLANNER_SCRIPT.ACTION_MATERIALIZE_STATIC,
		false,
		true,
		"prepared static mesh wins over missing flag",
		failures
	)
	prototype.free()


func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void:
	var planner: RefCounted = PLANNER_SCRIPT.new()
	var static_wait: Dictionary = planner.call(
		"plan_step", 2, _make_snapshot(null, false, null, false)
	)
	_expect_plan(
		static_wait,
		PLANNER_SCRIPT.ACTION_WAIT_FOR_STATIC_MESH,
		true,
		false,
		"unresolved static mesh rotates without serial",
		failures
	)
	var missing: Dictionary = planner.call(
		"plan_step", 2, _make_snapshot(null, false, null, true)
	)
	_expect_plan(
		missing,
		PLANNER_SCRIPT.ACTION_ADVANCE_WITHOUT_MATERIALIZATION,
		false,
		true,
		"terminal missing model advances serial",
		failures
	)


func _verify_fresh_results(failures: Array[String]) -> void:
	var planner: RefCounted = PLANNER_SCRIPT.new()
	var prototype := Node3D.new()
	var snapshot := _make_snapshot(prototype, false, null, false)
	var first: Dictionary = planner.call("plan_step", 1, snapshot)
	first["action"] = &"mutated"
	first["rotate_queue"] = true
	var second: Dictionary = planner.call("plan_step", 1, snapshot)
	_expect_plan(
		second,
		PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED,
		false,
		true,
		"plan results are detached",
		failures
	)
	prototype.free()


func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
	var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
	var planner_source := FileAccess.get_file_as_string(PLANNER_PATH)
	_expect_true(
		loader_source.contains("M2_BUILD_DISPATCH_PLANNER_SCRIPT.new()"),
		"loader composes dispatch planner",
		failures
	)
	_expect_true(
		loader_source.contains("_m2_build_dispatch_planner.plan_step("),
		"loader delegates dispatch decision",
		failures
	)
	_expect_true(
		loader_source.contains("_m2_static_build_resource_observer.observe(")
		and loader_source.contains("_materialize_m2_animated_batch(")
		and loader_source.contains("_materialize_m2_group_batch(")
		and loader_source.contains("try_consume_permit("),
		"loader retains readiness, materialization and permits",
		failures
	)
	for forbidden_token in [
		"preload(",
		"ResourceLoader",
		"RenderingServer",
		"FileAccess",
		"queue_free",
		"Node3D",
		"WorkerThreadPool",
		"Thread",
		"Mutex",
	]:
		_expect_false(
			planner_source.contains(forbidden_token),
			"planner omits %s ownership" % forbidden_token,
			failures
		)


func _verify_bounded_timing(failures: Array[String]) -> float:
	var planner: RefCounted = PLANNER_SCRIPT.new()
	var snapshots := [
		_make_snapshot(null, true, null, false),
		_make_snapshot(null, false, null, false),
		_make_snapshot(null, false, ArrayMesh.new(), false),
		_make_snapshot(null, false, null, true),
	]
	var started_microseconds := Time.get_ticks_usec()
	for iteration in range(20000):
		planner.call(
			"plan_step",
			iteration % 5,
			snapshots[iteration % snapshots.size()]
		)
	var elapsed_milliseconds := (
		float(Time.get_ticks_usec() - started_microseconds) / 1000.0
	)
	_expect_true(
		elapsed_milliseconds < 1000.0,
		"20,000 dispatch plans remain bounded",
		failures
	)
	return elapsed_milliseconds


func _make_snapshot(
	animated_prototype: Node3D,
	animation_request_pending: bool,
	static_mesh: Mesh,
	static_model_missing: bool
) -> RefCounted:
	var snapshot: RefCounted = SNAPSHOT_SCRIPT.new(
		"world/test.m2",
		animated_prototype,
		animation_request_pending
	)
	snapshot.call(
		"adopt_static_observation",
		static_mesh,
		static_model_missing
	)
	return snapshot


func _expect_plan(
	plan: Dictionary,
	expected_action: StringName,
	expected_rotation: bool,
	expected_serial_increment: bool,
	label: String,
	failures: Array[String]
) -> void:
	if plan.get("action", &"") != expected_action:
		failures.append("%s action" % label)
	if bool(plan.get("rotate_queue", false)) != expected_rotation:
		failures.append("%s rotation" % label)
	if (
		bool(plan.get("increment_batch_serial", false))
		!= expected_serial_increment
	):
		failures.append("%s serial" % label)


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)
