extends SceneTree

## Synthetic predicate, memoization, boundary and timing regression for stale
## cached M2 runtime mesh rebuild decisions.

const CLASSIFIER_SCRIPT := preload(
	"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
)
const CLASSIFIER_PATH := "res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
const FINALIZER_PATH := "res://src/render/m2/m2_runtime_mesh_finalizer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"


func _initialize() -> void:
	var failures: Array[String] = []
	_verify_billboard_decisions(failures)
	_verify_uv_rotation_decisions(failures)
	_verify_stage_and_index_boundaries(failures)
	_verify_memoization_clear_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_RUNTIME_MESH_REBUILD_CLASSIFIER: %s" % failure)
		quit(1)
		return
	print(
		"M2_RUNTIME_MESH_REBUILD_CLASSIFIER PASS cases=12 iterations=100 elapsed_ms=%.3f"
		% elapsed_milliseconds
	)
	quit(0)


func _verify_billboard_decisions(failures: Array[String]) -> void:
	_expect_false(_classify("empty", {}), "empty data does not rebuild", failures)
	_expect_false(
		_classify("invalid", {"batches": ["invalid", 7, null]}),
		"non-Dictionary batches skipped",
		failures
	)
	_expect_true(
		_classify("flag", {"batches": [{"has_billboard": true}]}),
		"billboard flag rebuilds",
		failures
	)
	_expect_true(
		_classify("vertices", {"batches": [{"billboard_vertex_count": 1}]}),
		"positive billboard vertex count rebuilds",
		failures
	)
	_expect_false(
		_classify("zero_vertices", {"batches": [{"billboard_vertex_count": 0}]}),
		"zero billboard vertex count does not rebuild",
		failures
	)


func _verify_uv_rotation_decisions(failures: Array[String]) -> void:
	_expect_false(
		_classify("identity", _uv_data([_rotation_transform()])),
		"identity UV rotation does not rebuild",
		failures
	)
	_expect_true(
		_classify(
			"rotation",
			_uv_data([_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0))])
		),
		"non-identity UV rotation rebuilds",
		failures
	)
	_expect_true(
		_classify("positive_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000002)])),
		"positive UV rotation speed rebuilds",
		failures
	)
	_expect_true(
		_classify("negative_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), -0.000002)])),
		"negative UV rotation speed rebuilds",
		failures
	)
	_expect_false(
		_classify("epsilon", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000001)])),
		"exact UV rotation epsilon does not rebuild",
		failures
	)


func _verify_stage_and_index_boundaries(failures: Array[String]) -> void:
	var fifth_stage_transforms: Array = [
		_rotation_transform(),
		_rotation_transform(),
		_rotation_transform(),
		_rotation_transform(),
		_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0)),
	]
	_expect_false(
		_classify(
			"fifth_stage",
			_uv_data(fifth_stage_transforms, PackedInt32Array([0, 1, 2, 3, 4]), 5)
		),
		"fifth texture stage remains ignored",
		failures
	)
	_expect_false(
		_classify(
			"invalid_indices",
			_uv_data([_rotation_transform(Vector4(0, 1, -1, 0))], PackedInt32Array([-1, 9]), 2)
		),
		"invalid transform indices skipped",
		failures
	)
	_expect_false(
		_classify(
			"invalid_transform",
			_uv_data(["not-a-transform"], PackedInt32Array([0]), 1)
		),
		"non-Dictionary transform uses identity defaults",
		failures
	)


func _verify_memoization_clear_and_diagnostics(failures: Array[String]) -> void:
	var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
	var billboard_data := {"batches": [{"has_billboard": true}]}
	_expect_true(
		classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", billboard_data),
		"first true decision",
		failures
	)
	_expect_true(
		classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
		"first path decision memoized",
		failures
	)
	classifier.call("needs_runtime_mesh_rebuild", "world/rock.m2", {})
	_expect_equal(int(classifier.call("cached_path_count")), 2, "cached path count", failures)
	var snapshot: Dictionary = classifier.call("diagnostic_snapshot")
	snapshot["world/tree.m2"] = false
	_expect_true(
		classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
		"diagnostics detached",
		failures
	)
	classifier.call("clear")
	_expect_equal(int(classifier.call("cached_path_count")), 0, "clear count", failures)
	_expect_false(
		classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
		"clear allows recomputation",
		failures
	)


func _verify_ownership_boundaries(failures: Array[String]) -> void:
	var classifier_source := FileAccess.get_file_as_string(CLASSIFIER_PATH)
	var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
	var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
	_expect_true(
		finalizer_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
		"runtime Mesh finalizer composes classifier",
		failures
	)
	_expect_equal(
		loader_source.count("_m2_runtime_mesh_finalizer.clear()"),
		2,
		"loader retains two finalizer clear sites",
		failures
	)
	_expect_false(
		loader_source.contains("_m2_runtime_rebuild_required_cache"),
		"loader removes legacy rebuild cache",
		failures
	)
	for removed_function in [
		"func _m2_raw_data_needs_runtime_mesh_rebuild",
		"func _m2_raw_data_has_billboards",
		"func _m2_raw_data_has_uv_rotation",
	]:
		_expect_false(loader_source.contains(removed_function), "loader removes %s" % removed_function, failures)
	for forbidden_dependency in [
		"ResourceLoader.",
		"WorkerThreadPool.",
		"M2_BUILDER_SCRIPT",
		"ClassDB.instantiate",
		"FileAccess.",
	]:
		_expect_false(
			classifier_source.contains(forbidden_dependency),
			"classifier omits %s dependency" % forbidden_dependency,
			failures
		)


func _verify_bounded_timing(failures: Array[String]) -> float:
	var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
	var ordinary_data := {"batches": [{"has_billboard": false}]}
	var started_microseconds := Time.get_ticks_usec()
	for iteration in range(100):
		for path_index in range(256):
			classifier.call(
				"needs_runtime_mesh_rebuild",
				"world/model_%d.m2" % path_index,
				ordinary_data
			)
		classifier.call("clear")
	var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
	_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 classifications under 1 second", failures)
	return elapsed_milliseconds


func _classify(normalized_path: String, raw_data: Dictionary) -> bool:
	var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
	return bool(classifier.call("needs_runtime_mesh_rebuild", normalized_path, raw_data))


func _uv_data(
		texture_transforms: Array,
		texture_transform_combos: PackedInt32Array = PackedInt32Array([0]),
		texture_count: int = 1) -> Dictionary:
	return {
		"texture_transforms": texture_transforms,
		"texture_transform_combos": texture_transform_combos,
		"batches": [{
			"texture_count": texture_count,
			"texture_transform_combo_index": 0,
		}],
	}


func _rotation_transform(
		rotation: Vector4 = Vector4(1.0, 0.0, 0.0, 1.0),
		rotation_speed: float = 0.0) -> Dictionary:
	return {
		"rotation": rotation,
		"rotation_speed": rotation_speed,
	}


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])
