extends SceneTree

## Headless M02 contract, interpolation, cache and player-injection regression.

const TERRAIN_QUERY_PATH := "res://src/gameplay/terrain/terrain_query.gd"
const ADT_TERRAIN_QUERY_PATH := "res://src/gameplay/terrain/adt_terrain_query.gd"
const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn"

var _synthetic_adt_tile_data: Dictionary = {}
var _synthetic_loader_call_count := 0


class FlatTerrainQuery extends TerrainQuery:
	var _height_units: float

	func _init(height_units: float) -> void:
		_height_units = height_units

	func sample_ground_height(_godot_world_position: GodotWorldPosition) -> TerrainGroundSample:
		return TerrainGroundSample.available(_height_units)


func _initialize() -> void:
	_run_verification.call_deferred()


func _run_verification() -> void:
	var failures: Array[String] = []
	_verify_typed_result_contract(failures)
	_verify_synthetic_adt_interpolation_and_cache(failures)
	_verify_unavailable_tile(failures)
	_verify_player_injection(failures)
	_verify_source_boundaries(failures)

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

	print("TERRAIN_QUERY PASS contract=4 interpolation=1 cache=1 failures=2 player_injection=1")
	quit(0)


func _verify_typed_result_contract(failures: Array[String]) -> void:
	var available_sample := TerrainGroundSample.available(123.5)
	_expect_true(available_sample.is_available, "available sample state", failures)
	_expect_near(available_sample.height_units, 123.5, "available sample height", failures)
	_expect_true(available_sample.failure_code.is_empty(), "available sample has no failure", failures)

	var unavailable_sample := TerrainGroundSample.unavailable(&"synthetic_failure")
	_expect_true(not unavailable_sample.is_available, "unavailable sample state", failures)
	_expect_equal(unavailable_sample.failure_code, &"synthetic_failure", "unavailable failure code", failures)

	var base_sample := TerrainQuery.new().sample_ground_height(GodotWorldPosition.new(0.0, 0.0, 0.0))
	_expect_equal(base_sample.failure_code, &"terrain_query_not_implemented", "base query failure", failures)

	var non_finite_sample := TerrainGroundSample.available(NAN)
	_expect_true(not non_finite_sample.is_available, "non-finite sample unavailable", failures)
	_expect_equal(non_finite_sample.failure_code, &"terrain_height_not_finite", "non-finite failure code", failures)


func _verify_synthetic_adt_interpolation_and_cache(failures: Array[String]) -> void:
	var query_position := GodotWorldPosition.new(17000.0, 999.0, 26525.0)
	_synthetic_adt_tile_data = _build_synthetic_adt_tile(query_position)
	_synthetic_loader_call_count = 0
	var terrain_query := AdtTerrainQuery.new("res://synthetic", "Azeroth", _load_synthetic_adt_tile)

	var first_sample := terrain_query.sample_ground_height(query_position)
	_expect_true(first_sample.is_available, "synthetic ADT sample available", failures)
	_expect_near_with_tolerance(first_sample.height_units, 137.0, 0.002, "synthetic bilinear height", failures)
	var second_sample := terrain_query.sample_ground_height(query_position)
	_expect_near_with_tolerance(second_sample.height_units, 137.0, 0.002, "cached synthetic height", failures)
	_expect_equal(_synthetic_loader_call_count, 1, "ADT tile loaded once", failures)


func _verify_unavailable_tile(failures: Array[String]) -> void:
	var query_position := GodotWorldPosition.new(17000.0, 0.0, 26525.0)
	var empty_query := AdtTerrainQuery.new("res://synthetic", "Azeroth", func(_path: String) -> Dictionary: return {})
	var empty_sample := empty_query.sample_ground_height(query_position)
	_expect_true(not empty_sample.is_available, "empty ADT unavailable", failures)
	_expect_equal(empty_sample.failure_code, &"adt_tile_unavailable", "empty ADT failure", failures)

	var invalid_heights_data := _build_synthetic_adt_tile(query_position)
	var invalid_chunks: Array = invalid_heights_data.get("chunks", [])
	(invalid_chunks[invalid_chunks.size() - 1] as Dictionary)["heights"] = PackedFloat32Array()
	var invalid_query := AdtTerrainQuery.new(
		"res://synthetic",
		"Azeroth",
		func(_path: String) -> Dictionary: return invalid_heights_data
	)
	var invalid_sample := invalid_query.sample_ground_height(query_position)
	_expect_equal(invalid_sample.failure_code, &"adt_chunk_heights_invalid", "invalid heights failure", failures)


