# ------------------------------------------------------------------------------
# Used to keep track of info about each test ran.
# ------------------------------------------------------------------------------
class Test:
	# indicator if it passed or not.  defaults to true since it takes only
	# one failure to make it not pass.  _fail in gut will set this.
	var passed = true
	# the name of the function
	var name = ""
	# flag to know if the name has been printed yet.
	var has_printed_name = false
	# the number of arguments the method has
	var arg_count = 0
	# The number of asserts in the test
	var assert_count = 0
	# if the test has been marked pending at anypont during
	# execution.
	var pending = false


# ------------------------------------------------------------------------------
# This holds all the meta information for a test script.  It contains the
# name of the inner class and an array of Test "structs".
#
# This class also facilitates all the exporting and importing of tests.
# ------------------------------------------------------------------------------
class TestScript:
	var inner_class_name = null
	var tests = []
	var path = null
	var _utils = null
	var _lgr = null

	func _init(utils = null, logger = null):
		_utils = utils
		_lgr = logger

	func to_s():
		var to_return = path
		if inner_class_name != null:
			to_return += str(".", inner_class_name)
		to_return += "\n"
		for i in range(tests.size()):
			to_return += str("  ", tests[i].name, "\n")
		return to_return

	func get_new():
		return load_script().new()

	func load_script():
		#print('loading:  ', get_full_name())
		var to_return = load(path)
		if inner_class_name != null:
			# If we wanted to do inner classes in inner classses
			# then this would have to become some kind of loop or recursive
			# call to go all the way down the chain or this class would
			# have to change to hold onto the loaded class instead of
			# just path information.
			to_return = to_return.get(inner_class_name)
		return to_return

	func get_filename_and_inner():
		var to_return = get_filename()
		if inner_class_name != null:
			to_return += "." + inner_class_name
		return to_return

	func get_full_name():
		var to_return = path
		if inner_class_name != null:
			to_return += "." + inner_class_name
		return to_return

	func get_filename():
		return path.get_file()

	func has_inner_class():
		return inner_class_name != null

	# Note:  although this no longer needs to export the inner_class names since
	#        they are pulled from metadata now, it is easier to leave that in
	#        so we don't have to cut the export down to unique script names.
	func export_to(config_file, section):
		config_file.set_value(section, "path", path)
		config_file.set_value(section, "inner_class", inner_class_name)
		var names = []
		for i in range(tests.size()):
			names.append(tests[i].name)
		config_file.set_value(section, "tests", names)

	func _remap_path(source_path):
		var to_return = source_path
		if !_utils.file_exists(source_path):
			_lgr.debug("Checking for remap for:  " + source_path)
			var remap_path = source_path.get_basename() + ".gd.remap"
			if _utils.file_exists(remap_path):
				var cf = ConfigFile.new()
				cf.load(remap_path)
				to_return = cf.get_value("remap", "path")
			else:
				_lgr.warn("Could not find remap file " + remap_path)
		return to_return

	func import_from(config_file, section):
		path = config_file.get_value(section, "path")
		path = _remap_path(path)
		# Null is an acceptable value, but you can't pass null as a default to
		# get_value since it thinks you didn't send a default...then it spits
		# out red text.  This works around that.
		var inner_name = config_file.get_value(section, "inner_class", "Placeholder")
		if inner_name != "Placeholder":
			inner_class_name = inner_name
		else:  # just being explicit
			inner_class_name = null

	func get_test_named(name):
		return _utils.search_array(tests, "name", name)


# ------------------------------------------------------------------------------
# start test_collector, I don't think I like the name.
# ------------------------------------------------------------------------------
var scripts = []
var _test_prefix = "test_"
var _test_class_prefix = "Test"

var _utils = load("res://addons/gut/utils.gd").get_instance()
var _lgr = _utils.get_logger()


func _does_inherit_from_test(thing):
	var base_script = thing.get_base_script()
	var to_return = false
	if base_script != null:
		var base_path = base_script.get_path()
		if base_path == "res://addons/gut/test.gd":
			to_return = true
		else:
			to_return = _does_inherit_from_test(base_script)
	return to_return


