-- monoids.lua
-- This file defines a set of testing chatcommands for the monoids system.

local speed = player_monoids.speed
local jump = player_monoids.jump

minetest.register_privilege("monoid_master", {
	description = "Allows testing of player monoids.",
	give_to_singleplayer = false,
	give_to_admin = true,
})

--------------------------------------------------------------------------------
-- Helper: reset branches for both speed and jump
--------------------------------------------------------------------------------
local function reset_all_monoid_branches(player)
	local p_name = player:get_player_name()

	for _, monoid in ipairs({speed, jump}) do
		if monoid and monoid.player_map and monoid.player_map[p_name] then
			local pm = monoid.player_map[p_name]
			local to_delete = {}
			for bn, _ in pairs(pm.branches) do
				table.insert(to_delete, bn)
			end
			for _, bn in ipairs(to_delete) do
				local br = monoid:get_branch(bn)
				if br then
					br:reset(player)
					br:delete(player)
				end
			end
			monoid:get_branch("main")
			monoid:checkout_branch(player, "main")
		end
	end
end

--------------------------------------------------------------------------------
-- 1) Test Speed Add/Remove
--------------------------------------------------------------------------------
local function test_speed_add_remove(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local before = speed:value(player)
	local ch_id = speed:add_change(player, 10)
	local after_add = speed:value(player)

	if after_add == before then
		minetest.chat_send_player(p_name, "[Add/Remove] FAIL: no speed change.")
	else
		minetest.chat_send_player(p_name, "[Add/Remove] PASS: speed " .. before .. " -> " .. after_add)
	end

	minetest.after(0.5, function()
		local again = minetest.get_player_by_name(p_name)
		if not again then return end
		speed:del_change(again, ch_id)
		local after_del = speed:value(again)
		if math.abs(after_del - before) < 0.0001 then
			minetest.chat_send_player(p_name, "[Add/Remove] PASS: speed returned to " .. before)
		else
			minetest.chat_send_player(p_name, "[Add/Remove] FAIL: final " .. after_del .. " != initial " .. before)
		end
	end)
end

--------------------------------------------------------------------------------
-- 2) Test Branch Isolation
--------------------------------------------------------------------------------
local function test_branch_isolation(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local main_spd_0 = speed:value(player)
	speed:checkout_branch(player, "arena")
	speed:add_change(player, 0.5, "arena_slowdown", "arena")
	local arena_spd = speed:value(player)
	if arena_spd >= main_spd_0 then
		minetest.chat_send_player(p_name, "[BranchIsolation] FAIL: arena slowdown not effective.")
	else
		minetest.chat_send_player(p_name, "[BranchIsolation] PASS: arena slow " .. main_spd_0 .. " -> " .. arena_spd)
	end

	local id_speed_boost
	minetest.after(0.5, function()
		speed:checkout_branch(player, "main")
		id_speed_boost = speed:add_change(player, 2)
		local main_spd_1 = speed:value(player)
		if main_spd_1 <= main_spd_0 then
			minetest.chat_send_player(p_name, "[BranchIsolation] FAIL: main speedup not effective.")
		else
			minetest.chat_send_player(p_name, "[BranchIsolation] PASS: main speed " .. main_spd_0 .. " -> " .. main_spd_1)
		end
	end)

	minetest.after(1.0, function()
		speed:checkout_branch(player, "arena")
		local arena_spd_2 = speed:value(player)
		minetest.chat_send_player(p_name, "[BranchIsolation] re-check => " .. arena_spd_2)
		-- Revert to default
		speed:checkout_branch(player, "main")
		speed:del_change(player, id_speed_boost)
	end)
end

--------------------------------------------------------------------------------
-- 3) Test Branch Concurrent
--------------------------------------------------------------------------------
local function test_branch_concurrent(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local init = speed:value(player)

	local arena_branch = speed:checkout_branch(player, "arena")
	arena_branch:add_change(player, 0.5, "arena_slowdown")
	local arena_spd = speed:value(player)
	if arena_spd >= init then
		minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: arena slowdown didn't reduce speed.")
	else
		minetest.chat_send_player(p_name, "[BranchConcurrent] PASS: arena from " .. init .. " -> " .. arena_spd)
	end

	minetest.after(0.5, function()
		local mining_branch = speed:checkout_branch(player, "mining")
		if not mining_branch then
			minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: 'mining' branch could not be created.")
			return
		end
		mining_branch:add_change(player, 0.3, "mining_slowdown")

		local main_branch = speed:get_branch("main")
		if not main_branch then
			minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: 'main' branch doesn't exist?")
			return
		end
		main_branch:add_change(player, 2, "main_speedup_concurrent")

		local mining_spd = speed:value(player)
		local main_spd = main_branch:value(player)
		if mining_spd >= init then
			minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: mining slowdown not effective.")
		else
			minetest.chat_send_player(p_name, "[BranchConcurrent] PASS: mining slow => " .. mining_spd)
		end
		if main_spd <= init then
			minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: main concurrent speedup not effective.")
		else
			minetest.chat_send_player(p_name, "[BranchConcurrent] PASS: main speed => " .. main_spd)
		end
	end)

	minetest.after(1.0, function()
		local arena_b = speed:get_branch("arena")
		if arena_b then
			arena_b:reset(player)
			speed:checkout_branch(player, "arena")
			local reset_spd = speed:value(player)
			minetest.chat_send_player(p_name, "[BranchConcurrent] arena reset => " .. reset_spd)
		else
			minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: 'arena' branch not found?")
		end
	end)

	minetest.after(1.5, function()
		speed:checkout_branch(player, "main")
		local main_spd = speed:value(player)
		minetest.chat_send_player(p_name, "[BranchConcurrent] final main => " .. main_spd)

		-- Revert to default
		speed:del_change(player, "main_speedup_concurrent")
	end)
end

--------------------------------------------------------------------------------
-- 4) Test OnChange ListenAll
--------------------------------------------------------------------------------
local function test_onchange_listen_all(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local call_count = 0
	speed.def.listen_to_all_changes = true

	local old_on_change = speed.def.on_change
	speed.def.on_change = function(old, new, plyr, branch)
		call_count = call_count + 1
	end

	speed:add_change(player, 1, "active_change")
	speed:add_change(player, 0.5, "arena_slowdown", "arena")

	minetest.after(1, function()
		speed.def.on_change = old_on_change
		speed.def.listen_to_all_changes = false
		if call_count == 0 then
			minetest.chat_send_player(p_name, "[OnChangeAll] FAIL: on_change not triggered.")
		else
			minetest.chat_send_player(p_name, "[OnChangeAll] PASS: on_change called " .. call_count .. " times.")
		end
	end)
end

minetest.register_chatcommand("test_listen_all", {
	description = "Test on_change across all branches.",
	privs = {monoid_master = true},
	func = function(p_name)
		local player = minetest.get_player_by_name(p_name)
		if player then
			test_onchange_listen_all(player)
		end
	end,
})

--------------------------------------------------------------------------------
-- 5) Test OnChange ListenActive
--------------------------------------------------------------------------------
local function test_onchange_listen_active(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local call_count = 0
	speed.def.listen_to_all_changes = false

	local old_on_change = speed.def.on_change
	speed.def.on_change = function(old, new, plyr, branch)
		call_count = call_count + 1
	end

	speed:add_change(player, 1, "active_change")
	speed:add_change(player, 0.5, "arena_slowdown", "arena")

	minetest.after(1, function()
		speed.def.on_change = old_on_change
		speed.def.listen_to_all_changes = false
		if call_count == 0 then
			minetest.chat_send_player(p_name, "[OnChangeActive] FAIL: on_change not called.")
		else
			minetest.chat_send_player(p_name, "[OnChangeActive] PASS: on_change triggered " .. call_count .. " times.")
		end
	end)
end

minetest.register_chatcommand("test_listen_active", {
	description = "Test on_change only in active branch.",
	privs = {monoid_master = true},
	func = function(p_name)
		local player = minetest.get_player_by_name(p_name)
		if player then
			test_onchange_listen_active(player)
		end
	end,
})

--------------------------------------------------------------------------------
-- 6) Test BranchName
--------------------------------------------------------------------------------
local function test_branch_name_check(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	speed:checkout_branch(player, "arena")
	local branch = speed:get_branch("arena")
	if not branch then
		minetest.chat_send_player(p_name, "[BranchNameCheck] FAIL: get_branch('arena') is nil?")
		return
	end
	local got = branch:get_name()
	if got == "arena" then
		minetest.chat_send_player(p_name, "[BranchNameCheck] PASS: got 'arena'")
	else
		minetest.chat_send_player(p_name, "[BranchNameCheck] FAIL: expected 'arena', got '" .. (got or "nil") .. "'")
	end
end

minetest.register_chatcommand("test_branch_name", {
	description = "Test branch:get_name() method.",
	privs = {monoid_master = true},
	func = function(p_name)
		local player = minetest.get_player_by_name(p_name)
		if player then
			test_branch_name_check(player)
		end
	end,
})

--------------------------------------------------------------------------------
-- 7) Test ActiveBranchGet
--------------------------------------------------------------------------------
local function test_active_branch_check(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	speed:checkout_branch(player, "arena")
	local active = speed:get_active_branch(player)
	local got = active and active:get_name() or "(nil)"
	if got == "arena" then
		minetest.chat_send_player(p_name, "[ActiveBranchGet] PASS: 'arena' active")
	else
		minetest.chat_send_player(p_name, "[ActiveBranchGet] FAIL: expected 'arena', got '" .. got .. "'")
	end
end

minetest.register_chatcommand("test_active_branch", {
	description = "Test monoid:get_active_branch.",
	privs = {monoid_master = true},
	func = function(p_name)
		local player = minetest.get_player_by_name(p_name)
		if player then
			test_active_branch_check(player)
		end
	end,
})

--------------------------------------------------------------------------------
-- 8) Test BranchDelete
--------------------------------------------------------------------------------
local function test_branch_delete_check(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local main_branch = speed:get_branch("main")
	local before_main = main_branch:value(player)

	local del_branch = speed:checkout_branch(player, "delete_test")
	del_branch:add_change(player, 0.2, "delete_test_slowdown")
	local delete_spd = speed:value(player)

	if delete_spd == before_main then
		minetest.chat_send_player(p_name, "[BranchDelete] FAIL: no slowdown?")
	else
		minetest.chat_send_player(p_name, "[BranchDelete] PASS: speed from " .. before_main .. " to " .. delete_spd)
	end

	minetest.after(1, function()
		del_branch:delete(player)
		if speed.player_map[p_name].branches["delete_test"] then
			minetest.chat_send_player(p_name, "[BranchDelete] FAIL: branch still exists.")
		else
			local active_b = speed.player_map[p_name].active_branch
			local after_main = speed:value(player)
			if active_b ~= "main" then
				minetest.chat_send_player(p_name, "[BranchDelete] FAIL: active branch is " .. active_b)
			elseif math.abs(after_main - before_main) > 0.0001 then
				minetest.chat_send_player(p_name, "[BranchDelete] FAIL: main speed not restored.")
			else
				minetest.chat_send_player(p_name, "[BranchDelete] PASS: 'delete_test' gone, main restored.")
			end
		end
	end)
end

minetest.register_chatcommand("test_branch_delete_monoids", {
	description = "Runs a test on monoids branch deletion.",
	privs = {monoid_master = true},
	func = function(p_name)
		local player = minetest.get_player_by_name(p_name)
		if player then
			test_branch_delete_check(player)
		end
	end,
})

--------------------------------------------------------------------------------
-- 9) Test GetBranches
--------------------------------------------------------------------------------
local function test_get_branches(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local br_a = speed:checkout_branch(player, "testA")
	if br_a then
		br_a:add_change(player, 2, "testA_boost")
	else
		minetest.chat_send_player(p_name, "[GetBranches] FAIL: unable to create 'testA'.")
	end

	local br_b = speed:new_branch(player, "testB")
	if br_b then
		br_b:add_change(player, 0.3, "testB_slow")
	else
		minetest.chat_send_player(p_name, "[GetBranches] FAIL: new_branch('testB') returned nil.")
	end

	local br_map = speed:get_branches(player)
	if br_map["testA"] and br_map["testB"] then
		minetest.chat_send_player(p_name, "[GetBranches] PASS: 'testA' and 'testB' found.")
	else
		minetest.chat_send_player(p_name, "[GetBranches] FAIL: missing 'testA' or 'testB' in get_branches.")
	end

	-- Revert to default
	speed:checkout_branch(player, "main")
end

--------------------------------------------------------------------------------
-- 10) Test on_branch_created / on_branch_deleted
--------------------------------------------------------------------------------
local function test_on_branch_create_delete(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local created_count = 0
	local deleted_count = 0

	local old_on_branch_created = speed.def.on_branch_created
	local old_on_branch_deleted = speed.def.on_branch_deleted

	speed.def.on_branch_created = function(monoid, plyr, branch_name)
		created_count = created_count + 1
	end

	speed.def.on_branch_deleted = function(monoid, plyr, branch_name)
		deleted_count = deleted_count + 1
	end

	local new_branch = speed:checkout_branch(player, "my_new_branch")
	if not new_branch then
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: checkout_branch returned nil.")
	else
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] Created 'my_new_branch'.")
	end

	local del_branch = speed:checkout_branch(player, "my_del_branch")
	if not del_branch then
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: couldn't create 'my_del_branch'.")
	else
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] Created 'my_del_branch'. Deleting...")
		del_branch:delete(player)
	end

	if created_count == 0 then
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: on_branch_created not called.")
	else
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] PASS: on_branch_created called " ..
			created_count .. " time(s).")
	end

	if deleted_count == 0 then
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: on_branch_deleted not called.")
	else
		minetest.chat_send_player(p_name, "[OnBranchCreateDelete] PASS: on_branch_deleted called " ..
			deleted_count .. " time(s).")
	end

	speed.def.on_branch_created = old_on_branch_created
	speed.def.on_branch_deleted = old_on_branch_deleted
