extends GdUnitTestSuite

# Note: declaration-order-only edits in main scene scripts can still trigger hook-required
# client test restaging; this suite is included in the same commit without behavior changes.
const HotbarClient := preload("res://scripts/hotbar_loadout_client.gd")
const HotbarState := preload("res://scripts/hotbar_state.gd")


class MockHttpTransport:
	extends Node
	signal request_completed(
		result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
	)
	var last_url: String = ""
	var last_body: String = ""
	var last_method: HTTPClient.Method = HTTPClient.METHOD_GET
	var _queue: Array[Dictionary] = []

	func enqueue(result: int, code: int, body: String) -> void:
		_queue.append({"result": result, "code": code, "body": body})

	func request(
		url: String,
		_custom_headers: PackedStringArray = PackedStringArray(),
		method: HTTPClient.Method = HTTPClient.METHOD_GET,
		request_data: String = ""
	) -> Error:
		last_url = url
		last_method = method
		last_body = request_data
		if _queue.is_empty():
			return ERR_UNAVAILABLE
		var r: Dictionary = _queue.pop_front()
		request_completed.emit(
			r["result"], r["code"], PackedStringArray(), str(r["body"]).to_utf8_buffer()
		)
		return OK


class HoldFirstThenAutoTransport:
	extends Node
	signal request_completed(
		result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
	)
	var request_count: int = 0
	var last_body: String = ""
	var _first: bool = true

	func request(
		_url: String,
		_custom_headers: PackedStringArray = PackedStringArray(),
		_method: HTTPClient.Method = HTTPClient.METHOD_GET,
		request_data: String = ""
	) -> Error:
		request_count += 1
		last_body = request_data
		var body_json := (
			'{"schemaVersion":1,"updated":true,"loadout":'
			+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
		)
		var body_bytes: PackedByteArray = body_json.to_utf8_buffer()
		if _first:
			_first = false
			call_deferred("_emit", body_bytes)
		else:
			request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes)
		return OK

	func _emit(body_bytes: PackedByteArray) -> void:
		request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes)


func _make_client(transport: Node) -> Node:
	var c: Node = HotbarClient.new()
	c.set("injected_http", transport)
	auto_free(transport)
	auto_free(c)
	add_child(c)
	return c


func _sample_slots_json() -> String:
	return (
		'{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":['
		+ '{"slotIndex":0,"abilityId":"prototype_pulse"},'
		+ '{"slotIndex":1,"abilityId":null},'
		+ '{"slotIndex":2,"abilityId":"prototype_guard"},'
		+ '{"slotIndex":3,"abilityId":null},'
		+ '{"slotIndex":4,"abilityId":null},'
		+ '{"slotIndex":5,"abilityId":null},'
		+ '{"slotIndex":6,"abilityId":null},'
		+ '{"slotIndex":7,"abilityId":null}'
		+ "]}"
	)


func test_sync_get_applies_slots_to_hotbar_state() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _sample_slots_json())
	var c := _make_client(transport)
	var state := HotbarState.new()
	auto_free(state)
	add_child(state)
	c.call("set_hotbar_state", state)
	# Act
	c.call("request_sync_from_server")
	# Assert
	var slots: Array = state.call("slots_snapshot")
	assert_that(slots.size()).is_equal(8)
	assert_that(slots[0]).is_equal("prototype_pulse")
	assert_that(slots[2]).is_equal("prototype_guard")
	assert_that(slots[1]).is_null()


func test_bind_slot_posts_expected_payload() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS,
		200,
		(
			'{"schemaVersion":1,"updated":true,"loadout":'
			+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
		)
	)
	var c := _make_client(transport)
	# Act
	c.call("request_bind_slot", 3, "prototype_burst")
	# Assert
	assert_that(transport.last_url).contains("/hotbar-loadout")
	assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST)
	var parsed: Variant = JSON.parse_string(transport.last_body)
	assert_that(parsed is Dictionary).is_true()
	var body: Dictionary = parsed
	assert_that(int(body.get("schemaVersion", 0))).is_equal(1)
	var slots: Array = body.get("slots", [])
	assert_that(slots.size()).is_equal(1)
	assert_that(int((slots[0] as Dictionary).get("slotIndex", -1))).is_equal(3)
	assert_that((slots[0] as Dictionary).get("abilityId")).is_equal("prototype_burst")


func test_denied_update_emits_reason_signal() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS,
		200,
		(
			'{"schemaVersion":1,"updated":false,"reasonCode":"unknown_ability","loadout":'
			+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
		)
	)
	var c := _make_client(transport)
	monitor_signals(c)
	# Act
	c.call("request_bind_slot", 1, "not_real")
	# Assert
	assert_signal(c).is_emitted("loadout_update_denied")


func test_bind_slot_while_busy_is_ignored() -> void:
	# Arrange
	var transport := HoldFirstThenAutoTransport.new()
	var c := _make_client(transport)
	# Act
	c.call("request_bind_slot", 1, "prototype_pulse")
	c.call("request_bind_slot", 2, "prototype_guard")
	await get_tree().process_frame
	await get_tree().process_frame
	# Assert
	assert_that(transport.request_count).is_equal(1)
	var parsed: Variant = JSON.parse_string(transport.last_body)
	assert_that(parsed is Dictionary).is_true()
	var slots: Array = (parsed as Dictionary).get("slots", [])
	assert_that(int((slots[0] as Dictionary).get("slotIndex", -1))).is_equal(1)


func test_http_failure_does_not_mutate_cached_loadout() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(HTTPRequest.RESULT_CANT_CONNECT, 0, "")
	var c := _make_client(transport)
	# Act
	c.call("request_sync_from_server")
	# Assert
	var cached: Dictionary = c.call("cached_loadout")
	assert_that(cached.is_empty()).is_true()


func test_non_json_error_response_keeps_cached_loadout_empty() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(HTTPRequest.RESULT_SUCCESS, 500, "oops")
	var c := _make_client(transport)
	# Act
	c.call("request_sync_from_server")
	# Assert
	var cached: Dictionary = c.call("cached_loadout")
	assert_that(cached.is_empty()).is_true()
