extends SceneTree

## Asset-free M03 regression for pure runtime/editor ADT target planning.

const PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
const POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")

const PLANNER_PATH := "res://src/render/streaming/streaming_target_planner.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const PERFORMANCE_ITERATIONS := 250
const MAXIMUM_AVERAGE_PLAN_MILLISECONDS := 4.0


func _initialize() -> void:
	var failures: Array[String] = []
	var planner = PLANNER_SCRIPT.new()
	_verify_center_radii_and_isolation(planner, failures)
	_verify_corner_prefetch_and_catalog_filter(planner, failures)
	_verify_policy_clamps(planner, failures)
	_verify_editor_plan(planner, failures)
	_verify_scene_free_extraction(failures)
	var elapsed_milliseconds := _measure_planner_iterations(planner, failures)

	if not failures.is_empty():
		for failure in failures:
			push_error("STREAMING_TARGET_PLANNER: %s" % failure)
		quit(1)
		return

	print("STREAMING_TARGET_PLANNER PASS cases=5 iterations=%d elapsed_ms=%.3f average_ms=%.3f" % [
		PERFORMANCE_ITERATIONS,
		elapsed_milliseconds,
		elapsed_milliseconds / float(PERFORMANCE_ITERATIONS),
	])
	quit(0)


func _verify_center_radii_and_isolation(planner, failures: Array[String]) -> void:
	var available_tiles := _tile_catalog(28, 36, 44, 52)
	var focus_position = _tile_position(32, 48, 0.5, 0.5)
	var policy = POLICY_SCRIPT.new(0, 1, 0, 1, 1, 0.35)
	var plan = planner.plan_runtime(available_tiles, focus_position, policy)
	var wanted_tile_keys: Dictionary = plan.wanted_tile_keys()
	var retained_tile_keys: Dictionary = plan.retained_tile_keys()

	_expect_equal(plan.focus_tile.tile_x, 32, "center focus tile X", failures)
	_expect_equal(plan.focus_tile.tile_y, 48, "center focus tile Y", failures)
	_expect_equal(wanted_tile_keys.size(), 25, "center wanted 5x5", failures)
	_expect_equal(retained_tile_keys.size(), 49, "center retained 7x7", failures)
	_expect_true(wanted_tile_keys.has("30_46"), "wanted includes north-west radius edge", failures)
	_expect_true(wanted_tile_keys.has("34_50"), "wanted includes south-east radius edge", failures)
	_expect_true(retained_tile_keys.has("29_45"), "retained includes hysteresis edge", failures)

	_expect_true(wanted_tile_keys.is_read_only(), "wanted result is immutable", failures)
	_expect_true(retained_tile_keys.is_read_only(), "retained result is immutable", failures)
	_expect_equal(available_tiles.size(), 81, "available catalog remains unchanged", failures)


func _verify_corner_prefetch_and_catalog_filter(planner, failures: Array[String]) -> void:
	var available_tiles := {
		"32_48": "tile-a",
		"33_48": "tile-b",
		"32_49": "tile-c",
		"33_49": "tile-d",
		"31_48": "outside-prefetch",
	}
	var focus_position = _tile_position(32, 48, 0.99, 0.99)
	var policy = POLICY_SCRIPT.new(0, 0, 0, 0, 0, 0.35)
	var plan = planner.plan_runtime(available_tiles, focus_position, policy)
	_expect_set_equal(
		plan.wanted_tile_keys(),
		["32_48", "33_48", "32_49", "33_49"],
		"corner prefetch cross product",
		failures
	)
	_expect_set_equal(
		plan.retained_tile_keys(),
		["32_48", "33_48", "32_49", "33_49"],
		"corner retained cross product",
		failures
	)


func _verify_policy_clamps(planner, failures: Array[String]) -> void:
	var policy = POLICY_SCRIPT.new(16, 2, -5, -3, -4, 0.9)
	_expect_equal(policy.visible_tile_radius(), 2, "visible radius keeps tile minimum", failures)
	_expect_equal(policy.load_tile_radius(), 2, "negative prewarm clamps to zero", failures)
	_expect_equal(policy.retained_tile_radius(), 2, "negative retain clamps to zero", failures)
	_expect_near(policy.clamped_boundary_prefetch_threshold(), 0.49, "threshold upper clamp", failures)

	var plan = planner.plan_runtime(
		_tile_catalog(30, 34, 46, 50),
		_tile_position(32, 48, 0.5, 0.5),
		policy
	)
	_expect_equal(plan.wanted_tile_keys().size(), 25, "clamped policy wanted radius", failures)
	_expect_equal(plan.retained_tile_keys().size(), 25, "clamped policy retained radius", failures)