end

--------------------------------------------------------------------------------
-- 11) Test monoid:new_branch
--------------------------------------------------------------------------------
local function test_new_branch_method(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local init_speed = speed:value(player)

	local custom_branch = speed:new_branch(player, "custom_new_branch")
	if not custom_branch then
		minetest.chat_send_player(p_name, "[NewBranchMethod] FAIL: new_branch returned nil")
		return
	end
	custom_branch:add_change(player, 0.4, "custom_slow")

	local after_new = speed:value(player)
	if math.abs(after_new - init_speed) > 0.0001 then
		minetest.chat_send_player(p_name, "[NewBranchMethod] FAIL: Speed changed even though new branch not active.")
	else
		minetest.chat_send_player(p_name, "[NewBranchMethod] PASS: Speed unchanged = " .. after_new)
	end

	speed:checkout_branch(player, "custom_new_branch")
	local after_checkout = speed:value(player)
	if after_checkout == init_speed then
		minetest.chat_send_player(p_name, "[NewBranchMethod] FAIL: Speed not changed after activation.")
	else
		minetest.chat_send_player(p_name, "[NewBranchMethod] PASS: Speed changed from " .. init_speed
			.. " to " .. after_checkout)
	end

	-- Revert to default
	speed:checkout_branch(player, "main")
end

--------------------------------------------------------------------------------
-- 12) Test main branch cannot be deleted
--------------------------------------------------------------------------------
local function test_main_branch_cant_delete(player)
	local p_name = player:get_player_name()
	reset_all_monoid_branches(player)

	local main_br = speed:get_branch("main")
	if not main_br then
		minetest.chat_send_player(p_name, "[MainBranchCantDelete] FAIL: main branch does not exist?")
		return
	end

	main_br:delete(player)
	local still_main = speed.player_map[p_name] and speed.player_map[p_name].branches["main"]
	if still_main then
		minetest.chat_send_player(p_name, "[MainBranchCantDelete] PASS: main not deleted.")
	else
		minetest.chat_send_player(p_name, "[MainBranchCantDelete] FAIL: main branch was deleted!")
	end
