extends SceneTree

## Validates the non-proprietary M01 coordinate fixture against coordinate
## contract version 2 and the existing M00 build-12340 manifest. The script does
## not read server databases, proprietary ADT bytes or original-client images.

const CoordinateMapperScript = preload("res://src/domain/coordinates/coordinate_mapper.gd")
const CanonicalWowWorldPositionScript = preload("res://src/domain/coordinates/canonical_wow_world_position.gd")
const ServerWorldPositionScript = preload("res://src/domain/coordinates/server_world_position.gd")
const ServerWorldYawScript = preload("res://src/domain/coordinates/server_world_yaw.gd")

const FIXTURE_PATH := "res://src/tests/fixtures/coordinate_golden_points.json"
const RENDER_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const SUPPORTED_FIXTURE_SCHEMA_VERSION := 1
const REQUIRED_COORDINATE_CONTRACT_VERSION := 2


func _initialize() -> void:
	var failures: Array[String] = []
	var fixture := _load_json_object(FIXTURE_PATH, failures)
	var render_manifest := _load_json_object(RENDER_MANIFEST_PATH, failures)
	if fixture.is_empty() or render_manifest.is_empty():
		_report_and_quit(failures)
		return

	_expect_equal(fixture.get("schema_version", -1), SUPPORTED_FIXTURE_SCHEMA_VERSION, "fixture schema version", failures)
	_expect_equal(fixture.get("coordinate_contract_version", -1), REQUIRED_COORDINATE_CONTRACT_VERSION, "coordinate contract version", failures)
	var position_tolerance_yards := float(fixture.get("tolerances", {}).get("position_yards", 0.0))
	var yaw_tolerance_radians := float(fixture.get("tolerances", {}).get("yaw_radians", 0.0))
	_expect_true(position_tolerance_yards > 0.0, "positive position tolerance", failures)
	_expect_true(yaw_tolerance_radians > 0.0, "positive yaw tolerance", failures)
	_verify_source_provenance(fixture.get("sources", {}), failures)
	_verify_server_spawns(fixture.get("server_spawns", []), position_tolerance_yards, yaw_tolerance_radians, failures)
	_verify_raw_adt_chunks(fixture.get("raw_adt_chunks", []), position_tolerance_yards, failures)
	_verify_original_client_points(fixture.get("original_client_points", []), render_manifest, position_tolerance_yards, failures)

	if not failures.is_empty():
		_report_and_quit(failures)
		return
	print("COORDINATE_GOLDEN_FIXTURES PASS server_spawns=1 raw_adt_chunks=1 original_client_points=5")
	quit(0)


func _verify_source_provenance(sources: Dictionary, failures: Array[String]) -> void:
	var azerothcore_source: Dictionary = sources.get("azerothcore_playercreateinfo", {})
	_expect_equal(azerothcore_source.get("status", ""), "verified", "AzerothCore source status", failures)
	_expect_equal(String(azerothcore_source.get("revision", "")).length(), 40, "AzerothCore pinned revision length", failures)
	_expect_true(String(azerothcore_source.get("url", "")).contains(String(azerothcore_source.get("revision", ""))), "AzerothCore URL contains pinned revision", failures)

	var trinitycore_source: Dictionary = sources.get("trinitycore_playercreateinfo", {})
	_expect_equal(trinitycore_source.get("status", ""), "not_verified", "TrinityCore source status", failures)
	_expect_true(not String(trinitycore_source.get("reason", "")).is_empty(), "TrinityCore gap reason", failures)

	var adt_source: Dictionary = sources.get("azeroth_32_48_adt", {})
	_expect_equal(adt_source.get("status", ""), "verified_private_metadata", "ADT source status", failures)
	_expect_equal(String(adt_source.get("sha256", "")).length(), 64, "ADT SHA-256 length", failures)


