## Netcode Bug Bash — Automated Netcode Test Suite
##
## Tests connection lifecycle, prediction & reconciliation, round lifecycle,
## economy & weapons, and RCON functionality — all without real ENet traffic
## by calling internal APIs directly.
##
## Usage:
##   godot --headless --script tests/netcode/netcode_bugbash.gd
##
## Exit code: 0 = all pass, 1 = any failure

extends Node

# Preload TeamData for enum access (it's a class_name Resource, not an autoload)
const TeamData = preload("res://scripts/teams/team_data.gd")

# ---------------------------------------------------------------------------
# Test counters
# ---------------------------------------------------------------------------
var _passed: int = 0
var _failed: int = 0
var _total: int = 0

# ---------------------------------------------------------------------------
# Test infrastructure references
# ---------------------------------------------------------------------------
var _game_server: Node = null
var _world_3d: World3D = null
var _net_mgr: Node = null

# ---------------------------------------------------------------------------
# Test helpers
# ---------------------------------------------------------------------------
func check_pass(name: String) -> void:
	_passed += 1
	_total += 1
	print("  ✓  PASS: %s" % name)

func check_fail(name: String, reason: String) -> void:
	_failed += 1
	_total += 1
	print("  ✗  FAIL: %s — %s" % [name, reason])

func assert_eq(name: String, got, expected, msg: String = "") -> void:
	if got == expected:
		check_pass(name)
	else:
		var extra: String = ": " + msg if msg else ""
		check_fail(name, "expected %s, got %s%s" % [str(expected), str(got), extra])

func assert_true(name: String, cond: bool, msg: String = "") -> void:
	if cond:
		check_pass(name)
	else:
		var extra: String = ": " + msg if msg else ""
		check_fail(name, "condition was false%s" % [extra])

func assert_false(name: String, cond: bool, msg: String = "") -> void:
	if not cond:
		check_pass(name)
	else:
		var extra: String = ": " + msg if msg else ""
		check_fail(name, "condition was true%s" % [extra])

# ---------------------------------------------------------------------------
# Infrastructure setup
# ---------------------------------------------------------------------------
func _setup_infrastructure() -> void:
	# Create NetworkManager manually (autoloads not loaded in --script mode)
	var nm_script = load("res://scripts/network/network_manager.gd")
	_net_mgr = nm_script.new()
	add_child(_net_mgr)
	print("  Created NetworkManager node")

	# Create a 3D physics world so physics-dependent subsystems work
	_world_3d = World3D.new()
	get_window().world_3d = _world_3d

	# Add a static body so raycasts have something to hit
	var sb := StaticBody3D.new()
	var col := CollisionShape3D.new()
	var box := BoxShape3D.new()
	box.size = Vector3(10, 10, 10)
	col.shape = box
	sb.add_child(col)
	add_child(sb)
	sb.global_position = Vector3(0, 0, -20)

	# Create GameServer with all subsystems
	var gs_script = load("res://server/scripts/game_server.gd")
	_game_server = gs_script.new()
	add_child(_game_server)

	# Manually ensure the simulation server is running
	if _game_server.has_method("start_simulation"):
		_game_server.start_simulation()

	print("  Setup complete: GameServer + all subsystems created")
	print("")

# ---------------------------------------------------------------------------
# Main entry
# ---------------------------------------------------------------------------
func _ready() -> void:
	print("")
	print("=".repeat(64))
	print("  Netcode Bug Bash — Automated Test Suite")
	print("=".repeat(64))
	print("")

	var vi = Engine.get_version_info()
	print("  Engine:   Godot %d.%d.%d" % [vi.major, vi.minor, vi.patch])
	print("  Platform: %s" % OS.get_name())
	print("  Headless: %s" % str(DisplayServer.get_name() == "headless"))
	print("")

	_setup_infrastructure()

	# === GROUP 1: Connection Lifecycle (5 tests) ===
	print("─".repeat(64))
	print("  GROUP 1: Connection Lifecycle")
	print("─".repeat(64))
	test_conn_client_connects()
	test_conn_client_disconnect_cleanup()
	test_conn_client_reconnect_fresh()
	test_conn_full_server_rejected()
	test_conn_invalid_data_no_crash()

	# === GROUP 2: Prediction & Reconciliation (4 tests) ===
	print("")
	print("─".repeat(64))
	print("  GROUP 2: Prediction & Reconciliation")
	print("─".repeat(64))
	test_pred_client_input_processed()
	test_pred_server_snapshot_reconcile()
	test_pred_lag_compensation_rewind()
	test_pred_packet_loss_recovery()

	# === GROUP 3: Round Lifecycle (3 tests) ===
	print("")
	print("─".repeat(64))
	print("  GROUP 3: Round Lifecycle")
	print("─".repeat(64))
	test_round_transitions()
	test_round_bomb_explosion()
	test_round_bomb_defuse()

	# === GROUP 4: Economy & Weapons (3 tests) ===
	print("")
	print("─".repeat(64))
	print("  GROUP 4: Economy & Weapons")
	print("─".repeat(64))
	test_econ_kill_reward()
	test_econ_round_win_loss()
	test_econ_weapon_purchase()

	# === GROUP 5: RCON (3 tests) ===
	print("")
	print("─".repeat(64))
	print("  GROUP 5: RCON")
	print("─".repeat(64))
	test_rcon_auth_command()
	test_rcon_invalid_password()
	test_rcon_start_match_end_round()

	# === SUMMARY ===
	_print_summary()
	get_tree().quit(0 if _failed == 0 else 1)

