extends GdUnitTestSuite

# main.gd sets base_url/dev_player_id on AbilityCastClient from the same authority block as
# HotbarLoadoutClient (NEO-31).
# NEO-30: Slice 3 ability_cast_denied dev warning includes reasonCode from AbilityCastResponse;
# server deny hook sites are documented in AbilityCastApi.cs (gdlint: script doc lines <= 100 cols).

const CastClient := preload("res://scripts/ability_cast_client.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,"accepted":true}'
		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 = CastClient.new()
	c.set("injected_http", transport)
	auto_free(transport)
	auto_free(c)
	add_child(c)
	return c


func test_request_cast_posts_expected_payload_with_null_target() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}')
	var c := _make_client(transport)
	# Act
	var started: bool = bool(c.call("request_cast", 2, "prototype_guard", null))
	# Assert
	assert_that(started).is_true()
	assert_that(transport.last_url).contains("/ability-cast")
	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)
	assert_that(int(body.get("slotIndex", -1))).is_equal(2)
	assert_that(body.get("abilityId")).is_equal("prototype_guard")
	assert_that(body.get("targetId", "missing")).is_equal(null)


func test_request_cast_posts_target_id_string_when_set() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}')
	var c := _make_client(transport)
	# Act
	var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha"))
	# Assert
	assert_that(started).is_true()
	var parsed: Variant = JSON.parse_string(transport.last_body)
	var body: Dictionary = parsed
	assert_that(body.get("targetId")).is_equal("prototype_target_alpha")


func test_request_cast_returns_false_when_http_request_fails_to_start() -> void:
	# Arrange
	# Empty queue → transport.request returns ERR_UNAVAILABLE (same as failed start).
	var transport := MockHttpTransport.new()
	var c := _make_client(transport)
	# Act
	var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null))
	# Assert
	assert_that(started).is_false()


func test_request_cast_while_busy_is_ignored() -> void:
	# Arrange
	var transport := HoldFirstThenAutoTransport.new()
	var c := _make_client(transport)
	# Act
	var first_started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null))
	var second_started: bool = bool(c.call("request_cast", 1, "prototype_guard", null))
	await get_tree().process_frame
	await get_tree().process_frame
	# Assert
	assert_that(first_started).is_true()
	assert_that(second_started).is_false()
	assert_that(transport.request_count).is_equal(1)
	var parsed: Variant = JSON.parse_string(transport.last_body)
	var body: Dictionary = parsed
	assert_that(int(body.get("slotIndex", -1))).is_equal(0)


func test_cast_result_received_emits_false_with_reason_on_denied() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS,
		200,
		'{"schemaVersion":1,"accepted":false,"reasonCode":"invalid_target"}'
	)
	var c := _make_client(transport)
	var got: Array = []
	c.connect(
		"cast_result_received",
		func(accepted: bool, reason: String) -> void: got.append([accepted, reason])
	)
	# Act
	var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha"))
	# Assert
	assert_that(started).is_true()
	assert_that(got.size()).is_equal(1)
	assert_that(bool(got[0][0])).is_false()
	assert_that(str(got[0][1])).is_equal("invalid_target")


func test_cast_result_received_emits_true_with_empty_reason_on_accepted() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}')
	var c := _make_client(transport)
	var got: Array = []
	c.connect(
		"cast_result_received",
		func(accepted: bool, reason: String) -> void: got.append([accepted, reason])
	)
	# Act
	var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null))
	# Assert
	assert_that(started).is_true()
	assert_that(got.size()).is_equal(1)
	assert_that(bool(got[0][0])).is_true()
	assert_that(str(got[0][1])).is_equal("")