func _verify_server_spawns(server_spawns: Array, position_tolerance_yards: float, yaw_tolerance_radians: float, failures: Array[String]) -> void:
	_expect_equal(server_spawns.size(), 1, "server spawn count", failures)
	for spawn_variant in server_spawns:
		if not (spawn_variant is Dictionary):
			failures.append("server spawn must be an object")
			continue
		var spawn: Dictionary = spawn_variant
		var name := String(spawn.get("name", "server_spawn"))
		var server_position_values := _require_three_numbers(spawn.get("server_position", null), "%s server_position" % name, failures)
		if server_position_values.is_empty():
			continue
		var server_position = ServerWorldPositionScript.new(server_position_values[0], server_position_values[1], server_position_values[2])
		var canonical_position = CoordinateMapperScript.server_to_canonical(server_position)
		var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
		var adt_placement = CoordinateMapperScript.canonical_to_adt_placement(canonical_position)
		var adt_tile = CoordinateMapperScript.canonical_to_adt_tile(canonical_position)
		var adt_local = CoordinateMapperScript.canonical_to_adt_tile_local(canonical_position)
		var adt_chunk = CoordinateMapperScript.adt_tile_local_to_chunk(adt_tile, adt_local)

		_expect_three_near([godot_position.x_units, godot_position.y_units, godot_position.z_units], spawn.get("expected_godot_position", null), position_tolerance_yards, "%s Godot position" % name, failures)
		_expect_three_near([adt_placement.x_offset_yards, adt_placement.height_yards, adt_placement.z_offset_yards], spawn.get("expected_adt_placement", null), position_tolerance_yards, "%s ADT placement" % name, failures)
		_expect_three_near([adt_local.east_yards, adt_local.height_yards, adt_local.south_yards], spawn.get("expected_adt_tile_local", null), position_tolerance_yards, "%s ADT local" % name, failures)
		_expect_pair([adt_tile.tile_x, adt_tile.tile_y], spawn.get("expected_adt_tile", null), "%s ADT tile" % name, failures)
		_expect_pair([adt_chunk.chunk_x, adt_chunk.chunk_y], spawn.get("expected_adt_chunk", null), "%s ADT chunk" % name, failures)

		var server_yaw = ServerWorldYawScript.new(float(spawn.get("server_yaw_radians", 0.0)))
		var godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(CoordinateMapperScript.server_to_canonical_yaw(server_yaw))
		_expect_near(godot_yaw.radians, float(spawn.get("expected_godot_yaw_radians", 0.0)), yaw_tolerance_radians, "%s Godot yaw" % name, failures)


func _verify_raw_adt_chunks(raw_adt_chunks: Array, position_tolerance_yards: float, failures: Array[String]) -> void:
	_expect_equal(raw_adt_chunks.size(), 1, "raw ADT chunk count", failures)
	for chunk_variant in raw_adt_chunks:
		if not (chunk_variant is Dictionary):
			failures.append("raw ADT chunk must be an object")
			continue
		var chunk: Dictionary = chunk_variant
		var name := String(chunk.get("name", "raw_adt_chunk"))
		var origin_values := _require_three_numbers(chunk.get("raw_header_canonical_origin", null), "%s raw origin" % name, failures)
		if origin_values.is_empty():
			continue
		var half_chunk_size_yards := CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 0.5
		var chunk_center = CanonicalWowWorldPositionScript.new(
			origin_values[0] - half_chunk_size_yards,
			origin_values[1] - half_chunk_size_yards,
			origin_values[2]
		)
		var tile_coordinate = CoordinateMapperScript.canonical_to_adt_tile(chunk_center)
		var local_position = CoordinateMapperScript.canonical_to_adt_tile_local(chunk_center)
		var chunk_coordinate = CoordinateMapperScript.adt_tile_local_to_chunk(tile_coordinate, local_position)
		_expect_pair([tile_coordinate.tile_x, tile_coordinate.tile_y], chunk.get("adt_tile", null), "%s ADT tile" % name, failures)
		_expect_pair([chunk_coordinate.chunk_x, chunk_coordinate.chunk_y], chunk.get("mcnk_index", null), "%s MCNK index" % name, failures)
		var godot_center = CoordinateMapperScript.canonical_to_godot(chunk_center)
		_expect_three_near([godot_center.x_units, godot_center.y_units, godot_center.z_units], chunk.get("expected_godot_chunk_center", null), position_tolerance_yards, "%s Godot center" % name, failures)


