extends SceneTree

const BattleStateScript := preload("res://scripts/core/battle_state.gd")
const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd")


func _init() -> void:
	var failures: Array[String] = []
	_check_deferred_move_events(failures)
	_check_scene_post_move_menu_flow(failures)
	_check_scene_post_move_edge_positioning(failures)
	_check_scene_post_move_tactic_picker_flow(failures)
	_check_scene_post_move_item_picker_flow(failures)

	if failures.is_empty():
		print("post move action flow smoke ok")
		quit(0)
		return

	for failure in failures:
		push_error(failure)
	quit(1)


func _check_deferred_move_events(failures: Array[String]) -> void:
	var state = BattleStateScript.new()
	if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
		failures.append("could not load opening battle for pending move state")
		return

	var from_cell := Vector2i(1, 3)
	var to_cell := Vector2i(2, 3)
	state.battle_events.append({
		"id": "pending_move_gold",
		"once": true,
		"when": {
			"type": "unit_reaches_tile",
			"unit_ids": ["cao_cao"],
			"pos": [to_cell.x, to_cell.y]
		},
		"actions": [{"type": "grant_gold", "amount": 77}]
	})

	if not state.select_unit("cao_cao"):
		failures.append("could not select Cao Cao for pending move state")
		return
	if not state.try_move_selected(to_cell, true):
		failures.append("tentative move should succeed")
		return

	var cao_cao: Dictionary = state.get_unit("cao_cao")
	if cao_cao.get("pos", Vector2i.ZERO) != to_cell:
		failures.append("tentative move should update position")
	if not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("tentative move should mark moved but not acted")
	if state.get_battle_gold_reward() != 0:
		failures.append("tentative move should defer unit_reaches_tile rewards")

	if not state.cancel_pending_move("cao_cao", from_cell, to_cell):
		failures.append("pending move cancel should succeed")
	cao_cao = state.get_unit("cao_cao")
	if cao_cao.get("pos", Vector2i.ZERO) != from_cell or bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("pending move cancel should restore original action state")
	if state.get_battle_gold_reward() != 0:
		failures.append("canceled pending move should not fire movement event")

	if not state.try_move_selected(to_cell, true):
		failures.append("second tentative move should succeed")
	if not state.commit_pending_move_events("cao_cao"):
		failures.append("pending move commit should keep battle active")
	if state.get_battle_gold_reward() != 77:
		failures.append("pending move commit should fire deferred movement event")


func _check_scene_post_move_menu_flow(failures: Array[String]) -> void:
	var scene = BattleSceneScript.new()
	scene._create_hud()
	if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
		failures.append("could not load opening battle for scene flow")
		scene.free()
		return
	scene.battle_started = true
	scene.campaign_complete_screen = false
	scene.briefing_panel.visible = false
	scene.result_panel.visible = false
	scene._clear_pending_move_state()

	if not scene.state.select_unit("cao_cao"):
		failures.append("could not select Cao Cao for scene flow")
		scene.free()
		return
	scene._refresh_ranges()
	scene._handle_board_click(_screen_for_cell(Vector2i(2, 3)))

	var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
	if not scene._has_pending_move():
		failures.append("scene should keep pending move metadata after move click")
	if scene.post_move_menu == null or not scene.post_move_menu.visible:
		failures.append("post-move action menu should be visible after moving")
	else:
		_check_local_panel_position(failures, scene, scene.post_move_menu, Vector2i(2, 3), "post-move action menu")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("scene move should be pending with action still available")
	if scene.state.get_selected_unit().is_empty():
		failures.append("scene should keep moved unit selected for follow-up action")
	if not scene.attack_cells.is_empty():
		failures.append("attack cells should wait until Attack is chosen from the post-move menu")

	var right_click := InputEventMouseButton.new()
	right_click.button_index = MOUSE_BUTTON_RIGHT
	right_click.pressed = true
	right_click.position = _screen_for_cell(Vector2i(2, 3))
	scene._unhandled_input(right_click)
	cao_cao = scene.state.get_unit("cao_cao")
	if scene._has_pending_move() or (scene.post_move_menu != null and scene.post_move_menu.visible):
		failures.append("right-click cancel handler should clear pending move menu state")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(1, 3) or bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("right-click cancel handler should restore the starting cell")

	scene._refresh_ranges()
	scene._handle_board_click(_screen_for_cell(Vector2i(2, 3)))
	scene._on_post_move_wait_pressed()
	cao_cao = scene.state.get_unit("cao_cao")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or not bool(cao_cao.get("acted", false)):
		failures.append("post-move Wait should commit movement and end the unit action")
	if not scene.state.get_selected_unit().is_empty() or scene._has_pending_move():
		failures.append("post-move Wait should clear selection and pending metadata")

	_check_scene_post_move_attack_targeting(failures)

	scene.free()