func _populate_tests(test_script):
	var methods = test_script.load_script().get_script_method_list()
	for i in range(methods.size()):
		var name = methods[i]["name"]
		if name.begins_with(_test_prefix):
			var t = Test.new()
			t.name = name
			t.arg_count = methods[i]["args"].size()
			test_script.tests.append(t)


func _get_inner_test_class_names(loaded):
	var inner_classes = []
	var const_map = loaded.get_script_constant_map()
	for key in const_map:
		var thing = const_map[key]
		if _utils.is_gdscript(thing):
			if key.begins_with(_test_class_prefix):
				if _does_inherit_from_test(thing):
					inner_classes.append(key)
				else:
					_lgr.warn(
						str(
							"Ignoring Inner Class ",
							key,
							" because it does not extend res://addons/gut/test.gd"
						)
					)

			# This could go deeper and find inner classes within inner classes
			# but requires more experimentation.  Right now I'm keeping it at
			# one level since that is what the previous version did and there
			# has been no demand for deeper nesting.
			# _populate_inner_test_classes(thing)
	return inner_classes


func _parse_script(test_script):
	var inner_classes = []
	var scripts_found = []

	var loaded = load(test_script.path)
	if _does_inherit_from_test(loaded):
		_populate_tests(test_script)
		scripts_found.append(test_script.path)
		inner_classes = _get_inner_test_class_names(loaded)

	for i in range(inner_classes.size()):
		var loaded_inner = loaded.get(inner_classes[i])
		if _does_inherit_from_test(loaded_inner):
			var ts = TestScript.new(_utils, _lgr)
			ts.path = test_script.path
			ts.inner_class_name = inner_classes[i]
			_populate_tests(ts)
			scripts.append(ts)
			scripts_found.append(test_script.path + "[" + inner_classes[i] + "]")

	return scripts_found


# -----------------
# Public
# -----------------
func add_script(path):
	# SHORTCIRCUIT
	if has_script(path):
		return []

	var f = File.new()
	# SHORTCIRCUIT
	if !f.file_exists(path):
		_lgr.error("Could not find script:  " + path)
		return

	var ts = TestScript.new(_utils, _lgr)
	ts.path = path
	scripts.append(ts)
	return _parse_script(ts)


func clear():
	scripts.clear()


func has_script(path):
	var found = false
	var idx = 0
	while idx < scripts.size() and !found:
		if scripts[idx].get_full_name() == path:
			found = true
		else:
			idx += 1
	return found


func export_tests(path):
	var success = true
	var f = ConfigFile.new()
	for i in range(scripts.size()):
		scripts[i].export_to(f, str("TestScript-", i))
	var result = f.save(path)
	if result != OK:
		_lgr.error(str("Could not save exported tests to [", path, "].  Error code:  ", result))
		success = false
	return success


func import_tests(path):
	var success = false
	var f = ConfigFile.new()
	var result = f.load(path)
	if result != OK:
		_lgr.error(str("Could not load exported tests from [", path, "].  Error code:  ", result))
	else:
		var sections = f.get_sections()
		for key in sections:
			var ts = TestScript.new(_utils, _lgr)
			ts.import_from(f, key)
			_populate_tests(ts)
			scripts.append(ts)
		success = true
	return success


func get_script_named(name):
	return _utils.search_array(scripts, "get_filename_and_inner", name)


func get_test_named(script_name, test_name):
	var s = get_script_named(script_name)
	if s != null:
		return s.get_test_named(test_name)
	else:
		return null


func to_s():
	var to_return = ""
	for i in range(scripts.size()):
		to_return += scripts[i].to_s() + "\n"
	return to_return


# ---------------------
# Accessors
# ---------------------
func get_logger():
	return _lgr


func set_logger(logger):
	_lgr = logger


func get_test_prefix():
	return _test_prefix


func set_test_prefix(test_prefix):
	_test_prefix = test_prefix


func get_test_class_prefix():
	return _test_class_prefix


func set_test_class_prefix(test_class_prefix):
	_test_class_prefix = test_class_prefix