extends SceneTree

## Headless M02 camera state, scene wiring and collision-policy regression.

const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
const CAMERA_RIG_PATH := "res://src/scenes/player/third_person_camera_rig.gd"
const NO_COLLISION_POLICY_PATH := "res://src/scenes/player/no_camera_collision_policy.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",
]


class FixedDistanceCameraCollisionPolicy extends CameraCollisionPolicy:
	var _fixed_distance_units: float

	func _init(fixed_distance_units: float) -> void:
		_fixed_distance_units = fixed_distance_units

	func resolve_camera_distance(
			_camera_pivot: Node3D,
			_requested_distance_units: float
	) -> float:
		return _fixed_distance_units


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


func _run_verification() -> void:
	var failures: Array[String] = []
	var player := _instantiate_regression_player(failures)
	if player != null:
		_verify_initial_state(player, failures)
		_verify_zoom_clamps(player, failures)
		_verify_orbit_and_capture(player, failures)
		_verify_collision_policy(player, failures)
		player.free()
	_verify_source_boundaries(failures)
	_verify_scene_wiring(failures)

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

	print("THIRD_PERSON_CAMERA PASS state_cases=12 policy_cases=3 scenes=3 player_boundary=1")
	quit(0)


func _instantiate_regression_player(failures: Array[String]) -> CharacterBody3D:
	var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene
	_expect_true(packed_scene != null, "regression scene loads", failures)
	if packed_scene == null:
		return null
	var player := packed_scene.instantiate() as CharacterBody3D
	_expect_true(player != null, "regression player instantiates", failures)
	if player == null:
		return null
	root.add_child(player)
	player.set_physics_process(false)
	var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig
	camera_rig.set_physics_process(false)
	return player


func _verify_initial_state(player: CharacterBody3D, failures: Array[String]) -> void:
	var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig
	var camera := camera_rig.get_node("Camera3D") as Camera3D
	_expect_near(camera_rig.yaw_radians, 0.0, "initial yaw", failures)
	_expect_near(camera_rig.pitch_radians, deg_to_rad(-18.0), "initial pitch", failures)
	_expect_near(camera_rig.requested_distance_units, 8.0, "initial requested distance", failures)
	_expect_near(camera_rig.resolved_distance_units, 8.0, "initial resolved distance", failures)
	_expect_near(camera_rig.position.y, 1.7, "initial pivot height", failures)
	_expect_near(camera.position.z, 8.0, "initial camera local Z", failures)
	_expect_near(camera.far, 50000.0, "camera far plane", failures)
	_expect_true(camera_rig.godot_world_flight_movement_basis().is_equal_approx(camera_rig.global_basis), "flight basis is pivot basis", failures)


func _verify_zoom_clamps(player: CharacterBody3D, failures: Array[String]) -> void:
	var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig
	var zoom_in_event := _action_event(PlayerInputActions.CAMERA_ZOOM_IN, true)
	for _step in range(20):
		camera_rig.handle_camera_input(zoom_in_event)
	_expect_near(camera_rig.requested_distance_units, 2.0, "minimum zoom clamp", failures)
	for _step in range(30):
		camera_rig.handle_camera_input(_action_event(PlayerInputActions.CAMERA_ZOOM_OUT, true))
	_expect_near(camera_rig.requested_distance_units, 18.0, "maximum zoom clamp", failures)


func _verify_orbit_and_capture(player: CharacterBody3D, failures: Array[String]) -> void:
	var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig
	_expect_true(camera_rig.handle_camera_input(_action_event(PlayerInputActions.CAMERA_ROTATE, true)), "capture action consumed", failures)
	_expect_true(camera_rig.is_mouse_captured, "mouse capture state", failures)

	var mouse_motion := InputEventMouseMotion.new()
	mouse_motion.relative = Vector2(100.0, -50.0)
	_expect_true(camera_rig.handle_camera_input(mouse_motion), "captured mouse motion consumed", failures)
	_expect_near(camera_rig.yaw_radians, -0.3, "orbit yaw", failures)
	_expect_near(camera_rig.pitch_radians, deg_to_rad(-18.0) + 0.15, "orbit pitch", failures)
	_expect_near(player.rotation.y, -0.3, "character follows orbit yaw", failures)

	var clamp_up_motion := InputEventMouseMotion.new()
	clamp_up_motion.relative = Vector2(0.0, -100000.0)
	camera_rig.handle_camera_input(clamp_up_motion)
	_expect_near(camera_rig.pitch_radians, deg_to_rad(35.0), "maximum pitch clamp", failures)
	var clamp_down_motion := InputEventMouseMotion.new()
	clamp_down_motion.relative = Vector2(0.0, 100000.0)
	camera_rig.handle_camera_input(clamp_down_motion)
	_expect_near(camera_rig.pitch_radians, deg_to_rad(-65.0), "minimum pitch clamp", failures)

	camera_rig.handle_camera_input(_action_event(PlayerInputActions.CAMERA_ROTATE, false))
	_expect_true(not camera_rig.is_mouse_captured, "mouse release state", failures)


func _verify_collision_policy(player: CharacterBody3D, failures: Array[String]) -> void:
	var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig
	var camera := camera_rig.get_node("Camera3D") as Camera3D
	camera_rig.set_collision_policy(FixedDistanceCameraCollisionPolicy.new(3.0))
	_expect_near(camera_rig.resolved_distance_units, 3.0, "fixed policy distance", failures)
	_expect_near(camera.position.z, 3.0, "fixed policy camera transform", failures)
	camera_rig.set_collision_policy(NoCameraCollisionPolicy.new())
	_expect_near(camera_rig.resolved_distance_units, 18.0, "identity policy restores request", failures)


func _verify_source_boundaries(failures: Array[String]) -> void:
	var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
	for forbidden_text in ["var _captured", "var _yaw", "var _pitch", "func _apply_camera_transform", "camera_distance"]:
		_expect_true(not player_source.contains(forbidden_text), "player omits %s" % forbidden_text, failures)
	_expect_true(player_source.contains("_camera_rig.handle_camera_input"), "player delegates camera input", failures)

	var rig_source := _read_text(CAMERA_RIG_PATH, failures)
	_expect_true(rig_source.contains("extends Node3D"), "camera rig owns pivot Node3D", failures)
	for forbidden_text in ["ADTLoader", "TerrainQuery", "MoveIntent", "AnimationPlayer"]:
		_expect_true(not rig_source.contains(forbidden_text), "camera rig omits %s" % forbidden_text, failures)

	var no_collision_source := _read_text(NO_COLLISION_POLICY_PATH, failures)
	for forbidden_text in ["intersect_ray", "direct_space_state", "PhysicsRayQueryParameters3D"]:
		_expect_true(not no_collision_source.contains(forbidden_text), "baseline policy 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("third_person_camera_rig.gd"), "%s references camera rig" % scene_path, failures)
		_expect_true(scene_source.contains('script = ExtResource("4_camera_rig")') or scene_source.contains('script = ExtResource("2_camera_rig")'), "%s wires CameraPivot script" % scene_path, failures)


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


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)