func _check_scene_post_move_edge_positioning(failures: Array[String]) -> void:
	var scene = BattleSceneScript.new()
	scene._create_hud()
	if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
		failures.append("could not load opening battle for edge menu positioning")
		scene.free()
		return
	scene.battle_started = true
	scene.campaign_complete_screen = false
	scene.briefing_panel.visible = false
	scene.result_panel.visible = false
	scene._clear_pending_move_state()

	if not scene.state.select_unit("cao_cao"):
		failures.append("could not select Cao Cao for edge menu positioning")
		scene.free()
		return
	var edge_cell := Vector2i(scene.state.map_size.x - 1, 3)
	var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
	cao_cao["pos"] = edge_cell
	cao_cao["moved"] = true
	cao_cao["acted"] = false
	scene.pending_move_unit_id = "cao_cao"
	scene.pending_move_from_cell = Vector2i(edge_cell.x - 1, edge_cell.y)
	scene.pending_move_to_cell = edge_cell
	scene._show_post_move_menu()

	if scene.post_move_menu == null or not scene.post_move_menu.visible:
		failures.append("edge post-move action menu should be visible")
	else:
		_check_local_panel_position(failures, scene, scene.post_move_menu, edge_cell, "edge post-move action menu")

	scene.free()


func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void:
	var scene = BattleSceneScript.new()
	scene._create_hud()
	if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
		failures.append("could not load opening battle for attack targeting flow")
		scene.free()
		return
	scene.battle_started = true
	scene.campaign_complete_screen = false
	scene.briefing_panel.visible = false
	scene.result_panel.visible = false
	scene._clear_pending_move_state()

	var enemy: Dictionary = scene.state.get_unit("yellow_turban_1")
	enemy["pos"] = Vector2i(3, 3)
	enemy["hp"] = 24
	enemy["def"] = 1
	var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
	cao_cao["agi"] = 999
	cao_cao["atk"] = 60
	scene.state.rng.seed = 1

	scene.state.battle_events.append({
		"id": "pending_attack_gold",
		"once": true,
		"when": {
			"type": "unit_reaches_tile",
			"unit_ids": ["cao_cao"],
			"pos": [2, 3]
		},
		"actions": [{"type": "grant_gold", "amount": 33}]
	})

	if not scene.state.select_unit("cao_cao"):
		failures.append("could not select Cao Cao for attack targeting flow")
		scene.free()
		return
	scene._refresh_ranges()
	scene._handle_board_click(_screen_for_cell(Vector2i(2, 3)))
	if not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible:
		failures.append("attack targeting setup should leave the post-move menu visible")
	if scene.basic_attack_targeting:
		failures.append("attack targeting should be off before pressing Attack")
	if not scene.attack_cells.is_empty():
		failures.append("attack range should stay hidden before pressing Attack")
	if scene.state.get_battle_gold_reward() != 0:
		failures.append("attack setup should not commit pending move events early")

	scene._on_post_move_attack_pressed()
	if not scene.basic_attack_targeting:
		failures.append("Attack should enter target selection mode")
	if scene.post_move_menu != null and scene.post_move_menu.visible:
		failures.append("post-move menu should hide while selecting an attack target")
	if scene.targeting_hint_panel == null or not scene.targeting_hint_panel.visible:
		failures.append("targeting hint panel should be visible during attack targeting")
	if not scene.attack_cells.has(Vector2i(3, 3)):
		failures.append("attack targeting should highlight the adjacent enemy cell")
	var markers := scene._attack_target_marker_entries()
	if markers.size() != 1 or markers[0].get("cell", Vector2i.ZERO) != Vector2i(3, 3):
		failures.append("attack target markers should include only the reachable enemy: %s" % str(markers))

	scene._on_targeting_back_pressed()
	cao_cao = scene.state.get_unit("cao_cao")
	if scene.basic_attack_targeting or not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible:
		failures.append("Back should return to the post-move action menu")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("Back should preserve the pending moved unit without acting")

	scene._on_post_move_attack_pressed()
	var enemy_hp_before := int(enemy.get("hp", 0))
	scene._handle_board_click(_screen_for_cell(Vector2i(3, 3)))
	cao_cao = scene.state.get_unit("cao_cao")
	enemy = scene.state.get_unit("yellow_turban_1")
	if scene._has_pending_move() or scene.basic_attack_targeting:
		failures.append("attack click should clear pending move and targeting state")
	if not scene.state.get_selected_unit().is_empty():
		failures.append("attack click should clear selected unit after acting")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or not bool(cao_cao.get("acted", false)):
		failures.append("attack click should commit the moved attacker action")
	if int(enemy.get("hp", 0)) >= enemy_hp_before and bool(enemy.get("alive", true)):
		failures.append("attack click should damage or defeat the target")
	if scene.state.get_battle_gold_reward() != 33:
		failures.append("attack click should commit deferred movement event before resolving attack")

	scene.free()