end

--------------------------------------------------------------------------------
-- 13) Test using speed + jump together in the same branch
--------------------------------------------------------------------------------
local function test_speed_and_jump_together(player)
	local p_name = player:get_player_name()

	if not jump then
		minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: 'player_monoids.jump' not defined!")
		return
	end

	reset_all_monoid_branches(player)

	-- Grab initial speed + jump
	local init_speed = speed:value(player)
	local init_jump = jump:value(player)

	-- Create or checkout a test branch that affects both
	speed:checkout_branch(player, "double_test")
	jump:checkout_branch(player, "double_test")

	local sp_ch_id = speed:add_change(player, 2, "double_spd", "double_test")
	local jp_ch_id = jump:add_change(player, 1.5, "double_jmp", "double_test")

	local after_speed = speed:value(player)
	local after_jump = jump:value(player)

	local speed_changed = after_speed ~= init_speed
	local jump_changed = after_jump ~= init_jump

	if speed_changed and jump_changed then
		minetest.chat_send_player(p_name, "[SpeedJumpTogether] PASS: Speed changed " ..
			init_speed .. "->" .. after_speed .. ", Jump changed " .. init_jump .. "->" .. after_jump)
	else
		minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: Speed changed=" ..
			tostring(speed_changed) .. ", Jump changed=" .. tostring(jump_changed))
	end

	-- Remove only speed change, see if jump remains.
	speed:del_change(player, sp_ch_id, "double_test")
	local sp2 = speed:value(player)
	local jp2 = jump:value(player)

	if math.abs(sp2 - init_speed) < 0.0001 and math.abs(jp2 - after_jump) < 0.0001 then
		minetest.chat_send_player(p_name, "[SpeedJumpTogether] PASS: Speed reverted to " ..
			init_speed .. ", jump remains " .. jp2)
	else
		minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: partial revert mismatch. Speed=" ..
			sp2 .. ", jump=" .. jp2)
	end

	-- Remove jump change, confirm both are back to init
	jump:del_change(player, jp_ch_id, "double_test")
	local sp3 = speed:value(player)
	local jp3 = jump:value(player)

	if math.abs(sp3 - init_speed) < 0.0001 and math.abs(jp3 - init_jump) < 0.0001 then
		minetest.chat_send_player(p_name, "[SpeedJumpTogether] PASS: both speed/jump back to init.")
	else
		minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: final mismatch. Speed=" ..
			sp3 .. ", jump=" .. jp3)
	end
