extends GdUnitTestSuite

## NEO-26: `TargetSelectionClient.selection_event` — cause strings and lock-id-only rule.

const TargetSelectionTestDouble := preload("res://test/target_selection_test_double.gd")

const ALPHA_ID := "prototype_target_alpha"
const BETA_ID := "prototype_target_beta"


class MockHttpTransport:
	extends Node
	signal request_completed(
		result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
	)
	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:
		if _queue.is_empty():
			return ERR_UNAVAILABLE
		var r: Dictionary = _queue.pop_front()
		var body_bytes: PackedByteArray = str(r["body"]).to_utf8_buffer()
		request_completed.emit(r["result"], r["code"], PackedStringArray(), body_bytes)
		return OK


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


func _target_state_json(locked_id_or_null: Variant, validity: String, sequence: int = 0) -> String:
	var locked := "null"
	if locked_id_or_null is String and not (locked_id_or_null as String).is_empty():
		locked = '"%s"' % (locked_id_or_null as String)
	return (
		'{"schemaVersion":1,"playerId":"dev-local-1","lockedTargetId":%s,' % locked
		+ '"validity":"%s","sequence":%d}' % [validity, sequence]
	)


func _select_response_json(
	applied: bool,
	locked_id_or_null: Variant,
	validity: String,
	sequence: int = 0,
	reason: String = ""
) -> String:
	var target_state := _target_state_json(locked_id_or_null, validity, sequence)
	if applied:
		return '{"schemaVersion":1,"selectionApplied":true,"targetState":%s}' % target_state
	var reason_segment := ""
	if not reason.is_empty():
		reason_segment = ',"reasonCode":"%s"' % reason
	return (
		'{"schemaVersion":1,"selectionApplied":false%s,"targetState":%s}'
		% [reason_segment, target_state]
	)


func test_tab_lock_change_emits_tab() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
	)
	var c := _make_client(transport)
	var events: Array = []
	c.selection_event.connect(func(ev: Dictionary) -> void: events.append(ev.duplicate()))
	# Act
	c.request_tab_next()
	# Assert
	assert_that(events.size()).is_equal(1)
	var last: Dictionary = events[0] as Dictionary
	assert_that(last.get("cause", "")).is_equal("tab")
	assert_that(last.get("previous")).is_null()
	assert_that(last.get("next")).is_equal(ALPHA_ID)
	assert_that(int(last.get("sequence", -1))).is_equal(1)


func test_clear_lock_change_emits_clear() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
	)
	transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, null, "none", 2))
	var c := _make_client(transport)
	var events: Array = []
	c.selection_event.connect(func(ev: Dictionary) -> void: events.append(ev.duplicate()))
	# Act
	c.request_tab_next()
	c.request_clear_target()
	# Assert
	assert_that(events.size()).is_equal(2)
	assert_that(int((events[0] as Dictionary).get("sequence", -1))).is_equal(1)
	assert_that((events[1] as Dictionary).get("cause", "")).is_equal("clear")
	assert_that((events[1] as Dictionary).get("previous")).is_equal(ALPHA_ID)
	assert_that((events[1] as Dictionary).get("next")).is_null()
	assert_that(int((events[1] as Dictionary).get("sequence", -1))).is_equal(2)


func test_get_lock_id_change_emits_server_correction() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
	)
	transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(BETA_ID, "ok", 2))
	var c := _make_client(transport)
	var events: Array = []
	c.selection_event.connect(func(ev: Dictionary) -> void: events.append(ev.duplicate()))
	# Act
	c.request_tab_next()
	c.request_sync_from_server()
	# Assert
	assert_that(events.size()).is_equal(2)
	var last: Dictionary = events[1] as Dictionary
	assert_that(last.get("cause", "")).is_equal("server_correction")
	assert_that(last.get("previous")).is_equal(ALPHA_ID)
	assert_that(last.get("next")).is_equal(BETA_ID)
	assert_that(int(last.get("sequence", -1))).is_equal(2)


func test_get_validity_only_change_emits_nothing() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
	)
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(ALPHA_ID, "out_of_range", 1)
	)
	var c := _make_client(transport)
	var events: Array = []
	c.selection_event.connect(func(ev: Dictionary) -> void: events.append(ev.duplicate()))
	# Act
	c.request_tab_next()
	c.request_sync_from_server()
	# Assert
	assert_that(events.size()).is_equal(1)
	assert_that((events[0] as Dictionary).get("cause", "")).is_equal("tab")
	assert_that(int((events[0] as Dictionary).get("sequence", -1))).is_equal(1)


func test_denial_without_lock_id_change_emits_nothing() -> void:
	# Arrange
	var transport := MockHttpTransport.new()
	transport.enqueue(
		HTTPRequest.RESULT_SUCCESS,
		200,
		_select_response_json(false, null, "none", 0, "out_of_range")
	)
	var c := _make_client(transport)
	var events: Array = []
	c.selection_event.connect(func(ev: Dictionary) -> void: events.append(ev.duplicate()))
	# Act
	c.request_select_target_id(ALPHA_ID)
	# Assert
	assert_that(events.size()).is_equal(0)
