extends SceneTree

## Asset-free M02 appearance, animation and player-boundary regression.

const MODEL_FIXTURE_PATH := "res://src/tests/scenes/character_presentation_model_fixture.tscn"
const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
const APPEARANCE_PRESENTER_PATH := "res://src/scenes/character/character_appearance_presenter.gd"
const ANIMATION_PRESENTER_PATH := "res://src/scenes/character/character_animation_presenter.gd"
const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn"
const RUNTIME_SCENE_PATHS: Array[String] = [
	"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
	"res://src/scenes/streaming/kalimdor_streaming.tscn",
]


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


func _run_verification() -> void:
	var failures: Array[String] = []
	await _verify_appearance_presenter(failures)
	_verify_animation_presenter(failures)
	_verify_source_boundaries(failures)
	_verify_scene_wiring(failures)

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

	print("CHARACTER_PRESENTATION PASS appearance_cases=9 animation_cases=10 scenes=3 player_boundary=1")
	quit(0)


func _verify_appearance_presenter(failures: Array[String]) -> void:
	var presenter := CharacterAppearancePresenter.new()
	presenter.character_model_path = MODEL_FIXTURE_PATH
	presenter.character_scale = 2.0
	presenter.character_yaw_offset_degrees = 45.0
	root.add_child(presenter)
	var ready_roots: Array[Node3D] = []
	presenter.character_appearance_ready.connect(func(character_root: Node3D) -> void: ready_roots.append(character_root))

	_expect_true(presenter.load_character_appearance("res://missing-extracted-fixture"), "fixture load starts", failures)
	await process_frame
	var character_root := presenter.character_root
	_expect_true(character_root != null, "character root composed", failures)
	if character_root != null:
		_expect_near(character_root.scale.x, 2.0, "uniform character scale", failures)
		_expect_near(character_root.rotation_degrees.y, 45.0, "character yaw offset", failures)
		_expect_near(character_root.position.y, -2.0, "character grounded after scale", failures)
		_expect_true(character_root.has_node("CharacterTextureCompositor"), "texture compositor composed", failures)
	_expect_true(ready_roots.size() == 1, "appearance ready emitted", failures)
	_expect_true(presenter.extracted_data_directory == "res://missing-extracted-fixture", "content root override retained", failures)

	var previous_root := character_root
	_expect_true(presenter.load_character_appearance(), "replacement load starts", failures)
	_expect_true(presenter.character_root != previous_root, "replacement swaps owned root", failures)
	await process_frame
	_expect_true(ready_roots.size() == 2, "replacement ready emitted", failures)

	presenter.character_model_path = ""
	_expect_true(not presenter.load_character_appearance(), "empty model rejected", failures)
	_expect_true(presenter.character_root == null, "empty model clears old root", failures)
	await process_frame
	presenter.free()