func _verify_editor_plan(planner, failures: Array[String]) -> void:
	var available_tiles := _tile_catalog(31, 33, 47, 49)
	var focus_position = _tile_position(32, 48, 0.5, 0.5)
	var negative_radius_plan = planner.plan_editor(available_tiles, focus_position, -4)
	_expect_set_equal(
		negative_radius_plan.wanted_tile_keys(),
		["32_48"],
		"editor negative radius clamps to focus",
		failures
	)
	_expect_set_equal(
		negative_radius_plan.retained_tile_keys(),
		["32_48"],
		"editor retained matches wanted",
		failures
	)

	var radius_one_plan = planner.plan_editor(available_tiles, focus_position, 1)
	_expect_equal(radius_one_plan.wanted_tile_keys().size(), 9, "editor radius one 3x3", failures)
	_expect_set_equal(
		radius_one_plan.retained_tile_keys(),
		radius_one_plan.wanted_tile_keys().keys(),
		"editor radius retained matches wanted",
		failures
	)


func _verify_scene_free_extraction(failures: Array[String]) -> void:
	var planner_source := _read_text(PLANNER_PATH, failures)
	_expect_true(planner_source.contains("extends RefCounted"), "planner is RefCounted", failures)
	_expect_true(not planner_source.contains("extends Node"), "planner does not inherit Node", failures)
	_expect_true(not planner_source.contains("Vector3"), "planner keeps typed position boundary", failures)
	for forbidden_text in ["WorkerThreadPool", "RenderingServer", "ResourceLoader", "_tile_load_queue"]:
		_expect_true(not planner_source.contains(forbidden_text), "planner omits %s" % forbidden_text, failures)

	var loader_source := _read_text(LOADER_PATH, failures)
	_expect_true(loader_source.contains("_streaming_target_planner.plan_runtime"), "loader delegates runtime planning", failures)
	_expect_true(loader_source.contains("_streaming_target_planner.plan_editor"), "loader delegates editor planning", failures)
	_expect_true(not loader_source.contains("func _predictive_focus_tiles"), "loader no longer owns prefetch planner", failures)


func _measure_planner_iterations(planner, failures: Array[String]) -> float:
	var available_tiles := _tile_catalog(24, 40, 40, 56)
	var focus_position = _tile_position(32, 48, 0.99, 0.99)
	var policy = POLICY_SCRIPT.new(40, 5, 1, 3, 4, 0.42)
	var started_microseconds := Time.get_ticks_usec()
	for _iteration in PERFORMANCE_ITERATIONS:
		planner.plan_runtime(available_tiles, focus_position, policy)
	var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
	var average_plan_milliseconds := elapsed_milliseconds / float(PERFORMANCE_ITERATIONS)
	_expect_true(
		average_plan_milliseconds <= MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
		"average plan stays within %.1f ms (actual %.3f)" % [
			MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
			average_plan_milliseconds,
		],
		failures
	)
	return elapsed_milliseconds


func _tile_catalog(minimum_tile_x: int, maximum_tile_x: int, minimum_tile_y: int, maximum_tile_y: int) -> Dictionary:
	var available_tiles: Dictionary = {}
	for tile_y in range(minimum_tile_y, maximum_tile_y + 1):
		for tile_x in range(minimum_tile_x, maximum_tile_x + 1):
			available_tiles["%d_%d" % [tile_x, tile_y]] = "synthetic"
	return available_tiles


func _tile_position(tile_x: int, tile_y: int, east_fraction: float, south_fraction: float) -> GodotWorldPosition:
	var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(tile_x, tile_y)
	var tile_local_position = ADT_TILE_LOCAL_POSITION_SCRIPT.new(
		east_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS,
		0.0,
		south_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
	)
	return COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(tile_coordinate, tile_local_position)


func _read_text(path: String, failures: Array[String]) -> String:
	var file := FileAccess.open(path, FileAccess.READ)
	if file == null:
		failures.append("cannot open %s" % path)
		return ""
	return file.get_as_text()


func _expect_set_equal(actual_set: Dictionary, expected_keys: Array, label: String, failures: Array[String]) -> void:
	var actual_keys := actual_set.keys()
	actual_keys.sort()
	var sorted_expected_keys := expected_keys.duplicate()
	sorted_expected_keys.sort()
	if actual_keys != sorted_expected_keys:
		failures.append("%s expected %s, got %s" % [label, sorted_expected_keys, actual_keys])


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


func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
	if not is_equal_approx(actual_value, expected_value):
		failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])


func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
	if not actual_value:
		failures.append("%s expected true" % label)