end

--------------------------------------------------------------------------------
-- 14) Test monoid:value() with or without a branch param
--     to confirm it uses the active branch if omitted.
--------------------------------------------------------------------------------
local function test_value_api(player)
	reset_all_monoid_branches(player)
	local p_name = player:get_player_name()

	local init_val = speed:value(player) -- active=main

	-- new branch and add a slowdown
	local br_test = speed:new_branch(player, "value_api_test")
	br_test:add_change(player, 0.5, "slower")
	-- the newly created branch is not active yet, so speed:value(player) remains init_val
	local current_active_val = speed:value(player)
	local named_branch_val = speed:value(player, "value_api_test")

	if math.abs(current_active_val - init_val) < 0.0001 and named_branch_val < init_val then
		minetest.chat_send_player(p_name, "[ValueAPI] PASS: value(player) used 'main'; " ..
			"value(player,'value_api_test') reflected slowdown.")
	else
		minetest.chat_send_player(p_name, "[ValueAPI] FAIL: mismatch. active=" .. current_active_val ..
			", named=" .. named_branch_val .. ", init=" .. init_val)
		return
	end

	-- now switch to that branch, confirm speed:value() uses that branch by default
	speed:checkout_branch(player, "value_api_test")
	local active_switched_val = speed:value(player)
	if math.abs(active_switched_val - named_branch_val) < 0.0001 then
		minetest.chat_send_player(p_name, "[ValueAPI] PASS: after checkout, " ..
			"value(player) matches 'value_api_test' branch value.")
	else
		minetest.chat_send_player(p_name, "[ValueAPI] FAIL: after checkout mismatch. got=" ..
			active_switched_val .. " vs named=" .. named_branch_val)
	end

	-- Revert to default
	speed:checkout_branch(player, "main")
