extends SceneTree

const FINALIZER_SCRIPT := preload(
	"res://src/render/m2/m2_animation_resource_finalizer.gd"
)
const PIPELINE_SCRIPT := preload(
	"res://src/render/m2/m2_animation_load_pipeline_state.gd"
)
const PROTOTYPE_CACHE_SCRIPT := preload(
	"res://src/render/m2/m2_prototype_cache_state.gd"
)
const FINALIZER_PATH := "res://src/render/m2/m2_animation_resource_finalizer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"


class FakeResourceLoaderAdapter extends RefCounted:
	var statuses: Dictionary = {}
	var resources: Dictionary = {}
	var status_paths: Array[String] = []
	var load_paths: Array[String] = []

	func load_threaded_get_status(resource_path: String) -> int:
		status_paths.append(resource_path)
		return int(statuses.get(
			resource_path,
			ResourceLoader.THREAD_LOAD_IN_PROGRESS
		))

	func load_threaded_get(resource_path: String) -> Resource:
		load_paths.append(resource_path)
		return resources.get(resource_path, null) as Resource


class FakeAnimatedSceneFinalizer extends RefCounted:
	var candidate_by_resource: Dictionary = {}
	var finalization_by_candidate: Dictionary = {}
	var instantiated_resources: Array[Resource] = []
	var repaired_candidates: Array[Node3D] = []
	var repaired_material_sources: Array[Node3D] = []

	func instantiate_candidate(resource: Resource) -> Node3D:
		instantiated_resources.append(resource)
		return candidate_by_resource.get(resource, null) as Node3D

	func repair_materials(candidate: Node3D, material_source: Node3D) -> void:
		repaired_candidates.append(candidate)
		repaired_material_sources.append(material_source)

	func finalize_candidate(candidate: Node3D) -> Dictionary:
		return finalization_by_candidate.get(candidate, {}) as Dictionary


class EmptyPathPipeline extends RefCounted:
	var discarded_path: String = ""

	func request_records_snapshot() -> Array[Dictionary]:
		return [{"normalized": "world/empty.m2", "path": ""}]

	func discard_request(normalized_relative_path: String) -> bool:
		discarded_path = normalized_relative_path
		return true


func _initialize() -> void:
	var failures: Array[String] = []
	_verify_polling(failures)
	_verify_empty_path_polling(failures)
	_verify_terminal_rejections(failures)
	_verify_preparation_and_adoption(failures)
	_verify_finalize_rejection(failures)
	_verify_source_boundaries(failures)
	var elapsed_milliseconds := _verify_bounded_timing(failures)
	if not failures.is_empty():
		for failure in failures:
			push_error("M2_ANIMATION_RESOURCE_FINALIZER: %s" % failure)
		quit(1)
		return
	print(
		"M2_ANIMATION_RESOURCE_FINALIZER PASS "
		+ "cases=28 iterations=1000 elapsed_ms=%.3f" % elapsed_milliseconds
	)
	quit(0)


func _verify_polling(failures: Array[String]) -> void:
	var resource_loader := FakeResourceLoaderAdapter.new()
	var scene_finalizer := FakeAnimatedSceneFinalizer.new()
	var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
	var pipeline: RefCounted = PIPELINE_SCRIPT.new()
	var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
	for request in [
		["world/pending.m2", "res://pending.tscn"],
		["world/loaded.m2", "res://loaded.tscn"],
		["world/failed.m2", "res://failed.tscn"],
	]:
		pipeline.call("remember_request", request[0], request[1])
	resource_loader.statuses = {
		"res://pending.tscn": ResourceLoader.THREAD_LOAD_IN_PROGRESS,
		"res://loaded.tscn": ResourceLoader.THREAD_LOAD_LOADED,
		"res://failed.tscn": ResourceLoader.THREAD_LOAD_FAILED,
	}
	_expect_equal(
		int(service.call("poll_terminal_requests", pipeline, prototype_cache)),
		2,
		"two terminal requests completed",
		failures
	)
	_expect_equal(
		int(pipeline.call("pending_request_count")),
		1,
		"pending request retained",
		failures
	)
	_expect_equal(
		int(pipeline.call("finalize_record_count")),
		2,
		"terminal FIFO receives two records",
		failures
	)
	_expect_string_array(
		resource_loader.status_paths,
		["res://pending.tscn", "res://loaded.tscn", "res://failed.tscn"],
		"status polling insertion order",
		failures
	)
	prototype_cache.call("clear_and_release")


