extends SceneTree

var failures: Array[String] = []


func _initialize() -> void:
	call_deferred("_run")


func _run() -> void:
	var main_scene: Node = load("res://main.tscn").instantiate()
	root.add_child(main_scene)
	await process_frame
	await physics_frame
	await physics_frame

	var manager: Node = main_scene.get_node("SimulationManager")
	var view: Node = main_scene.get_node("WorldViewManager")
	manager.set_process(false)
	var npc: SimNPC = manager.npcs[0]
	var visual: Node3D = view.active_npc_visuals[npc.id]
	visual.set_physics_process(false)
	var carried_food: MeshInstance3D = visual.get_node("CarriedFood")
	_check(not carried_food.visible, "NPC should begin without a carried-food prop")
	npc.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
	manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 1.0)
	_check(carried_food.visible, "Carried food should be visible on the NPC")
	npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
	manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 0.0)
	_check(not carried_food.visible, "Deposited or eaten food should be hidden")

	var resource_state: ResourceStateRecord = manager.get_resource_state(&"berry_bush_01")
	npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
	npc.target_id = resource_state.get_node_id()
	npc.travel_target_position = Vector3(-6.0, 0.0, -8.0)
	npc.has_travel_target = true
	resource_state.reserve(npc.id)
	manager.request_current_travel(npc.id)

	var saved_action := npc.current_task
	var saved_target_id := npc.target_id
	var saved_target_position := npc.travel_target_position
	var authoritative_position := Vector3(-2.5, 0.5, 1.75)
	visual.global_position = authoritative_position

	_check(view.despawn_npc_visual(npc.id), "Visual should unload")
	await process_frame
	_check(
		npc.position.is_equal_approx(authoritative_position),
		"Unload should synchronize the last active position"
	)
	_check(
		(
			npc.current_task == saved_action
			and npc.target_id == saved_target_id
			and npc.travel_target_position.is_equal_approx(saved_target_position)
		),
		"Unload should preserve action and target state"
	)
	_check(
		resource_state.get_reserved_by() == npc.id,
		"Unload should preserve the resource reservation"
	)
	var unloaded_checksum: String = manager.get_state_checksum()

	_check(view.reload_npc_visual(npc.id), "Visual should reload")
	var reloaded: Node3D = view.active_npc_visuals[npc.id]
	reloaded.set_physics_process(false)
	_check(
		reloaded.global_position.is_equal_approx(authoritative_position),
		"Reloaded visual should use authoritative position"
	)
	var resumed_target: Vector3 = reloaded.get("current_target")
	_check(
		resumed_target.is_equal_approx(saved_target_position),
		"Reloaded visual should resume the persisted travel target"
	)
	_check(
		manager.get_state_checksum() == unloaded_checksum,
		"Loading a visual must not mutate simulation state"
	)
	_check(resource_state.get_reserved_by() == npc.id, "Reload should preserve the reservation")

	var moved_position := authoritative_position + Vector3(0.5, 0.0, 0.25)
	reloaded.emit_signal("position_changed", npc.id, moved_position)
	_check(
		npc.position.is_equal_approx(moved_position),
		"Active position callback should synchronize simulation state"
	)

	var saved_state: String = manager.serialize_state()
	var visual_before_restore: Node3D = view.active_npc_visuals[npc.id]
	npc.hunger = 99.0
	_check(
		manager.restore_state_from_json(saved_state),
		"Visual lifecycle scenario should restore valid state"
	)
	var visual_after_restore: Node3D = view.active_npc_visuals[npc.id]
	_check(
		visual_after_restore != visual_before_restore,
		"State restoration should rebuild active NPC presentation"
	)
	_check(
		visual_after_restore.global_position.is_equal_approx(npc.position),
		"Rebuilt presentation should use the restored authoritative position"
	)

	if failures.is_empty():
		print("[TEST] NPC visual lifecycle passed")
		quit(0)
		return
	for failure in failures:
		push_error("[TEST] " + failure)
	quit(1)


func _check(condition: bool, message: String) -> void:
	if not condition:
		failures.append(message)