end

--------------------------------------------------------------------------------
-- /test_monoids runs all tests in sequence
--------------------------------------------------------------------------------

local all_tests = {
	{name = "AddRemove",            func = test_speed_add_remove,        delay = 1},
	{name = "BranchIsolation",      func = test_branch_isolation,        delay = 1},
	{name = "BranchConcurrent",     func = test_branch_concurrent,       delay = 2},
	{name = "OnChangeAll",          func = test_onchange_listen_all,     delay = 1},
	{name = "OnChangeActive",       func = test_onchange_listen_active,  delay = 1},
	{name = "BranchNameCheck",      func = test_branch_name_check,       delay = 0},
	{name = "ActiveBranchGet",      func = test_active_branch_check,     delay = 0},
	{name = "BranchDelete",         func = test_branch_delete_check,     delay = 1},
	{name = "GetBranches",          func = test_get_branches,            delay = 0},
	{name = "OnBranchCreateDelete", func = test_on_branch_create_delete, delay = 0},
	{name = "NewBranchMethod",      func = test_new_branch_method,       delay = 0},
	{name = "MainBranchCantDelete", func = test_main_branch_cant_delete, delay = 0},
	{name = "SpeedJumpTogether",    func = test_speed_and_jump_together, delay = 0},
	{name = "ValueAPI",             func = test_value_api,               delay = 0},
}