func _verify_original_client_points(original_client_points: Array, render_manifest: Dictionary, position_tolerance_yards: float, failures: Array[String]) -> void:
	_expect_equal(original_client_points.size(), 5, "original-client point count", failures)
	var manifest_checkpoint_by_name: Dictionary = {}
	for checkpoint_variant in render_manifest.get("checkpoints", []):
		if checkpoint_variant is Dictionary:
			manifest_checkpoint_by_name[String(checkpoint_variant.get("name", ""))] = checkpoint_variant

	for point_variant in original_client_points:
		if not (point_variant is Dictionary):
			failures.append("original-client point must be an object")
			continue
		var point: Dictionary = point_variant
		var name := String(point.get("name", "original_client_point"))
		var client_position_values := _require_three_numbers(point.get("client_position", null), "%s client position" % name, failures)
		if client_position_values.is_empty():
			continue
		var canonical_position = CanonicalWowWorldPositionScript.new(client_position_values[0], client_position_values[1], client_position_values[2])
		var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
		_expect_three_near([godot_position.x_units, godot_position.y_units, godot_position.z_units], point.get("expected_godot_position", null), position_tolerance_yards, "%s mapped position" % name, failures)

		var manifest_checkpoint: Dictionary = manifest_checkpoint_by_name.get(name, {})
		_expect_true(not manifest_checkpoint.is_empty(), "%s exists in render manifest" % name, failures)
		if not manifest_checkpoint.is_empty():
			_expect_three_near(client_position_values, manifest_checkpoint.get("reference_wow_camera", null), position_tolerance_yards, "%s manifest client position" % name, failures)
			_expect_three_near(point.get("expected_godot_position", null), manifest_checkpoint.get("camera", null), position_tolerance_yards, "%s manifest Godot position" % name, failures)


func _require_three_numbers(value_variant, label: String, failures: Array[String]) -> Array[float]:
	if not (value_variant is Array) or value_variant.size() != 3:
		failures.append("%s must contain three numbers" % label)
		return []
	return [float(value_variant[0]), float(value_variant[1]), float(value_variant[2])]


func _expect_three_near(actual_values, expected_variant, tolerance: float, label: String, failures: Array[String]) -> void:
	var expected_values := _require_three_numbers(expected_variant, label, failures)
	if expected_values.is_empty() or not (actual_values is Array) or actual_values.size() != 3:
		return
	for component_index in range(3):
		_expect_near(float(actual_values[component_index]), expected_values[component_index], tolerance, "%s[%d]" % [label, component_index], failures)


func _expect_pair(actual_values: Array, expected_variant, label: String, failures: Array[String]) -> void:
	if not (expected_variant is Array) or expected_variant.size() != 2:
		failures.append("%s must contain two integers" % label)
		return
	_expect_equal(actual_values[0], int(expected_variant[0]), "%s[0]" % label, failures)
	_expect_equal(actual_values[1], int(expected_variant[1]), "%s[1]" % label, failures)


func _expect_near(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 %.9f, got %.9f" % [label, expected_value, actual_value])


func _expect_equal(actual_value, expected_value, 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)


func _load_json_object(path: String, failures: Array[String]) -> Dictionary:
	var file := FileAccess.open(path, FileAccess.READ)
	if file == null:
		failures.append("cannot open JSON fixture: %s" % path)
		return {}
	var parsed = JSON.parse_string(file.get_as_text())
	if not (parsed is Dictionary):
		failures.append("JSON fixture is not an object: %s" % path)
		return {}
	return parsed


func _report_and_quit(failures: Array[String]) -> void:
	if failures.is_empty():
		failures.append("fixture validation stopped without a diagnostic")
	for failure in failures:
		push_error("COORDINATE_GOLDEN_FIXTURES: %s" % failure)
	quit(1)