func _verify_player_injection(failures: Array[String]) -> void:
	var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene
	_expect_true(packed_scene != null, "regression scene loads", failures)
	if packed_scene == null:
		return
	var player := packed_scene.instantiate() as CharacterBody3D
	player.set_terrain_query(FlatTerrainQuery.new(20.0))
	root.add_child(player)
	player.set_physics_process(false)
	_expect_near(player.position.y, 20.05, "injected spawn ground height", failures)
	player.position.y = 30.0
	player.call("_physics_process", 1.0)
	_expect_near(player.position.y, 20.05, "injected physics ground snap", failures)
	player.free()


func _verify_source_boundaries(failures: Array[String]) -> void:
	var terrain_contract_source := _read_text(TERRAIN_QUERY_PATH, failures)
	_expect_true(not terrain_contract_source.contains("extends Node"), "TerrainQuery omits Node", failures)
	_expect_true(not terrain_contract_source.contains("Vector3"), "TerrainQuery omits untyped Vector3 position", failures)

	var adt_query_source := _read_text(ADT_TERRAIN_QUERY_PATH, failures)
	_expect_true(adt_query_source.contains("ADTLoader"), "ADT adapter owns native loader", failures)
	_expect_true(adt_query_source.contains("_adt_tile_cache"), "ADT adapter owns tile cache", failures)

	var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
	for forbidden_text in ["ADTLoader", "load_adt", "_adt_cache", "_find_chunk", "_outer_height_index"]:
		_expect_true(not player_source.contains(forbidden_text), "player omits %s" % forbidden_text, failures)
	_expect_true(player_source.contains("set_terrain_query"), "player exposes terrain query injection", failures)
	_expect_true(player_source.contains("_terrain_query.sample_ground_height"), "player delegates ground sampling", failures)


func _build_synthetic_adt_tile(query_position: GodotWorldPosition) -> Dictionary:
	var tile_coordinate := CoordinateMapper.godot_to_adt_tile(query_position)
	var tile_local_position := CoordinateMapper.godot_to_adt_tile_local(query_position)
	var chunk_coordinate := CoordinateMapper.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
	var chunk_size := CoordinateMapper.ADT_CHUNK_SIZE_YARDS
	var outer_grid_unit_size := chunk_size / 8.0
	var target_chunk_origin := Vector3(
		query_position.x_units - outer_grid_unit_size * 4.5,
		100.0,
		query_position.z_units - outer_grid_unit_size * 3.25
	)
	var tile_origin := target_chunk_origin - Vector3(
		chunk_coordinate.chunk_x * chunk_size,
		0.0,
		chunk_coordinate.chunk_y * chunk_size
	)

	var heights := PackedFloat32Array()
	heights.resize(145)
	for row in range(9):
		for column in range(9):
			heights[row * 17 + column] = float(row * 10 + column)
	var target_chunk := {"origin": target_chunk_origin, "heights": heights}
	if chunk_coordinate.chunk_x == 0 and chunk_coordinate.chunk_y == 0:
		return {"chunks": [target_chunk]}
	return {
		"chunks": [
			{"origin": tile_origin, "heights": PackedFloat32Array()},
			target_chunk,
		]
	}


func _load_synthetic_adt_tile(_absolute_path: String) -> Dictionary:
	_synthetic_loader_call_count += 1
	return _synthetic_adt_tile_data


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_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
	if absf(actual_value - expected_value) > 0.000001:
		failures.append("%s expected %.6f, got %.6f" % [label, expected_value, actual_value])


func _expect_near_with_tolerance(
		actual_value: float,
		expected_value: float,
		tolerance: float,
		label: String,
		failures: Array[String]
) -> void:
	if absf(actual_value - expected_value) > tolerance:
		failures.append("%s expected %.6f ± %.6f, got %.6f" % [label, expected_value, tolerance, actual_value])


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