local function run_tests_sequentially(player, index)
	local p_name = player:get_player_name()
	if index > #all_tests then
		minetest.chat_send_player(p_name, "All tests completed!")
		return
	end

	local info = all_tests[index]
	minetest.chat_send_player(p_name, "\n>>> " .. index .. "/" .. #all_tests .. " Running: " .. info.name .. "...")

	local phys_before = player:get_physics_override()
	info.func(player)

	minetest.after(info.delay, function()
		local again = minetest.get_player_by_name(p_name)
		if not again then return end

		-- Ensure that after running our test, the physics are back to original
		-- Other mods might influence this. Hence, do run the tests in a barebone environment.
		local phys_after = player:get_physics_override()
		local is_equal
		for k, v in pairs(phys_before) do
			local new_v = phys_after[k]
			if type(v) == "number" then
				is_equal = (math.abs(new_v - v) < 0.001)
			else
				is_equal = (new_v == v)
			end
			if not is_equal then
				core.chat_send_player(p_name, ("STOP! Internal test failure. field=%s. before=%s, after=%s")
					:format(k, tostring(v), tostring(new_v)))
				return
			end
		end

		run_tests_sequentially(again, index + 1)
	end)
end

minetest.register_chatcommand("test_monoids", {
	description = "Runs ALL monoid tests in sequence.",
	privs = {monoid_master = true},
	func = function(p_name)
		local player = minetest.get_player_by_name(p_name)
		if player then
			minetest.chat_send_player(p_name, "Starting all monoid tests...")
			run_tests_sequentially(player, 1)
		end
	end,
})