func _verify_empty_path_polling(failures: Array[String]) -> void:
	var resource_loader := FakeResourceLoaderAdapter.new()
	var service: RefCounted = FINALIZER_SCRIPT.new(
		FakeAnimatedSceneFinalizer.new(),
		resource_loader
	)
	var pipeline := EmptyPathPipeline.new()
	var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
	_expect_equal(
		int(service.call("poll_terminal_requests", pipeline, prototype_cache)),
		1,
		"empty resource path completed",
		failures
	)
	_expect_string_equal(
		pipeline.discarded_path,
		"world/empty.m2",
		"empty resource path discarded",
		failures
	)
	_expect_true(
		bool(prototype_cache.call("is_animation_static", "world/empty.m2")),
		"empty resource path marks static",
		failures
	)
	_expect_equal(resource_loader.status_paths.size(), 0, "empty path skips I/O", failures)
	prototype_cache.call("clear_and_release")


func _verify_terminal_rejections(failures: Array[String]) -> void:
	var resource_loader := FakeResourceLoaderAdapter.new()
	var scene_finalizer := FakeAnimatedSceneFinalizer.new()
	var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
	var pipeline: RefCounted = PIPELINE_SCRIPT.new()
	var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
	_expect_dictionary_empty(
		service.call("prepare_next_candidate", pipeline, prototype_cache),
		"empty finalize FIFO",
		failures
	)
	_enqueue_terminal(
		pipeline,
		"world/failed.m2",
		"res://failed.tscn",
		ResourceLoader.THREAD_LOAD_FAILED
	)
	_expect_dictionary_empty(
		service.call("prepare_next_candidate", pipeline, prototype_cache),
		"failed terminal record rejects",
		failures
	)
	_expect_true(
		bool(prototype_cache.call("is_animation_static", "world/failed.m2")),
		"failed terminal record marks static",
		failures
	)
	var cached := Node3D.new()
	prototype_cache.call("adopt_animated_prototype", "world/cached.m2", cached)
	_enqueue_terminal(
		pipeline,
		"world/cached.m2",
		"res://cached.tscn",
		ResourceLoader.THREAD_LOAD_LOADED
	)
	_expect_dictionary_empty(
		service.call("prepare_next_candidate", pipeline, prototype_cache),
		"cached terminal record skipped",
		failures
	)
	prototype_cache.call("mark_animation_static", "world/static.m2")
	_enqueue_terminal(
		pipeline,
		"world/static.m2",
		"res://static.tscn",
		ResourceLoader.THREAD_LOAD_LOADED
	)
	_expect_dictionary_empty(
		service.call("prepare_next_candidate", pipeline, prototype_cache),
		"static terminal record skipped",
		failures
	)
	_enqueue_terminal(pipeline, "world/null.m2", "res://null.tscn", ResourceLoader.THREAD_LOAD_LOADED)
	_expect_dictionary_empty(
		service.call("prepare_next_candidate", pipeline, prototype_cache),
		"null Resource candidate rejects",
		failures
	)
	_expect_true(
		bool(prototype_cache.call("is_animation_static", "world/null.m2")),
		"null Resource candidate marks static",
		failures
	)
	_expect_string_array(
		resource_loader.load_paths,
		["res://null.tscn"],
		"only uncached loaded record performs terminal get",
		failures
	)
	prototype_cache.call("clear_and_release")


func _verify_preparation_and_adoption(failures: Array[String]) -> void:
	var resource_loader := FakeResourceLoaderAdapter.new()
	var scene_finalizer := FakeAnimatedSceneFinalizer.new()
	var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
	var pipeline: RefCounted = PIPELINE_SCRIPT.new()
	var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
	var packed_scene := PackedScene.new()
	var candidate := Node3D.new()
	var prototype := Node3D.new()
	var material_source := Node3D.new()
	resource_loader.resources["res://animated.tscn"] = packed_scene
	scene_finalizer.candidate_by_resource[packed_scene] = candidate
	scene_finalizer.finalization_by_candidate[candidate] = {
		"prototype": prototype,
		"animation_player_count": 3,
	}
	_enqueue_terminal(
		pipeline,
		"world/animated.m2",
		"res://animated.tscn",
		ResourceLoader.THREAD_LOAD_LOADED
	)
	var preparation: Dictionary = service.call(
		"prepare_next_candidate",
		pipeline,
		prototype_cache
	)
	_expect_same(preparation.get("candidate"), candidate, "candidate identity", failures)
	_expect_string_equal(
		String(preparation.get("normalized", "")),
		"world/animated.m2",
		"prepared normalized path",
		failures
	)
	_expect_string_equal(
		String(preparation.get("path", "")),
		"res://animated.tscn",
		"prepared Resource path",
		failures
	)
	var adopted: Node3D = service.call(
		"finalize_prepared_candidate",
		preparation,
		material_source,
		prototype_cache,
		false
	)
	_expect_same(adopted, prototype, "adopted prototype identity", failures)
	_expect_same(
		prototype_cache.call("find_animated_prototype", "world/animated.m2"),
		prototype,
		"prototype cache adoption",
		failures
	)
	_expect_same(scene_finalizer.repaired_candidates[0], candidate, "repair candidate", failures)
	_expect_same(
		scene_finalizer.repaired_material_sources[0],
		material_source,
		"repair material source",
		failures
	)
	prototype_cache.call("clear_and_release")
	material_source.free()
	candidate.free()