func _print_summary() -> void:
	print("")
	print("=".repeat(64))
	print("  RESULTS: %d / %d passed, %d failed" % [_passed, _total, _failed])
	print("=".repeat(64))
	if _failed == 0:
		print("  ALL TESTS PASSED")
	else:
		print("  SOME TESTS FAILED — see above for details")
	print("=".repeat(64))

# ===================================================================
# GROUP 1: Connection Lifecycle
# ===================================================================

func test_conn_client_connects() -> void:
	if not _net_mgr or not _net_mgr.has_signal("player_connected"):
		check_fail("conn_01_client_connects", "NetworkManager not available")
		return

	var err = _net_mgr.start_server(0)
	if err != OK:
		check_fail("conn_01_client_connects", "Failed to start server: " + error_string(err))
		return

	assert_true("conn_01_server_started", _net_mgr.is_server,
		"NetworkManager should be in server mode after start_server")
	_net_mgr.stop()

func test_conn_client_disconnect_cleanup() -> void:
	if _game_server == null:
		check_fail("conn_02_disconnect_cleanup", "GameServer not available")
		return

	var gs = _game_server
	if not gs.is_running:
		gs.start_simulation()

	var entity_id: int = gs.spawn_player_entity(1001, Vector3(2, 0, 3))
	assert_true("conn_02_spawned", entity_id >= 0, "Player should spawn successfully")

	var peer = gs.get_peer_for_entity(entity_id)
	assert_eq("conn_02_entity_to_peer_mapped", peer, 1001,
		"entity_to_peer should map to peer_id=1001")

	# Despawn (simulating disconnect)
	gs.despawn_player_entity(entity_id)

	var peer_after = gs.get_peer_for_entity(entity_id)
	assert_eq("conn_02_entity_cleaned", peer_after, -1,
		"entity_to_peer should be cleared after despawn")

func test_conn_client_reconnect_fresh() -> void:
	if _game_server == null:
		check_fail("conn_03_reconnect_fresh", "GameServer not available")
		return

	var gs = _game_server

	var eid1: int = gs.spawn_player_entity(2001, Vector3(5, 0, 5))
	gs.despawn_player_entity(eid1)

	var eid2: int = gs.spawn_player_entity(2001, Vector3(10, 0, 10))
	assert_true("conn_03_re_spawned", eid2 >= 0, "Re-spawning should succeed")
	assert_true("conn_03_new_entity_id", eid2 != eid1, "Re-spawn should get a new entity_id")

	var old_peer = gs.get_peer_for_entity(eid1)
	assert_eq("conn_03_old_entity_cleaned", old_peer, -1, "Old entity_id should be unmapped")
	gs.despawn_player_entity(eid2)

func test_conn_full_server_rejected() -> void:
	if not _net_mgr:
		check_fail("conn_04_full_server", "NetworkManager not available")
		return

	_net_mgr.max_clients = 1
	var err = _net_mgr.start_server(0)
	if err != OK:
		check_fail("conn_04_full_server", "Failed to start server: " + error_string(err))
		return

	assert_true("conn_04_max_clients_set", _net_mgr.max_clients == 1,
		"max_clients should be 1")
	_net_mgr.stop()

