local ls_helpers = require("helpers")
local exec_lua, feed, exec =
	ls_helpers.exec_lua, ls_helpers.feed, ls_helpers.exec

---@param params { msg: string, fmt: string, args: string, expected: string, opts: string, prolouge: string}
local works = function(params)
	it(params.msg, function()
		-- normally `exec_lua` accepts a table which is passed to the code as `...`, but it
		-- fails if number- and string-keys are mixed ({1,2} works fine, {1, b=2} fails).
		-- So we limit ourselves to just passing strings, which are then turned into tables
		-- while load()ing the function.
		local result = exec_lua(string.format(
			[[
						local args = %s
						local mock_args = {}

						local fake_metatable = {
							__tostring = function(self)
								return self.value
							end,
							get_jump_index = function(self) return nil end
						}
						fake_metatable.__index = fake_metatable

						local fake_node = function(value)
							return setmetatable({value = value}, fake_metatable)
						end
						for key, arg in pairs(args) do
							mock_args[key] = fake_node(arg)
						end
						local result = require("luasnip.extras.fmt").interpolate("%s", mock_args, %s)

						local str_result = ""
						for _, value in pairs(result) do
							str_result = str_result .. tostring(value)
						end
						return str_result
						]],
			params.args,
			params.fmt,
			params.opts
		))
		assert.are.same(params.expected, result)
	end)
end

local fails = function(msg, fmt, args, opts)
	it(msg, function()
		assert.has_error(function()
			exec_lua(
				string.format(
					'return require("luasnip.extras.fmt").interpolate("%s", %s, %s)',
					fmt,
					args,
					opts
				)
			)
		end)
	end)
end

describe("fmt.interpolate", function()
	-- apparently clear() needs to run before anything else...
	ls_helpers.clear()
	-- set in makefile.
	exec("set rtp+=" .. os.getenv("LUASNIP_SOURCE"))

	works({
		msg = "expands with no numbers",
		fmt = "a{}b{}c{}d",
		args = "{ 4, 5, 6 }",
		expected = "a4b5c6d",
	})

	works({
		msg = "expands with explicit numbers",
		fmt = "a{2}b{1}c{3}d",
		args = "{ 4, 5, 6 }",
		expected = "a5b4c6d",
	})

	works({
		msg = "expands with mixed numbering",
		fmt = "a{}b{3}c{}d{2}e",
		args = "{ 1, 2, 3, 4 }",
		expected = "a1b3c4d2e",
	})

	works({
		msg = "expands named placeholders",
		fmt = "a{A}b{B}c{C}d",
		args = "{ A = 1, B = 2, C = 3 }",
		expected = "a1b2c3d",
	})

	works({
		msg = "expands all mixed",
		fmt = "a {A} b {} c {3} d {} e {B} f {A} g {2} h",
		args = "{ 1, 2, 3, 4, A = 10, B = 20 }",
		expected = "a 10 b 1 c 3 d 4 e 20 f 10 g 2 h",
	})

	works({
		msg = "current index changed by numbered nodes",
		fmt = "{} {} {1} {} {}",
		args = "{ 1, 2, 3 }",
		expected = "1 2 1 2 3",
	})

	works({
		msg = "excludes trailing text",
		fmt = "{}abcd{}",
		args = "{ 1, 2 }",
		expected = "1abcd2",
	})

	works({
		msg = "escapes empty double-braces",
		fmt = "a{{}}b{}c{{}}d{}e",
		args = "{ 2, 4 }",
		expected = "a{}b2c{}d4e",
	})

	works({
		msg = "escapes non-empty double-braces",
		fmt = "a{{d}}b{}c",
		args = "{ 2 }",
		expected = "a{d}b2c",
	})

	works({
		msg = "do not trim placeholders with whitespace",
		fmt = "a{ something}b{}c",
		args = '{ 2, [" something"] = 1 }',
		expected = "a1b2c",
	})

	works({
		msg = "replaces nested escaped braces",
		fmt = "a{{{{}}}}b{}c{{ {{ }}}}d",
		args = "{ 2 }",
		expected = "a{{}}b2c{ { }}d",
	})

	works({
		msg = "replaces umatched escaped braces",
		fmt = "a{{{{b{}c",
		args = "{ 2 }",
		expected = "a{{b2c",
	})

	works({
		msg = "replaces in braces inside escaped braces",
		fmt = "a{{{}}}b{{ {}}}c{{{} }}d{{ {} }}e",
		args = "{ 1, 2, 3, 4 }",
		expected = "a{1}b{ 2}c{3 }d{ 4 }e",
	})

	works({
		msg = "repeats node with default options",
		fmt = "{a}{a}",
		args = "{a = 1}",
		expected = "11",
	})

	fails("fails for unbalanced braces", "a{b", {})

	fails("fails for nested braces", "a{ { } }b", {})

	works({
		msg = "can use different delimiters",
		fmt = "foo() { return <>; };",
		args = "{ 10 }",
		expected = "foo() { return 10; };",
		opts = '{ delimiters = "<>" }',
	})

	local delimiters = { "()", "[]", "<>", "%$", "#@", "?!" }
	for _, delims in ipairs(delimiters) do
		local left, right = delims:sub(1, 1), delims:sub(2, 2)
		describe("can use custom delimiters", function()
			works({
				msg = delims,
				fmt = string.format("{ return %s%s; };", left, right),
				args = "{ 10 }",
				expected = "{ return 10; };",
				opts = string.format('{ delimiters = "%s" }', delims),
			})
		end)
	end

	works({
		msg = "can escape custom delimiters",
		fmt = "foo((x)) { return x + (); };",
		args = "{ 10 }",
		expected = "foo(x) { return x + 10; };",
		opts = '{ delimiters = "()" }',
	})

	works({
		msg = "can use named placeholders with custom delimiters",
		fmt = "foo(x) { return x + [y]; };",
		args = "{ y = 10 }",
		expected = "foo(x) { return x + 10; };",
		opts = '{ delimiters = "[]" }',
	})

	fails("dissallows unused list args", "a {} b {} c", "{ 1, 2, 3 }")

	fails(
		"dissallows unused map args",
		"a {A} b {B} c {} d",
		"{ 1, A = 10, B = 20, C = 30 }"
	)

	works({
		msg = "allows unused with strict=false",
		fmt = "a {A} b {B} c {} d",
		args = "{ 1, 2, A = 10, B = 20, C = 30 }",
		expected = "a 10 b 20 c 1 d",
		opts = "{ strict = false }",
	})
end)