func _check_scene_post_move_tactic_picker_flow(failures: Array[String]) -> void:
	var scene = BattleSceneScript.new()
	scene._create_hud()
	if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
		failures.append("could not load opening battle for tactic picker flow")
		scene.free()
		return
	scene.battle_started = true
	scene.campaign_complete_screen = false
	scene.briefing_panel.visible = false
	scene.result_panel.visible = false
	scene._clear_pending_move_state()

	var enemy: Dictionary = scene.state.get_unit("yellow_turban_1")
	enemy["pos"] = Vector2i(3, 3)
	enemy["hp"] = 28
	enemy["def"] = 1
	var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
	cao_cao["int"] = 80
	cao_cao["mp"] = 10

	scene.state.battle_events.append({
		"id": "pending_tactic_gold",
		"once": true,
		"when": {
			"type": "unit_reaches_tile",
			"unit_ids": ["cao_cao"],
			"pos": [2, 3]
		},
		"actions": [{"type": "grant_gold", "amount": 44}]
	})

	if not scene.state.select_unit("cao_cao"):
		failures.append("could not select Cao Cao for tactic picker flow")
		scene.free()
		return
	scene._refresh_ranges()
	scene._handle_board_click(_screen_for_cell(Vector2i(2, 3)))
	if scene.post_move_tactic_button == null or scene.post_move_tactic_button.disabled:
		failures.append("post-move Tactic should be enabled when Spark has a valid target")

	scene._on_tactic_pressed()
	if scene.post_move_picker_panel == null or not scene.post_move_picker_panel.visible or scene.post_move_picker_mode != "tactic":
		failures.append("post-move Tactic shortcut should open the local tactic picker")
	if scene.post_move_menu != null and scene.post_move_menu.visible:
		failures.append("post-move action menu should hide while the tactic picker is open")
	if scene.tactic_menu != null and scene.tactic_menu.visible:
		failures.append("post-move tactic shortcut should not use the side tactic menu")

	var escape := InputEventKey.new()
	escape.keycode = KEY_ESCAPE
	scene._handle_key(escape)
	cao_cao = scene.state.get_unit("cao_cao")
	if scene.post_move_picker_panel != null and scene.post_move_picker_panel.visible:
		failures.append("Escape should close the local tactic picker")
	if not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible:
		failures.append("Escape from the tactic picker should return to the post-move action menu")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("Escape from the tactic picker should preserve the pending move")

	scene._on_post_move_tactic_pressed()
	scene._on_post_move_tactic_option_pressed("spark")
	if scene.selected_skill_id != "spark":
		failures.append("choosing Spark should enter tactic targeting mode")
	if scene.post_move_picker_panel != null and scene.post_move_picker_panel.visible:
		failures.append("tactic picker should hide after choosing a tactic")
	if scene.targeting_hint_panel == null or not scene.targeting_hint_panel.visible:
		failures.append("targeting hint panel should be visible during tactic targeting")
	if not scene.skill_cells.has(Vector2i(3, 3)):
		failures.append("Spark targeting should highlight the adjacent enemy cell")
	var tactic_markers := scene._skill_target_marker_entries()
	if tactic_markers.size() != 1 or tactic_markers[0].get("cell", Vector2i.ZERO) != Vector2i(3, 3):
		failures.append("tactic target markers should include only the Spark target: %s" % str(tactic_markers))
	else:
		if str(tactic_markers[0].get("text", "")) != "KO":
			failures.append("tactic target marker should preview the deterministic KO: %s" % str(tactic_markers[0]))
		if str(tactic_markers[0].get("kind", "")) != "damage":
			failures.append("tactic target marker should use damage color: %s" % str(tactic_markers[0]))

	scene._on_targeting_back_pressed()
	cao_cao = scene.state.get_unit("cao_cao")
	if not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible:
		failures.append("tactic Back should return to the post-move action menu")
	if scene.selected_skill_id != "":
		failures.append("tactic Back should clear the selected tactic")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("tactic Back should preserve the pending moved unit without acting")

	scene._on_post_move_tactic_pressed()
	scene._on_post_move_tactic_option_pressed("spark")
	var enemy_hp_before := int(enemy.get("hp", 0))
	scene._handle_board_click(_screen_for_cell(Vector2i(3, 3)))
	cao_cao = scene.state.get_unit("cao_cao")
	enemy = scene.state.get_unit("yellow_turban_1")
	if scene._has_pending_move() or scene.selected_skill_id != "":
		failures.append("tactic target click should clear pending move and selected tactic")
	if not scene.state.get_selected_unit().is_empty():
		failures.append("tactic target click should clear selected unit after acting")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or not bool(cao_cao.get("acted", false)):
		failures.append("tactic target click should commit the moved caster action")
	if int(enemy.get("hp", 0)) >= enemy_hp_before and bool(enemy.get("alive", true)):
		failures.append("tactic target click should damage or defeat the target")
	if scene.state.get_battle_gold_reward() != 44:
		failures.append("tactic target click should commit deferred movement event before resolving tactic")

	scene.free()