func _verify_animation_presenter(failures: Array[String]) -> void:
	var animation_root := Node.new()
	var nested_node := Node.new()
	var animation_player := AnimationPlayer.new()
	animation_root.add_child(nested_node)
	nested_node.add_child(animation_player)
	var animation_library := AnimationLibrary.new()
	for animation_name in ["Stand", "Run", "Emote"]:
		animation_library.add_animation(animation_name, Animation.new())
	animation_player.add_animation_library("", animation_library)

	var presenter := CharacterAnimationPresenter.new()
	presenter.animation_blend_time_seconds = 0.25
	_expect_true(presenter.bind_character_root(animation_root), "nested animation player bound", failures)
	_expect_true(presenter.has_animation_player, "animation player state exposed", failures)
	_expect_true(presenter.active_animation_name == "Stand", "stationary animation selected", failures)
	_expect_true(animation_player.current_animation == "Stand", "stationary animation playing", failures)
	_expect_near(animation_player.playback_default_blend_time, 0.25, "default blend configured", failures)
	_expect_true(animation_player.get_animation("Stand").loop_mode == Animation.LOOP_LINEAR, "stand loops", failures)
	_expect_true(animation_player.get_animation("Run").loop_mode == Animation.LOOP_LINEAR, "run loops", failures)
	_expect_true(animation_player.get_animation("Emote").loop_mode == Animation.LOOP_NONE, "unrelated animation unchanged", failures)
	_expect_true(presenter.present_locomotion(true), "moving transition starts", failures)
	_expect_true(presenter.active_animation_name == "Run", "moving animation selected", failures)
	_expect_true(not presenter.present_locomotion(true), "duplicate moving state ignored", failures)

	var fallback_root := Node.new()
	var fallback_player := AnimationPlayer.new()
	fallback_root.add_child(fallback_player)
	var fallback_library := AnimationLibrary.new()
	fallback_library.add_animation("CharacterStandLoop", Animation.new())
	fallback_library.add_animation("FastRunCycle", Animation.new())
	fallback_player.add_animation_library("", fallback_library)
	_expect_true(presenter.bind_character_root(fallback_root), "fallback animation player bound", failures)
	_expect_true(presenter.active_animation_name == "CharacterStandLoop", "substring stationary fallback", failures)
	presenter.present_locomotion(true)
	_expect_true(presenter.active_animation_name == "FastRunCycle", "substring moving fallback", failures)
	fallback_root.free()
	_expect_true(not presenter.present_locomotion(false), "freed animation root rejected", failures)
	_expect_true(not presenter.has_animation_player, "freed animation binding cleared", failures)
	var missing_animation_root := Node.new()
	_expect_true(not presenter.bind_character_root(missing_animation_root), "missing animation player rejected", failures)
	_expect_true(not presenter.has_animation_player, "missing binding clears state", failures)
	presenter.free()
	animation_root.free()
	missing_animation_root.free()


func _verify_source_boundaries(failures: Array[String]) -> void:
	var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
	for forbidden_text in [
		"GEOSET_CONTROLLER_SCRIPT",
		"TEXTURE_COMPOSITOR_SCRIPT",
		"OUTFIT_RESOLVER_SCRIPT",
		"func _load_character_visual",
		"func _find_animation_player",
		"func _calculate_aabb",
		"_animation_player",
	]:
		_expect_true(not player_source.contains(forbidden_text), "player omits %s" % forbidden_text, failures)
	_expect_true(player_source.contains("_appearance_presenter.load_character_appearance"), "player delegates appearance", failures)
	_expect_true(player_source.contains("_animation_presenter.present_locomotion"), "player delegates animation", failures)

	var appearance_source := _read_text(APPEARANCE_PRESENTER_PATH, failures)
	for forbidden_text in ["MoveIntent", "TerrainQuery", "ThirdPersonCameraRig", "AnimationPlayer"]:
		_expect_true(not appearance_source.contains(forbidden_text), "appearance omits %s" % forbidden_text, failures)
	var animation_source := _read_text(ANIMATION_PRESENTER_PATH, failures)
	for forbidden_text in ["load(character_model_path)", "TerrainQuery", "MoveIntent", "Camera3D"]:
		_expect_true(not animation_source.contains(forbidden_text), "animation omits %s" % forbidden_text, failures)


func _verify_scene_wiring(failures: Array[String]) -> void:
	for scene_path in RUNTIME_SCENE_PATHS + [REGRESSION_SCENE_PATH]:
		var scene_source := _read_text(scene_path, failures)
		_expect_true(scene_source.contains("character_appearance_presenter.gd"), "%s references appearance presenter" % scene_path, failures)
		_expect_true(scene_source.contains("character_animation_presenter.gd"), "%s references animation presenter" % scene_path, failures)
		_expect_true(scene_source.contains('name="AnimationPresenter"'), "%s composes animation presenter" % scene_path, failures)


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_true(actual_value: bool, label: String, failures: Array[String]) -> void:
	if not actual_value:
		failures.append("%s expected true" % label)