func test_conn_invalid_data_no_crash() -> void:
	if _game_server == null:
		check_fail("conn_05_invalid_data", "GameServer not available")
		return

	var gs = _game_server
	var sim = gs.get_simulation_server()

	if sim and sim.has_method("apply_input"):
		sim.apply_input(-999, {"move_direction": "not_a_vector"})
		check_pass("conn_05_invalid_entity_input")

		var eid = gs.spawn_player_entity(3001, Vector3.ZERO)
		sim.apply_input(eid, {})
		check_pass("conn_05_empty_input_dict")
		gs.despawn_player_entity(eid)
	else:
		check_fail("conn_05_invalid_data", "Simulation server missing apply_input method")

	var bad_eid = gs.spawn_player_entity(3002, Vector3(NAN, NAN, NAN))
	gs.despawn_player_entity(bad_eid)
	check_pass("conn_05_nan_position")

# ===================================================================
# GROUP 2: Prediction & Reconciliation
# ===================================================================

func test_pred_client_input_processed() -> void:
	if _game_server == null:
		check_fail("pred_01_input_processed", "GameServer not available")
		return

	var gs = _game_server
	var sim = gs.get_simulation_server()

	# Test simulation_server.apply_input directly
	if sim and sim.has_method("apply_input"):
		var eid = gs.spawn_player_entity(4001, Vector3(0, 0, 0))
		sim.apply_input(eid, {"move_direction": Vector3(1, 0, 0), "sprint": false})
		check_pass("pred_01_apply_input_direct")
		gs.despawn_player_entity(eid)
	else:
		check_fail("pred_01_input_processed", "Simulation server missing apply_input")

func test_pred_server_snapshot_reconcile() -> void:
	var snapshot_script = load("res://scripts/network/snapshot.gd")
	var snap = snapshot_script.new()
	snap.timestamp = 42
	snap.position = Vector3(10, 5, 3)
	snap.rotation = Quaternion.IDENTITY
	snap.velocity = Vector3(2, 0, 1)
	snap.grounded = true

	var data = snap.to_dict()
	assert_eq("pred_02_serialize_timestamp", data.get("timestamp"), 42)
	assert_eq("pred_02_serialize_position", data.get("position"), [10.0, 5.0, 3.0])
	assert_eq("pred_02_serialize_grounded", data.get("grounded"), true)

	var restored = snapshot_script.from_dict(data)
	assert_eq("pred_02_deserialize_timestamp", restored.timestamp, 42)
	assert_eq("pred_02_deserialize_position", restored.position, Vector3(10, 5, 3))
	assert_eq("pred_02_deserialize_grounded", restored.grounded, true)

	# Test ClientPrediction ring buffer
	var cp_script = load("res://scripts/network/client_prediction.gd")
	var cp = cp_script.new()
	cp._snapshot_buffer.resize(64)

	var test_snap = snapshot_script.new()
	test_snap.timestamp = 5
	test_snap.position = Vector3(1, 2, 3)
	cp._snapshot_buffer[5 % 64] = test_snap
	cp._pending_inputs[5] = {"move_direction": Vector3(0, 0, -1)}

	assert_true("pred_02_buffer_has_snapshot",
		cp._snapshot_buffer[5 % 64] != null,
		"Snapshot should be stored in ring buffer")
	check_pass("pred_02_buffer_and_serialization_ok")

func test_pred_lag_compensation_rewind() -> void:
	if _game_server == null:
		check_fail("pred_03_lag_comp", "GameServer not available")
		return

	var lc = _game_server.lag_compensation
	if lc == null:
		check_fail("pred_03_lag_comp", "LagCompensation not wired in GameServer")
		return

	var test_node := Node3D.new()
	test_node.global_position = Vector3(10, 0, 20)
	add_child(test_node)

	var entity_id = 9999
	lc.register_player_node(entity_id, test_node)
	lc.record_tick(0)

	test_node.global_position = Vector3(30, 0, 40)
	lc.record_tick(1)

	# Move node away for rewind test
	test_node.global_position = Vector3(50, 0, 60)

	var origin := Vector3(0, 0, 0)
	var direction := Vector3(0, 0, -1).normalized()
	var result = lc.rewind_and_raycast(0, origin, direction, 100.0, [])

	assert_true("pred_03_rewind_no_crash", true,
		"rewind_and_raycast completed without error")

	var pos_after: Vector3 = test_node.global_position
	assert_eq("pred_03_node_restored", pos_after, Vector3(50, 0, 60),
		"Node position should be restored after rewind")

	lc.unregister_player_node(entity_id)
	test_node.queue_free()