func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void:
	var scene = BattleSceneScript.new()
	scene._create_hud()
	if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
		failures.append("could not load opening battle for item picker flow")
		scene.free()
		return
	scene.battle_started = true
	scene.campaign_complete_screen = false
	scene.briefing_panel.visible = false
	scene.result_panel.visible = false
	scene._clear_pending_move_state()
	scene.state.set_inventory_snapshot({"bean": 1})

	var ally: Dictionary = scene.state.get_unit("xiahou_dun")
	ally["pos"] = Vector2i(2, 4)
	ally["hp"] = 12
	var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
	cao_cao["mp"] = 10

	scene.state.battle_events.append({
		"id": "pending_item_gold",
		"once": true,
		"when": {
			"type": "unit_reaches_tile",
			"unit_ids": ["cao_cao"],
			"pos": [2, 3]
		},
		"actions": [{"type": "grant_gold", "amount": 55}]
	})

	if not scene.state.select_unit("cao_cao"):
		failures.append("could not select Cao Cao for item picker flow")
		scene.free()
		return
	scene._refresh_ranges()
	scene._handle_board_click(_screen_for_cell(Vector2i(2, 3)))
	if scene.post_move_item_button == null or scene.post_move_item_button.disabled:
		failures.append("post-move Item should be enabled when Bean can heal an adjacent ally")

	scene._on_item_pressed()
	if scene.post_move_picker_panel == null or not scene.post_move_picker_panel.visible or scene.post_move_picker_mode != "item":
		failures.append("post-move Item shortcut should open the local item picker")
	if scene.post_move_menu != null and scene.post_move_menu.visible:
		failures.append("post-move action menu should hide while the item picker is open")
	if scene.item_menu != null and scene.item_menu.visible:
		failures.append("post-move item shortcut should not use the side item menu")

	scene._on_post_move_item_option_pressed("bean")
	if scene.selected_item_id != "bean":
		failures.append("choosing Bean should enter item targeting mode")
	if scene.post_move_picker_panel != null and scene.post_move_picker_panel.visible:
		failures.append("item picker should hide after choosing an item")
	if scene.targeting_hint_panel == null or not scene.targeting_hint_panel.visible:
		failures.append("targeting hint panel should be visible during item targeting")
	if not scene.item_cells.has(Vector2i(2, 4)):
		failures.append("Bean targeting should highlight the adjacent injured ally")
	var item_markers := scene._item_target_marker_entries()
	if item_markers.size() != 1 or item_markers[0].get("cell", Vector2i.ZERO) != Vector2i(2, 4):
		failures.append("item target markers should include only the Bean target: %s" % str(item_markers))
	else:
		if str(item_markers[0].get("text", "")) != "+20 HP":
			failures.append("item target marker should preview healing: %s" % str(item_markers[0]))
		if str(item_markers[0].get("kind", "")) != "heal":
			failures.append("item target marker should use heal color: %s" % str(item_markers[0]))

	scene._on_targeting_back_pressed()
	cao_cao = scene.state.get_unit("cao_cao")
	if not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible:
		failures.append("item Back should return to the post-move action menu")
	if scene.selected_item_id != "":
		failures.append("item Back should clear the selected item")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)):
		failures.append("item Back should preserve the pending moved unit without acting")

	scene._on_post_move_item_pressed()
	scene._on_post_move_item_option_pressed("bean")
	var ally_hp_before := int(ally.get("hp", 0))
	scene._handle_board_click(_screen_for_cell(Vector2i(2, 4)))
	cao_cao = scene.state.get_unit("cao_cao")
	ally = scene.state.get_unit("xiahou_dun")
	if scene._has_pending_move() or scene.selected_item_id != "":
		failures.append("item target click should clear pending move and selected item")
	if not scene.state.get_selected_unit().is_empty():
		failures.append("item target click should clear selected unit after acting")
	if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or not bool(cao_cao.get("acted", false)):
		failures.append("item target click should commit the moved user action")
	if int(ally.get("hp", 0)) <= ally_hp_before:
		failures.append("item target click should heal the selected ally")
	if int(scene.state.get_inventory_snapshot().get("bean", 0)) != 0:
		failures.append("item target click should consume the Bean")
	if scene.state.get_battle_gold_reward() != 55:
		failures.append("item target click should commit deferred movement event before resolving item")

	scene.free()


