extends SceneTree

## Headless M02 profile/capability gate regression for debug player movement.

const MOVEMENT_CAPABILITIES_PATH := "res://src/gameplay/movement/player_movement_capabilities.gd"
const MOVEMENT_CONTROLLER_PATH := "res://src/gameplay/movement/local_player_movement_controller.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"


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


func _run_verification() -> void:
	var failures: Array[String] = []
	_verify_capability_profiles(failures)
	_verify_movement_gate(failures)
	_verify_real_scene_profiles(failures)
	_verify_source_boundary(failures)

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

	print("PLAYER_MOVEMENT_CAPABILITIES PASS profiles=3 sprint=2 flight=2 scenes=2 boundary=1")
	quit(0)


func _verify_capability_profiles(failures: Array[String]) -> void:
	var render_sandbox := PlayerMovementCapabilities.render_sandbox()
	_expect_true(render_sandbox.profile_id == PlayerMovementCapabilities.RENDER_SANDBOX_PROFILE_ID, "sandbox profile identity", failures)
	_expect_true(render_sandbox.allows_debug_sprint, "sandbox sprint capability", failures)
	_expect_true(render_sandbox.allows_debug_free_flight, "sandbox flight capability", failures)

	var blizzlike := PlayerMovementCapabilities.blizzlike_335()
	_expect_true(blizzlike.profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "blizzlike profile identity", failures)
	_expect_true(not blizzlike.allows_debug_sprint, "blizzlike rejects sprint", failures)
	_expect_true(not blizzlike.allows_debug_free_flight, "blizzlike rejects flight", failures)

	var unknown_profile := PlayerMovementCapabilities.for_profile_id(&"UnknownProfile")
	_expect_true(unknown_profile.profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "unknown profile fails closed", failures)
	_expect_true(not unknown_profile.allows_debug_sprint, "unknown profile rejects sprint", failures)
	_expect_true(not unknown_profile.allows_debug_free_flight, "unknown profile rejects flight", failures)


func _verify_movement_gate(failures: Array[String]) -> void:
	var sprint_intent := MoveIntent.new(1.0, 0.0, 0.0, true)
	var sandbox_controller := _new_controller(PlayerMovementCapabilities.render_sandbox())
	sandbox_controller.advance(sprint_intent, Basis.IDENTITY, 2.0)
	_expect_vector_near(sandbox_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -42.0), "sandbox sprint applied", failures)
	_expect_true(sandbox_controller.toggle_sandbox_flight(), "sandbox flight enabled", failures)

	var blizzlike_controller := _new_controller(PlayerMovementCapabilities.blizzlike_335())
	blizzlike_controller.advance(sprint_intent, Basis.IDENTITY, 2.0)
	_expect_vector_near(blizzlike_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -7.0), "blizzlike sprint ignored", failures)
	_expect_true(not blizzlike_controller.toggle_sandbox_flight(), "blizzlike flight rejected", failures)
	_expect_true(not blizzlike_controller.is_flight_enabled, "blizzlike remains grounded", failures)

	var safe_default_controller := LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0)
	_expect_true(safe_default_controller.movement_profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "missing capability defaults safe", failures)


func _verify_real_scene_profiles(failures: Array[String]) -> void:
	var sandbox_player := _instantiate_player("RenderSandbox", failures)
	if sandbox_player != null:
		_press_sprint_forward_for_two_seconds(sandbox_player)
		var sandbox_movement: LocalPlayerMovementController = sandbox_player.get("_local_movement_controller")
		_expect_vector_near(sandbox_movement.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -42.0), "sandbox scene sprint", failures)
		sandbox_player.call("_unhandled_input", _action_event(PlayerInputActions.DEBUG_TOGGLE_FLIGHT))
		_expect_true(sandbox_movement.is_flight_enabled, "sandbox scene flight", failures)
		sandbox_player.free()

	var blizzlike_player := _instantiate_player("Blizzlike335", failures)
	if blizzlike_player != null:
		_press_sprint_forward_for_two_seconds(blizzlike_player)
		var blizzlike_movement: LocalPlayerMovementController = blizzlike_player.get("_local_movement_controller")
		_expect_vector_near(blizzlike_movement.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -7.0), "blizzlike scene sprint rejected", failures)
		blizzlike_player.call("_unhandled_input", _action_event(PlayerInputActions.DEBUG_TOGGLE_FLIGHT))
		_expect_true(not blizzlike_movement.is_flight_enabled, "blizzlike scene flight rejected", failures)
		blizzlike_player.free()


func _instantiate_player(profile_id: String, failures: Array[String]) -> CharacterBody3D:
	var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene
	if packed_scene == null:
		failures.append("regression scene loads")
		return null
	var player := packed_scene.instantiate() as CharacterBody3D
	if player == null:
		failures.append("regression player instantiates")
		return null
	player.set("movement_profile_id", profile_id)
	root.add_child(player)
	player.set_physics_process(false)
	return player


func _press_sprint_forward_for_two_seconds(player: CharacterBody3D) -> void:
	Input.action_press(PlayerInputActions.MOVE_FORWARD)
	Input.action_press(PlayerInputActions.DEBUG_SPRINT)
	player.call("_physics_process", 2.0)
	Input.action_release(PlayerInputActions.DEBUG_SPRINT)
	Input.action_release(PlayerInputActions.MOVE_FORWARD)


func _action_event(action_name: StringName) -> InputEventAction:
	var input_event := InputEventAction.new()
	input_event.action = action_name
	input_event.pressed = true
	return input_event


func _verify_source_boundary(failures: Array[String]) -> void:
	var capability_source := _read_text(MOVEMENT_CAPABILITIES_PATH, failures)
	for forbidden_text in ["extends Node", "extends Resource", "Input.", "CharacterBody3D"]:
		_expect_true(not capability_source.contains(forbidden_text), "capability omits %s" % forbidden_text, failures)
	_expect_true(not capability_source.contains("\n\tset:"), "capability exposes no property setters", failures)
	var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures)
	_expect_true(movement_source.contains("allows_debug_sprint"), "movement gates sprint capability", failures)
	_expect_true(movement_source.contains("allows_debug_free_flight"), "movement gates flight capability", failures)
	var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
	_expect_true(player_source.contains("for_profile_id(StringName(movement_profile_id))"), "scene maps profile once", failures)
	_expect_true(not player_source.contains('movement_profile_id == "'), "scene omits profile branches", failures)


func _new_controller(capabilities: PlayerMovementCapabilities) -> LocalPlayerMovementController:
	return LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0, capabilities)


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_vector_near(actual_value: Vector3, expected_value: Vector3, label: String, failures: Array[String]) -> void:
	if not actual_value.is_equal_approx(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)