func test_pred_packet_loss_recovery() -> void:
	var cp_script = load("res://scripts/network/client_prediction.gd")
	var snapshot_script = load("res://scripts/network/snapshot.gd")
	var cp = cp_script.new()
	cp._snapshot_buffer.resize(64)

	for t in range(5):
		var snap = snapshot_script.new()
		snap.timestamp = t
		snap.position = Vector3(t * 2.0, 0, 0)
		snap.rotation = Quaternion.IDENTITY
		snap.velocity = Vector3(2, 0, 0)
		snap.grounded = true
		cp._snapshot_buffer[t % 64] = snap
		cp._pending_inputs[t] = {"move_direction": Vector3(1, 0, 0), "input_sequence": t}

	cp._last_confirmed_tick = -1

	var pending_after: Array[int] = []
	for tick in cp._pending_inputs.keys():
		if tick > 2:
			pending_after.append(tick)
	pending_after.sort()

	assert_eq("pred_04_pending_count", pending_after.size(), 2,
		"Should have 2 pending inputs after tick 2 (ticks 3, 4)")
	assert_eq("pred_04_pending_tick_3", pending_after[0], 3,
		"First pending tick should be 3")
	assert_eq("pred_04_pending_tick_4", pending_after[1], 4,
		"Second pending tick should be 4")
	check_pass("pred_04_packet_loss_recovery_logic")

# ===================================================================
# GROUP 3: Round Lifecycle
# ===================================================================

func test_round_transitions() -> void:
	if _game_server == null:
		check_fail("round_01_transitions", "GameServer not available")
		return

	var rm = _game_server.round_manager
	if rm == null:
		check_fail("round_01_transitions", "RoundManager not available")
		return

	assert_eq("round_01_initial_warmup", rm.get_phase(), rm.RoundPhase.WARMUP,
		"Initial phase should be WARMUP")

	rm.start_match()
	assert_eq("round_01_after_start_match", rm.get_phase(), rm.RoundPhase.PREP,
		"After start_match, phase should be PREP")
	assert_eq("round_01_round_1", rm.get_round_num(), 1,
		"Round number should be 1")
	assert_true("round_01_match_started", rm.is_match_started(),
		"Match should be started")

	rm._phase_timer = 0.001
	rm._advance_timer(0.1)
	assert_eq("round_01_live", rm.get_phase(), rm.RoundPhase.LIVE,
		"After PREP timer expires, phase should be LIVE")

	rm.end_round(TeamData.Team.COUNTER_TERRORIST, "test")
	assert_eq("round_01_post", rm.get_phase(), rm.RoundPhase.POST,
		"After end_round, phase should be POST")

	var ct_score = rm.team_manager.get_team_score(TeamData.Team.COUNTER_TERRORIST)
	assert_eq("round_01_score_ct", ct_score, 1,
		"CT should have 1 point after winning")
	check_pass("round_01_transition_sequence_ok")

func test_round_bomb_explosion() -> void:
	if _game_server == null:
		check_fail("round_02_bomb_explosion", "GameServer not available")
		return

	var rm = _game_server.round_manager
	if rm == null:
		check_fail("round_02_bomb_explosion", "RoundManager not available")
		return

	var tm = _game_server.team_manager
	if tm == null:
		check_fail("round_02_bomb_explosion", "TeamManager not available")
		return

	rm.start_match()
	rm.start_live()

	rm.end_round(TeamData.Team.TERRORIST, "bomb_exploded")

	assert_eq("round_02_phase", rm.get_phase(), rm.RoundPhase.POST,
		"Phase should be POST after bomb explosion")

	var t_score = tm.get_team_score(TeamData.Team.TERRORIST)
	assert_eq("round_02_t_score", t_score, 1,
		"Terrorist should have 1 point")
	check_pass("round_02_bomb_explosion_ok")

func test_round_bomb_defuse() -> void:
	if _game_server == null:
		check_fail("round_03_bomb_defuse", "GameServer not available")
		return

	var rm = _game_server.round_manager
	if rm == null:
		check_fail("round_03_bomb_defuse", "RoundManager not available")
		return

	var tm = _game_server.team_manager
	if tm == null:
		check_fail("round_03_bomb_defuse", "TeamManager not available")
		return

	rm.start_round(2)
	rm.start_live()

	rm.end_round(TeamData.Team.COUNTER_TERRORIST, "bomb_defused")

	assert_eq("round_03_phase", rm.get_phase(), rm.RoundPhase.POST,
		"Phase should be POST after bomb defuse")

	var ct_score = tm.get_team_score(TeamData.Team.COUNTER_TERRORIST)
	assert_eq("round_03_ct_score_updated", ct_score, 2,
		"CT should have 2 points (1 from prev + 1 from defuse)")
	check_pass("round_03_bomb_defuse_ok")