func _verify_finalize_rejection(failures: Array[String]) -> void:
	var scene_finalizer := FakeAnimatedSceneFinalizer.new()
	var service: RefCounted = FINALIZER_SCRIPT.new(
		scene_finalizer,
		FakeResourceLoaderAdapter.new()
	)
	var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
	var candidate := Node3D.new()
	_expect_null(
		service.call(
			"finalize_prepared_candidate",
			{"normalized": "world/rejected.m2", "candidate": candidate},
			null,
			prototype_cache
		),
		"rejected finalization returns null",
		failures
	)
	_expect_true(
		bool(prototype_cache.call("is_animation_static", "world/rejected.m2")),
		"rejected finalization marks static",
		failures
	)
	candidate.free()
	prototype_cache.call("clear_and_release")


func _verify_source_boundaries(failures: Array[String]) -> void:
	var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
	var service_source := FileAccess.get_file_as_string(FINALIZER_PATH)
	var drain_start := loader_source.find("func _drain_m2_animation_loads()")
	var drain_end := loader_source.find("func _drain_m2_mesh_loads()", drain_start)
	var drain_source := loader_source.substr(drain_start, drain_end - drain_start)
	_expect_true(
		drain_source.contains("_m2_animation_resource_finalizer.poll_terminal_requests("),
		"loader delegates polling",
		failures
	)
	_expect_true(
		drain_source.contains("_m2_animation_resource_finalizer.prepare_next_candidate("),
		"loader delegates candidate preparation",
		failures
	)
	_expect_true(
		drain_source.contains("_m2_animation_resource_finalizer.finalize_prepared_candidate("),
		"loader delegates finalization",
		failures
	)
	for removed_token in [
		"ResourceLoader.load_threaded_get_status",
		"ResourceLoader.load_threaded_get(",
		"_m2_animated_scene_finalizer.instantiate_candidate",
		"_m2_animated_scene_finalizer.repair_materials",
		"_m2_animated_scene_finalizer.finalize_candidate",
		"M2_ANIM_CACHE path=",
	]:
		_expect_false(drain_source.contains(removed_token), "loader omits %s" % removed_token, failures)
	for required_token in [
		"ResourceLoader.load_threaded_get_status",
		"ResourceLoader.load_threaded_get(",
		"\"instantiate_candidate\"",
		"\"repair_materials\"",
		"\"finalize_candidate\"",
		"M2_ANIM_CACHE path=%s cache=%s players=%d",
	]:
		_expect_true(
			service_source.contains(required_token),
			"service owns %s" % required_token,
			failures
		)


func _verify_bounded_timing(failures: Array[String]) -> float:
	var service: RefCounted = FINALIZER_SCRIPT.new(
		FakeAnimatedSceneFinalizer.new(),
		FakeResourceLoaderAdapter.new()
	)
	var pipeline: RefCounted = PIPELINE_SCRIPT.new()
	var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
	var started_microseconds := Time.get_ticks_usec()
	for iteration in range(1000):
		service.call("poll_terminal_requests", pipeline, prototype_cache)
	var elapsed_milliseconds := float(
		Time.get_ticks_usec() - started_microseconds
	) / 1000.0
	_expect_true(elapsed_milliseconds < 1000.0, "empty polling remains bounded", failures)
	prototype_cache.call("clear_and_release")
	return elapsed_milliseconds


func _enqueue_terminal(
	pipeline: RefCounted,
	normalized_relative_path: String,
	resource_path: String,
	status: int
) -> void:
	pipeline.call("remember_request", normalized_relative_path, resource_path)
	pipeline.call("complete_request", normalized_relative_path, status)


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_null(actual: Variant, label: String, failures: Array[String]) -> void:
	_expect_true(actual == null, label, failures)


func _expect_same(
	actual: Variant,
	expected: Variant,
	label: String,
	failures: Array[String]
) -> void:
	_expect_true(is_same(actual, expected), label, failures)


func _expect_dictionary_empty(
	actual: Variant,
	label: String,
	failures: Array[String]
) -> void:
	_expect_true(actual is Dictionary and (actual as Dictionary).is_empty(), 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_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_string_array(
	actual: Array[String],
	expected: Array[String],
	label: String,
	failures: Array[String]
) -> void:
	if actual != expected:
		failures.append("%s expected=%s actual=%s" % [label, expected, actual])