func _check_local_panel_position(failures: Array[String], scene, panel: Control, cell: Vector2i, label: String) -> void:
	var padding := BattleSceneScript.LOCAL_COMMAND_PANEL_BOARD_PADDING
	var board_position := BattleSceneScript.BOARD_OFFSET + Vector2(padding, padding)
	var board_size := Vector2(scene.state.map_size.x, scene.state.map_size.y) * BattleSceneScript.TILE_SIZE - Vector2(padding * 2.0, padding * 2.0)
	var board_rect := Rect2(board_position, board_size)
	var panel_rect := Rect2(panel.position, panel.size)
	var cell_rect: Rect2 = scene._rect_for_cell(cell).grow(2.0)
	if panel_rect.size.x <= 0.0 or panel_rect.size.y <= 0.0:
		failures.append("%s should have a stable visible size" % label)
	if panel_rect.position.x < board_rect.position.x or panel_rect.position.y < board_rect.position.y:
		failures.append("%s should stay inside the board minimum: %s" % [label, str(panel_rect)])
	if panel_rect.end.x > board_rect.end.x or panel_rect.end.y > board_rect.end.y:
		failures.append("%s should stay inside the board maximum: %s" % [label, str(panel_rect)])
	if panel_rect.intersects(cell_rect):
		failures.append("%s should not cover the moved unit cell: %s over %s" % [label, str(panel_rect), str(cell_rect)])


func _screen_for_cell(cell: Vector2i) -> Vector2:
	return BattleSceneScript.BOARD_OFFSET + Vector2(cell.x, cell.y) * BattleSceneScript.TILE_SIZE + Vector2.ONE * (BattleSceneScript.TILE_SIZE * 0.5)