# ===================================================================
# GROUP 4: Economy & Weapons
# ===================================================================

func test_econ_kill_reward() -> void:
	if _game_server == null:
		check_fail("econ_01_kill_reward", "GameServer not available")
		return

	var em = _game_server.economy_manager
	if em == null:
		check_fail("econ_01_kill_reward", "EconomyManager not available")
		return

	em.register_player(5001)
	var initial_money = em.get_money(5001)
	assert_eq("econ_01_initial_money", initial_money, 800,
		"Starting money should be 800")

	em.award_kill_reward(5001)
	var after_kill = em.get_money(5001)
	assert_eq("econ_01_after_kill", after_kill, 1100,
		"Money should increase by 300 (kill reward)")

	var signal_data: Dictionary = {}
	var signal_conn = func(pid, old_amt, new_amt, reason):
		signal_data = {"player_id": pid, "old": old_amt, "new": new_amt, "reason": reason}
	em.money_changed.connect(signal_conn)

	em.award_kill_reward(5001)
	assert_eq("econ_01_signal_reason", signal_data.get("reason", ""), "kill",
		"Signal reason should be 'kill'")
	assert_eq("econ_01_signal_new_amount", signal_data.get("new", 0), 1400,
		"Signal should report new balance after kill reward")
	em.money_changed.disconnect(signal_conn)

	check_pass("econ_01_kill_reward_ok")

func test_econ_round_win_loss() -> void:
	if _game_server == null:
		check_fail("econ_02_win_loss", "GameServer not available")
		return

	var em = _game_server.economy_manager
	var tm = _game_server.team_manager
	if em == null or tm == null:
		check_fail("econ_02_win_loss", "EconomyManager or TeamManager not available")
		return

	em.register_player(6001)
	em.register_player(6002)
	tm.assign_player(6001, TeamData.Team.COUNTER_TERRORIST)
	tm.assign_player(6002, TeamData.Team.TERRORIST)

	em.set_money(6001, 0, "admin")
	em.set_money(6002, 0, "admin")

	var ct_players = [6001]
	em.award_round_win(TeamData.Team.COUNTER_TERRORIST, ct_players)
	assert_eq("econ_02_ct_after_win", em.get_money(6001), 3250,
		"CT player should get 3250 round win bonus")

	var t_players = [6002]
	em.award_round_loss(TeamData.Team.TERRORIST, t_players)
	assert_eq("econ_02_t_after_first_loss", em.get_money(6002), 1900,
		"T player should get 1900 base loss money on first loss")

	# Second consecutive loss with escalated bonus
	em.set_money(6002, 0, "admin")
	em.award_round_loss(TeamData.Team.TERRORIST, t_players)
	assert_eq("econ_02_t_after_second_loss", em.get_money(6002), 2400,
		"T player should get 1900 + 500 = 2400 on second consecutive loss")

	check_pass("econ_02_win_loss_ok")

	# Clean up
	tm.unregister_player(6001)
	tm.unregister_player(6002)
	em.unregister_player(6001)
	em.unregister_player(6002)

func test_econ_weapon_purchase() -> void:
	if _game_server == null:
		check_fail("econ_03_purchase", "GameServer not available")
		return

	var em = _game_server.economy_manager
	var ws = _game_server.weapon_server
	if em == null:
		check_fail("econ_03_purchase", "EconomyManager not available")
		return

	em.register_player(7001)
	em.set_money(7001, 5000, "admin")

	assert_true("econ_03_can_afford_rifle", em.can_afford(7001, 2700),
		"Player with 5000 should afford a 2700 rifle")
	assert_true("econ_03_can_afford_pistol", em.can_afford(7001, 500),
		"Player should afford a 500 pistol")

	var success = em.spend_money(7001, 2700)
	assert_true("econ_03_spend_rifle", success, "Spending 2700 should succeed")
	assert_eq("econ_03_after_rifle", em.get_money(7001), 2300,
		"After spending 2700, balance should be 2300")

	var fail_spend = em.spend_money(7001, 99999)
	assert_false("econ_03_insufficient_funds", fail_spend,
		"Spending more than balance should fail")

	if ws:
		ws.give_weapon(7001, "rifle")
		var can_fire = ws.can_fire(7001, "rifle")
		assert_true("econ_03_can_fire_rifle", can_fire,
			"Player should be able to fire rifle after purchasing it")
		var ammo_info = ws.get_ammo_info(7001, "rifle")
		assert_eq("econ_03_rifle_ammo", ammo_info.get("ammo", 0), 30,
			"Rifle should start with full magazine (30 rounds)")
	else:
		check_fail("econ_03_purchase", "WeaponServer not available")

	em.unregister_player(7001)
	if ws:
		ws.unregister_player(7001)

