extends RefCounted
## Guild basics (server/guild.gd): selftest --only=guild. Founding (level / gold / name rules), invite + accept,
## members never hit each other, Guild PK mode attacks a rival guild, ranks (officer, hand-over), kick rules, the
## leader's place on leaving, disbanding, the registry file reload, a stale guild id dropped, the client button /
## panel / name tag. --shotguild=<file>: the guild panel.

const SelfTest = preload("res://scripts/selftest.gd")
const CharacterState = preload("res://scripts/server/character_state.gd")
const Pvp = preload("res://scripts/server/pvp.gd")
const Guild = preload("res://scripts/server/guild.gd")


static func check(ok: bool, what: String) -> void:
	SelfTest.check(ok, "guild: " + what)


static func _player(srv, cname: String, at: Vector3):
	var c := CharacterState.new()
	c.new_character(cname)
	c.base_lv = 45
	var p = srv.add_player(c, true, 97 if cname == "Recruit" else 96)
	srv.enter_map(p, srv.player(main_id(srv)).map.map_id, at)
	return p


static var _main_id := 0


static func main_id(_srv) -> int:
	return _main_id


static func run(main: Node) -> void:
	print("SELFTEST guild")
	var srv = main.server
	var a = main.me()
	_main_id = a.id
	Guild.path_override = "user://test_guilds.json"
	if FileAccess.file_exists(Guild.path_override):
		DirAccess.remove_absolute(Guild.path_override)
	srv.remove_meta("guilds")
	main.load_map("aldoria_meadows")
	await SelfTest.frames(main, 4)
	SelfTest.clear_mobs(main)
	main.teleport_player(Vector3(14, 0, 14))
	await SelfTest.frames(main, 3)
	a.ch.guild = ""
	a.ch.pk = {}
	a.ch.base_lv = 20
	a.ch.zeny = 0
	check(not bool(srv.submit({"t": "guild_create", "name": "Starfall"}, a.id).get("ok", true)), "Base Lv 20: cannot found a guild")
	a.ch.base_lv = 45
	check(not bool(srv.submit({"t": "guild_create", "name": "Starfall"}, a.id).get("ok", true)), "no gold: cannot found")
	a.ch.zeny = 250000
	check(not bool(srv.submit({"t": "guild_create", "name": "ab"}, a.id).get("ok", true)), "a 2-letter name is refused")
	var r: Dictionary = srv.submit({"t": "guild_create", "name": "Starfall"}, a.id)
	check(bool(r.get("ok", false)) and a.ch.guild != "" and a.ch.zeny == 150000 and Guild.name_of(srv, a) == "Starfall", "Starfall founded for 100,000 gold")
	# invite + accept
	var b = _player(srv, "Recruit", a.global_position + Vector3(2, 0, 0))
	var c = _player(srv, "Rival", a.global_position + Vector3(-2, 0, 0))
	await SelfTest.frames(main, 4)
	check(bool(srv.submit({"t": "guild_invite", "name": "Recruit"}, a.id).get("ok", false)), "invite sent")
	check(bool(srv.submit({"t": "guild_accept", "gid": a.ch.guild}, b.id).get("ok", false)) and b.ch.guild == a.ch.guild, "Recruit joined")
	c.ch.zeny = 200000
	srv.submit({"t": "guild_create", "name": "Thornguard"}, c.id)
	check(c.ch.guild != "" and c.ch.guild != a.ch.guild, "a rival guild founded")
	check(not bool(srv.submit({"t": "guild_create", "name": "starfall"}, b.id).get("ok", true)), "names are unique (any case)")
	# PvP: guild mates never, Guild mode attacks the rival guild
	for p in [a, b, c]:
		Pvp.pk(p.ch)["mode"] = "guild"
	check(Pvp.allies(srv, a, b) and not Pvp.can_hit(srv, a, b), "guild mates never hit each other")
	check(Pvp.can_hit(srv, a, c), "Guild PK mode attacks a rival guild member")
	Pvp.pk(a.ch)["mode"] = "free"
	check(not Pvp.can_hit(srv, a, b), "not even in Free mode")
	# ranks / kick
	check(not bool(srv.submit({"t": "guild_rank", "name": "Starfall", "rank": "officer"}, b.id).get("ok", true)), "a member cannot change ranks")
	check(bool(srv.submit({"t": "guild_rank", "name": "Recruit", "rank": "officer"}, a.id).get("ok", false)) and Guild.rank_of(Guild.of(srv, a), "Recruit") == "officer", "the leader makes Recruit an officer")
	check(not bool(srv.submit({"t": "guild_kick", "name": str(a.ch.char_name)}, b.id).get("ok", true)), "an officer cannot kick the leader")
	check(bool(srv.submit({"t": "guild_kick", "name": "Recruit"}, a.id).get("ok", false)) and b.ch.guild == "", "the leader removes Recruit")
	# the leader's place on leaving, disbanding
	srv.submit({"t": "guild_invite", "name": "Recruit"}, a.id)
	srv.submit({"t": "guild_accept", "gid": a.ch.guild}, b.id)
	var gid: String = a.ch.guild
	srv.submit({"t": "guild_leave"}, a.id)
	var g = (Guild.reg(srv)["guilds"] as Dictionary).get(gid, null)
	check(a.ch.guild == "" and g != null and str(g["leader"]) == "Recruit", "the leader leaves: Recruit leads now")
	srv.submit({"t": "guild_leave"}, b.id)
	check(not (Guild.reg(srv)["guilds"] as Dictionary).has(gid), "the last member out: the guild is disbanded")
	# the registry file + a stale id
	a.ch.zeny = 200000
	srv.submit({"t": "guild_create", "name": "Starfall"}, a.id)
	var gid2: String = a.ch.guild
	srv.remove_meta("guilds")
	check((Guild.reg(srv)["guilds"] as Dictionary).has(gid2), "the registry reloads from its file")
	b.ch.guild = "g999"
	Guild.validate(srv, b)
	check(b.ch.guild == "", "a guild id the registry does not back is dropped")
	check(str(a.ch.to_dict().get("guild", "")) == gid2, "the guild id is saved with the character")
	# client: button, panel, name tag on Recruit after joining
	srv.submit({"t": "guild_invite", "name": "Recruit"}, a.id)
	srv.submit({"t": "guild_accept", "gid": gid2}, b.id)
	await SelfTest.frames(main, 30)
	var pc = load("res://scripts/pvp_client.gd").inst(main)
	check(pc.guild_btn != null and str(pc.guild.get("name", "")) == "Starfall", "client: Guild button + guild view (%s)" % str(pc.guild.get("name", "")))
	var bv = null
	for v in main.get_tree().get_nodes_in_group("remote_players"):
		if int(v.get("id")) == b.id:
			bv = v
	var nl = bv.get("name_label") if bv else null
	var tg = nl.get_node_or_null("GuildTag") if nl else null
	check(tg != null and tg.visible and str(tg.text) == "<Starfall>" and not str(nl.text).contains("<"),
		"client: the guild tag on its own line above the name plate (%s / %s)" % [str(nl.text) if nl else "-", str(tg.text) if tg else "-"])
	var mine = main.player.name_label.get_node_or_null("GuildTag")
	check(mine != null and mine.visible and str(mine.text) == "<Starfall>", "client: my own name plate shows my guild tag")
	# Recruit stands 2 m away: the plates are stacked, not drawn over each other
	check(nl != null and nl.offset.y > 0.0 and main.player.name_label.offset.y == 0.0 and tg.offset == nl.offset,
		"client: plates of players standing close together are stacked (%s)" % str(nl.offset if nl else Vector2.ZERO))
	pc.toggle_guild(main)
	await SelfTest.frames(main, 4)
	check(pc.guild_panel.visible and pc.guild_panel.find_child("GuildTitle", true, false) != null and pc.guild_panel.find_child("Kick_Recruit", true, false) != null,
		"client: the panel lists the members with leader controls")
	check(main.hud.windows.get("pk_guild") == pc.guild_panel and pc.guild_panel.find_child("Title", true, false) != null,
		"client: the guild window is a UI v4 book window (title plaque + close)")
	check(pc.guild_btn.get_script() != null and load("res://scripts/ui/hud_menu.gd").art("ring_guild") != null, "client: the Guild button is a wooden-ring icon")
	for arg in OS.get_cmdline_user_args():
		if arg.begins_with("--shotguild="):
			await SelfTest.frames(main, 10)
			main.get_viewport().get_texture().get_image().save_jpg(arg.substr(12), 0.9)
			print("SHOT guild")
	pc.toggle_guild(main)
	# clean up
	srv.submit({"t": "guild_leave"}, b.id)
	srv.submit({"t": "guild_leave"}, a.id)
	srv.submit({"t": "guild_leave"}, c.id)
	srv.remove_player(b)
	srv.remove_player(c)
	a.ch.pk = {}
	srv.remove_meta("guilds")
	if FileAccess.file_exists(Guild.path_override):
		DirAccess.remove_absolute(Guild.path_override)
	Guild.path_override = ""