# ===================================================================
# GROUP 5: RCON
# ===================================================================

func test_rcon_auth_command() -> void:
	if _game_server == null:
		check_fail("rcon_01_auth_command", "GameServer not available")
		return

	var rcon_script = load("res://server/scripts/rcon_server.gd")
	var rcon = rcon_script.new()
	rcon.password = "testpass"
	rcon.enabled = false
	add_child(rcon)

	var handler = null
	for child in rcon.get_children():
		if child.has_method("handle_command"):
			handler = child
			break

	if handler == null:
		var handler_path = "res://server/scripts/rcon_command_handler.gd"
		if ResourceLoader.exists(handler_path):
			handler = load(handler_path).new()
			add_child(handler)

	if handler:
		var response = handler.handle_command("echo", ["hello"])
		assert_true("rcon_01_echo_response", "hello" in response,
			"Echo command should return the argument")

		response = handler.handle_command("help", [])
		assert_true("rcon_01_help_response", "Commands" in response or "help" in response,
			"Help command should return command list")

		response = handler.handle_command("echo", [])
		assert_true("rcon_01_ping_response", "Pong" in response,
			"Echo without args should return 'Pong'")

		response = handler.handle_command("status", [])
		assert_true("rcon_01_status_response",
			"Status" in response or "Server" in response,
			"Status command should return server info")
	else:
		check_fail("rcon_01_auth_command", "Command handler not created")

	rcon.queue_free()
	if handler and handler.get_parent():
		handler.queue_free()

func test_rcon_invalid_password() -> void:
	var rcon_script = load("res://server/scripts/rcon_server.gd")
	var rcon = rcon_script.new()
	rcon.password = "secretpass"
	rcon.enabled = false
	add_child(rcon)

	assert_eq("rcon_02_password_set", rcon.password, "secretpass",
		"Password should be set correctly")

	# Test IP ban logic
	rcon._banned_ips["127.0.0.1"] = Time.get_unix_time_from_system() + 5.0
	assert_true("rcon_02_ip_banned", rcon._is_ip_banned("127.0.0.1"),
		"IP should be banned after 3 failures")

	rcon.queue_free()
	check_pass("rcon_02_bad_password_rejected")

func test_rcon_start_match_end_round() -> void:
	if _game_server == null:
		check_fail("rcon_03_start_match_end_round", "GameServer not available")
		return

	var rm = _game_server.round_manager
	if rm == null:
		check_fail("rcon_03_start_match_end_round", "RoundManager not available")
		return

	rm.start_warmup()

	var handler_path = "res://server/scripts/rcon_command_handler.gd"
	if not ResourceLoader.exists(handler_path):
		check_fail("rcon_03_start_match_end_round", "RconCommandHandler not found")
		return

	var handler = load(handler_path).new()
	add_child(handler)

	if handler.has_signal("rcon_command"):
		handler.rcon_command.connect(_game_server._on_rcon_command)

	var response = handler.handle_command("start_match", [])
	assert_true("rcon_03_start_match_response",
		"Command" in response or "dispatched" in response,
		"start_match command should be dispatched")

	assert_true("rcon_03_match_started", rm.is_match_started(),
		"Match should be started via RCON")

	rm.start_live()

	response = handler.handle_command("end_round", ["ct", "bomb_defused"])
	assert_true("rcon_03_end_round_response",
		"Command" in response or "dispatched" in response,
		"end_round command should be dispatched")

	assert_eq("rcon_03_phase_post", rm.get_phase(), rm.RoundPhase.POST,
		"Phase should be POST after end_round via RCON")

	handler.queue_free()
	check_pass("rcon_03_start_match_end_round_ok")
