@ 261102.1
current
package fzf

import (
	"bytes"
	"io"
	"os"
	"regexp"
	"strings"
	"testing"
	"text/template"

	"github.com/junegunn/fzf/src/util"
)

func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems [3][]*Item) string {
	replaced, _ := replacePlaceholder(replacePlaceholderParams{
		template:   template,
		stripAnsi:  stripAnsi,
		delimiter:  delimiter,
		printsep:   printsep,
		forcePlus:  forcePlus,
		query:      query,
		allItems:   allItems,
		lastAction: actBackwardDeleteCharEof,
		prompt:     "prompt",
		executor:   util.NewExecutor(""),
	})
	return replaced
}

func TestReplacePlaceholder(t *testing.T) {
	// Pin $SHELL so the quoting style doesn't depend on the test runner's shell
	t.Setenv("SHELL", "cmd")

	item1 := newItem("  foo'bar \x1b[31mbaz\x1b[m")
	items1 := [3][]*Item{{item1}, {item1}, nil}
	items2 := [3][]*Item{
		{newItem("foo'bar \x1b[31mbaz\x1b[m")},
		{newItem("foo'bar \x1b[31mbaz\x1b[m"),
			newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}, nil}

	delim := "'"
	var regex *regexp.Regexp

	var result string
	check := func(expected string) {
		if result != expected {
			t.Errorf("expected: %s, actual: %s", expected, result)
		}
	}
	// helper function that converts template format into string and carries out the check()
	checkFormat := func(format string) {
		type quotes struct{ O, I, S string } // outer, inner quotes, print separator
		unixStyle := quotes{`'`, `'\''`, "\n"}
		windowsStyle := quotes{`^"`, `'`, "\n"}
		var effectiveStyle quotes

		if util.IsWindows() {
			effectiveStyle = windowsStyle
		} else {
			effectiveStyle = unixStyle
		}

		expected := templateToString(format, effectiveStyle)
		check(expected)
	}
	printsep := "\n"

	/*
		Test multiple placeholders and the function parameters.
	*/

	// {}, preserve ansi
	result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")

	// {}, strip ansi
	result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.I}}bar baz{{.O}}")

	// {r}, strip ansi
	result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo   foo'bar baz")

	// {r..}, strip ansi
	result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo foo'bar baz")

	// {}, with multiple items
	result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
	checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")

	// {..}, strip leading whitespaces, preserve ansi
	result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")

	// {..}, strip leading whitespaces, strip ansi
	result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")

	// {q}
	result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")

	// {q}, multiple items
	result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
	checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")

	result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
	checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")

	result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}  foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")

	result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
	checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")

	result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
	checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")

	// forcePlus
	result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
	checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")

	// Whitespace preserving flag with "'" delimiter
	result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.O}}")

	result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}bar baz{{.O}}")

	result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.I}}bar baz{{.O}}")

	result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.I}}bar baz{{.O}}")

	// Whitespace preserving flag with regex delimiter
	regex = regexp.MustCompile(`\w+`)

	result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  {{.O}}")

	result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}{{.I}}{{.O}}")

	result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
	checkFormat("echo {{.O}} {{.O}}")

	// No match
	result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, nil, nil})
	check("echo /")

	// No match, but with selections
	result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, {item1}, nil})
	checkFormat("echo /{{.O}}  foo{{.I}}bar baz{{.O}}")

	// String delimiter
	result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")

	// Regex delimiter
	regex = regexp.MustCompile("[oa]+")
	// foo'bar baz
	result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
	checkFormat("echo {{.O}}  foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")

	/*
		Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
		see: TestParsePlaceholder
	*/
	items3 := [3][]*Item{
		// single line
		{newItem("1a 1b 1c 1d 1e 1f")},
		// multi line
		{newItem("1a 1b 1c 1d 1e 1f"),
			newItem("2a 2b 2c 2d 2e 2f"),
			newItem("3a 3b 3c 3d 3e 3f"),
			newItem("4a 4b 4c 4d 4e 4f"),
			newItem("5a 5b 5c 5d 5e 5f"),
			newItem("6a 6b 6c 6d 6e 6f"),
			newItem("7a 7b 7c 7d 7e 7f")},
		nil,
	}
	stripAnsi := false
	forcePlus := false
	query := "sample query"

	templateToOutput := make(map[string]string)
	templateToFile := make(map[string]string) // same as above, but the file contents will be matched
	// I. item type placeholder
	templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
	templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
	templateToOutput[`{n}`] = `0`
	templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
	templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
	templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
	templateToFile[`{nf}`] = `0{{.S}}`
	templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`

	// II. token type placeholders
	templateToOutput[`{..}`] = templateToOutput[`{}`]
	templateToOutput[`{1..}`] = templateToOutput[`{}`]
	templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
	templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
	templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
	// shorthand for x..x range
	templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
	templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
	templateToOutput[`{-6}`] = templateToOutput[`{1}`]
	// multiple ranges
	templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
	templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
	templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
	templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
	// flags
	templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
	templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
	templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
	templateToFile[`{f1}`] = `1a{{.S}}`
	templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
	templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`

	// III. query type placeholder
	// query flag is not removed after parsing, so it gets doubled
	// while the double q is invalid, it is useful here for testing purposes
	templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
	templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
	templateToOutput[`{fzf:action} {fzf:prompt}`] = `backward-delete-char-eof {{.O}}prompt{{.O}}`

	// IV. escaping placeholder
	templateToOutput[`\{}`] = `{}`
	templateToOutput[`\{q}`] = `{q}`
	templateToOutput[`\{fzf:query}`] = `{fzf:query}`
	templateToOutput[`\{fzf:action}`] = `{fzf:action}`
	templateToOutput[`\{++}`] = `{++}`
	templateToOutput[`{++}`] = templateToOutput[`{+}`]

	for giveTemplate, wantOutput := range templateToOutput {
		result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
		checkFormat(wantOutput)
	}
	for giveTemplate, wantOutput := range templateToFile {
		path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)

		data, err := readFile(path)
		if err != nil {
			t.Errorf("Cannot read the content of the temp file %s.", path)
		}
		result = string(data)

		checkFormat(wantOutput)
	}
}

func TestQuoteEntry(t *testing.T) {
	type quotes struct{ E, O, SQ, DQ, BS, PB string } // standalone escape, outer, single and double quotes, doubled and plain backslash
	unixStyle := quotes{``, `'`, `'\''`, `"`, `\`, `\`}
	windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`, `\`}
	var effectiveStyle quotes

	// Pin $SHELL so the quoting style doesn't depend on the test runner's shell
	t.Setenv("SHELL", "cmd")
	exec := util.NewExecutor("")

	if util.IsWindows() {
		effectiveStyle = windowsStyle
	} else {
		effectiveStyle = unixStyle
	}

	tests := map[string]string{
		`'`:     `{{.O}}{{.SQ}}{{.O}}`,
		`"`:     `{{.O}}{{.DQ}}{{.O}}`,
		`\`:     `{{.O}}{{.BS}}{{.O}}`,
		`\"`:    `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
		`"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,

		`$`:       `{{.O}}${{.O}}`,
		`$HOME`:   `{{.O}}$HOME{{.O}}`,
		`'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,

		`&`:                       `{{.O}}{{.E}}&{{.O}}`,
		`|`:                       `{{.O}}{{.E}}|{{.O}}`,
		`<`:                       `{{.O}}{{.E}}<{{.O}}`,
		`>`:                       `{{.O}}{{.E}}>{{.O}}`,
		`(`:                       `{{.O}}{{.E}}({{.O}}`,
		`)`:                       `{{.O}}{{.E}}){{.O}}`,
		`@`:                       `{{.O}}@{{.O}}`,
		`^`:                       `{{.O}}{{.E}}^{{.O}}`,
		`%`:                       `{{.O}}{{.E}}%{{.O}}`,
		`!`:                       `{{.O}}{{.E}}!{{.O}}`,
		`%USERPROFILE%`:           `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
		`C:\Program Files (x86)\`: `{{.O}}C:{{.PB}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
		`"C:\Program Files"`:      `{{.O}}{{.DQ}}C:{{.PB}}Program Files{{.DQ}}{{.O}}`,
	}

	for input, expected := range tests {
		escaped := exec.QuoteEntry(input)
		expected = templateToString(expected, effectiveStyle)
		if escaped != expected {
			t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
		}
	}
}

// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
func TestUnixCommands(t *testing.T) {
	if util.IsWindows() {
		t.SkipNow()
	}
	tests := []testCase{
		// reference: give{template, query, items}, want{output OR match}

		// 1) working examples

		// paths that does not have to evaluated will work fine, when quoted
		{give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
		{give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
		{give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},

		// only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
		// quoting the tilde is required (to be treated as string)
		{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},

		// 2) problematic examples
		// (not necessarily unexpected)

		// paths that need to expand some part of it won't work (special characters and variables)
		{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
		{give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
	}
	testCommands(t, tests)
}

// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
func TestWindowsCommands(t *testing.T) {
	// XXX Deprecated
	t.SkipNow()

	tests := []testCase{
		// reference: give{template, query, items}, want{output OR match}

		// 1) working examples

		// example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
		{give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
		{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
		// example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
		{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
		// example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
		{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},

		// 2) problematic examples
		// (not necessarily unexpected)

		// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
		{give{`notepad++ -n{1} {2}`, ``, newItems(`12	C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},

		// cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
		// cat: "C:\\test.txt: Invalid argument
		{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
		// cat: "C:\\test.txt": Invalid argument
		{give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},

		// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
		// the temp file contains: `cat "C:\test.txt"`
		// TODO this should actually work
		{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
	}
	testCommands(t, tests)
}

// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
func TestPowershellCommands(t *testing.T) {
	if !util.IsWindows() {
		t.SkipNow()
	}

	tests := []testCase{
		// reference: give{template, query, items}, want{output OR match}

		/*
			You can read each line in the following table as a pipeline that
			consist of series of parsers that act upon your input (col. 1) and
			each cell represents the output value.

			For example:
			 - exec.Command("program.exe", `\''`)
			   - goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
			     - powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
			     - native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
			       - some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)

			Character⁰   CommandLineToArgvW   Powershell commands              Native commands from Powershell   Apps requiring escapes¹    | Being tested below
			----------   ------------------   ------------------------------   -------------------------------   -------------------------- | ------------------
			"            empty string²        missing argument error           ...                               ...                        |
			\"           literal "            unbalanced quote error           ...                               ...                        |
			'\"'         literal '"'          literal "                        empty string                      empty string (match all)   | yes
			'\\\"'       literal '\"'         literal \"                       literal "                         literal "                  |
			----------   ------------------   ------------------------------   -------------------------------   -------------------------- | ------------------
			\            transparent          transparent                      transparent                       regex error                |
			'\'          transparent          literal \                        literal \                         regex error                | yes
			\\           transparent          transparent                      transparent                       literal \                  |
			'\\'         transparent          literal \\                       literal \\                        literal \                  |
			----------   ------------------   ------------------------------   -------------------------------   -------------------------- | ------------------
			'            transparent          unbalanced quote error           ...                               ...                        |
			\'           transparent          literal \ and unb. quote error   ...                               ...                        |
			\''          transparent          literal \ and empty string       literal \                         regex error                | no, but given as example above
			'''          transparent          unbalanced quote error           ...                               ...                        |
			''''         transparent          literal '                        literal '                         literal '                  | yes
			----------   ------------------   ------------------------------   -------------------------------   -------------------------- | ------------------

			⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
			¹: native commands like grep, git grep, ripgrep
			²: interpreted as a grouping quote, affects argument parser and gets removed from the result

			[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
			[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
		*/

		// 1) working examples

		{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
		{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},

		// example of escaping single quotes
		{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},

		// chaining powershells
		{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},

		// 2) problematic examples
		// (not necessarily unexpected)

		// looking for a path string will only work with escaped backslashes
		{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
		// looking for a literal double quote will only work with triple escaped double quotes
		{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},

		// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
		// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
		{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},

		// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
		// to explorer, which will prompt user to pick editing program for the fzf-preview file
		// the temp file contains: `cat "C:\test.txt"`
		// TODO this should actually work
		{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-temp-[0-9]+$`}},
	}

	// to force powershell-style escaping we temporarily set environment variable that fzf honors
	shellBackup := os.Getenv("SHELL")
	os.Setenv("SHELL", "powershell")
	testCommands(t, tests)
	os.Setenv("SHELL", shellBackup)
}

/*
Test typical valid placeholders and parsing of them.

Also since the parser assumes the input is matched with `placeholder` regex,
the regex is tested here as well.
*/
func TestParsePlaceholder(t *testing.T) {
	// give, want pairs
	templates := map[string]string{
		// I. item type placeholder
		`{}`:    `{}`,
		`{+}`:   `{+}`,
		`{n}`:   `{n}`,
		`{+n}`:  `{+n}`,
		`{f}`:   `{f}`,
		`{+nf}`: `{+nf}`,

		// II. token type placeholders
		`{..}`:     `{..}`,
		`{1..}`:    `{1..}`,
		`{..2}`:    `{..2}`,
		`{1..2}`:   `{1..2}`,
		`{-2..-1}`: `{-2..-1}`,
		// shorthand for x..x range
		`{1}`:    `{1}`,
		`{1..1}`: `{1..1}`,
		`{-6}`:   `{-6}`,
		// multiple ranges
		`{1,2}`:         `{1,2}`,
		`{1,2,4}`:       `{1,2,4}`,
		`{1,2..4}`:      `{1,2..4}`,
		`{1..2,-4..-3}`: `{1..2,-4..-3}`,
		// flags
		`{+1}`:      `{+1}`,
		`{+-1}`:     `{+-1}`,
		`{s1}`:      `{s1}`,
		`{f1}`:      `{f1}`,
		`{+s1..2}`:  `{+s1..2}`,
		`{+sf1..2}`: `{+sf1..2}`,

		// III. query type placeholder
		// query flag is not removed after parsing, so it gets doubled
		// while the double q is invalid, it is useful here for testing purposes
		`{q}`:        `{qq}`,
		`{q:1}`:      `{qq:1}`,
		`{q:2..}`:    `{qq:2..}`,
		`{q:..}`:     `{qq:..}`,
		`{q:2..-1}`:  `{qq:2..-1}`,
		`{q:s2..-1}`: `{sqq:2..-1}`, // FIXME

		// IV. escaping placeholder
		`\{}`:   `{}`,
		`\{++}`: `{++}`,
		`{++}`:  `{+}`,
	}

	for giveTemplate, wantTemplate := range templates {
		if !placeholder.MatchString(giveTemplate) {
			t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
			continue
		}

		_, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
		gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]

		if gotTemplate != wantTemplate {
			t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
		}
	}
}

func TestExtractPassthroughs(t *testing.T) {
	for _, middle := range []string{
		"\x1bPtmux;\x1b\x1bbar\x1b\\",
		"\x1bPtmux;\x1b\x1bbar\x1bbar\x1b\\",
		"\x1b]1337;bar\x1b\\",
		"\x1b]1337;bar\x1bbar\x1b\\",
		"\x1b]1337;bar\a",
		"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\",
		"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\\r",
		"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1bbar\x1b\\\r",
		"\x1b_Gm=1;AAAAAAAAA=\x1b\\",
		"\x1b_Gm=1;AAAAAAAAA=\x1b\\\r",
		"\x1b_Gm=1;\x1bAAAAAAAAA=\x1b\\\r",
	} {
		line := "foo" + middle + "baz"
		loc := findPassThrough(line)
		if loc == nil || line[0:loc[0]] != "foo" || line[loc[1]:] != "baz" {
			t.Error("failed to find passthrough")
		}
		garbage := "\x1bPtmux;\x1b]1337;\x1b_Ga=\x1b]1337;bar\x1b."
		line = strings.Repeat("foo"+middle+middle+"baz", 3) + garbage
		passthroughs, result := extractPassThroughs(line)
		if result != "foobazfoobazfoobaz"+garbage || len(passthroughs) != 6 {
			t.Error("failed to extract passthroughs")
		}
	}
}

func TestWrapPassThrough(t *testing.T) {
	for _, passThrough := range []string{
		"\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\", // Already wrapped
		"\x1bP0;1;0q#0;2;0;0;0#0~~@@vv@@~~@\x1b\\",    // Sixel
		"\x1b]1337;File=inline=1:AAAA\a",              // iTerm2
	} {
		if got := wrapPassThrough(passThrough, true); got != passThrough {
			t.Errorf("should have been left alone: %q -> %q", passThrough, got)
		}
	}

	kitty := "\x1b_Ga=d,d=A\x1b\\"
	if got := wrapPassThrough(kitty, false); got != kitty {
		t.Errorf("should have been left alone outside of tmux: %q", got)
	}

	// ESC characters are doubled, and the trailing carriage return is kept
	// outside of the wrapper
	for _, test := range []struct{ input, want string }{
		{kitty, "\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\"},
		{kitty + "\r", "\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\\r"},
		{"\x1b_Gm=1;\x1bAAA=\x1b\\", "\x1bPtmux;\x1b\x1b_Gm=1;\x1b\x1bAAA=\x1b\x1b\\\x1b\\"},
	} {
		if got := wrapPassThrough(test.input, true); got != test.want {
			t.Errorf("expected %q, got %q", test.want, got)
		}
	}
}

func TestIsImagePassThrough(t *testing.T) {
	for _, tc := range []struct {
		given string
		image bool
	}{
		// Kitty
		{"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\", true},
		{"\x1b_Ga=p,i=1\x1b\\", true},
		// The action key is not always first, and a put carries no payload
		{"\x1b_Gi=1,a=p\x1b\\", true},
		{"\x1b_Ga=p\x1b\\", true},
		{"\x1b_Ga=T,f=100\x1b\\\r", true},
		{"\x1b_Gi=1,a=d\x1b\\", false},
		{"\x1b_Ga=d,d=A\x1b\\", false},          // 'kitten icat --clear'
		{"\x1b_Ga=q,i=1\x1b\\", false},          // query
		{"\x1b_Gi=1,f=100\x1b\\", false},        // transmit only, the default action
		{"\x1b_Gm=1;AAAA\x1b\\", false},         // continuation chunk
		{"\x1b_Ga=f,i=1;AAAA\x1b\\", false},     // animation frame
		{"\x1b_Ga=T,U=1,f=32;AAAA\x1b\\", true}, // unicode placeholder
		// Wrapped in the tmux passthrough sequence
		{"\x1bPtmux;\x1b\x1b_Ga=T,f=100\x1b\x1b\\\x1b\\", true},
		{"\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\", false},
		{"\x1bPtmux;\x1b\x1b_Gi=1,a=p\x1b\x1b\\\x1b\\", true},
		// iTerm2
		{"\x1b]1337;File=inline=1:AAAA\a", true},
		{"\x1b]1337;MultipartFile=inline=1\a", true},
		{"\x1b]1337;SetUserVar=foo=YmFy\a", false},
		{"\x1b]1337;CurrentDir=/tmp\a", false},
		// Sixel
		{"\x1bP0;1;0q#0;2;0;0;0#0~~@@vv@@~~@\x1b\\", true},
		{"\x1bPq#0~~\x1b\\", true},
		{"\x1bPtmux;\x1b\x1bP0;1;0q#0~~\x1b\x1b\\\x1b\\", true},
		{"", false},
	} {
		if actual := isImagePassThrough(tc.given); actual != tc.image {
			t.Errorf("expected %v for %q, got %v", tc.image, tc.given, actual)
		}
	}

	if containsImage([]string{"foo", "bar"}) {
		t.Error("plain text carries no image")
	}
	if !containsImage([]string{"foo", "bar\x1b_Ga=T,f=100\x1b\\baz"}) {
		t.Error("failed to find an image in a line")
	}
	if containsImage([]string{"foo\x1b_Ga=d,d=A\x1b\\"}) {
		t.Error("a delete command is not an image")
	}
	// The image is not the first passthrough on the line
	if !containsImage([]string{"\x1b_Ga=d,d=A\x1b\\text\x1b_Ga=T,f=100;AAAA\x1b\\"}) {
		t.Error("failed to look past an earlier passthrough")
	}
}

/* utilities section */

// Item represents one line in fzf UI. Usually it is relative path to files and folders.
func newItem(str string) *Item {
	bytes := []byte(str)
	trimmed, _, _ := extractColor(str, nil, nil)
	return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
}

// Functions tested in this file require array of items (allItems).
// This is helper function.
func newItems(str ...string) [3][]*Item {
	result := make([]*Item, len(str))
	for i, s := range str {
		result[i] = newItem(s)
	}
	return [3][]*Item{result, nil, nil}
}

// (for logging purposes)
func (item *Item) String() string {
	return item.AsString(true)
}

// Helper function to parse, execute and convert "text/template" to string. Panics on error.
func templateToString(format string, data any) string {
	bb := &bytes.Buffer{}

	err := template.Must(template.New("").Parse(format)).Execute(bb, data)
	if err != nil {
		panic(err)
	}

	return bb.String()
}

// ad hoc types for test cases
type give struct {
	template string
	query    string
	allItems [3][]*Item
}
type want struct {
	/*
		Unix:
		The `want.output` string is supposed to be formatted for evaluation by
		`sh -c command` system call.

		Windows:
		The `want.output` string is supposed to be formatted for evaluation by
		`cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
		behaviour, which is more favourable for nesting (possibly escaped)
		special characters. This is the relevant section of `help cmd`:

		...old behavior is to see if the first character is
		a quote character and if so, strip the leading character and
		remove the last quote character on the command line, preserving
		any text after the last quote character.
	*/
	output string // literal output
	match  string // output is matched against this regex (when output is empty string)
}
type testCase struct {
	give
	want
}

func testCommands(t *testing.T, tests []testCase) {
	// common test parameters
	delim := "\t"
	delimiter := Delimiter{str: &delim}
	printsep := ""
	stripAnsi := false
	forcePlus := false

	// evaluate the test cases
	for idx, test := range tests {
		gotOutput := replacePlaceholderTest(
			test.template, stripAnsi, delimiter, printsep, forcePlus,
			test.query,
			test.allItems)
		switch {
		case test.output != "":
			if gotOutput != test.output {
				t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
					idx,
					test.template, test.query, test.allItems,
					gotOutput, test.output)
			}
		case test.match != "":
			wantMatch := strings.ReplaceAll(test.match, `\`, `\\`)
			wantRegex := regexp.MustCompile(wantMatch)
			if !wantRegex.MatchString(gotOutput) {
				t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
					idx,
					test.template, test.query, test.allItems,
					gotOutput, test.match)
			}
		default:
			t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
		}
	}
}

// naive encoder of placeholder flags
func (flags placeholderFlags) encodePlaceholder() string {
	encoded := ""
	if flags.plus {
		encoded += "+"
	}
	if flags.preserveSpace {
		encoded += "s"
	}
	if flags.number {
		encoded += "n"
	}
	if flags.file {
		encoded += "f"
	}
	if flags.forceUpdate { // FIXME
		encoded += "q"
	}
	return encoded
}

// can be replaced with os.ReadFile() in go 1.16+
func readFile(path string) ([]byte, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	data := make([]byte, 0, 128)
	for {
		if len(data) >= cap(data) {
			d := append(data[:cap(data)], 0)
			data = d[:len(data)]
		}

		n, err := file.Read(data[len(data):cap(data)])
		data = data[:len(data)+n]
		if err != nil {
			if err == io.EOF {
				err = nil
			}
			return data, err
		}
	}
}

func TestWordWrapAnsiLine(t *testing.T) {
	term := &Terminal{}

	// Simple wrapping
	result := term.wordWrapAnsiLine("hello world", 7, 2)
	if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
		t.Errorf("Simple: %q", result)
	}

	// No wrapping needed
	result = term.wordWrapAnsiLine("hello", 10, 2)
	if len(result) != 1 || result[0] != "hello" {
		t.Errorf("No wrap: %q", result)
	}

	// ANSI codes preserved across split
	result = term.wordWrapAnsiLine("\x1b[31mhello \x1b[32mworld", 8, 2)
	if len(result) != 2 || result[0] != "\x1b[31mhello" || result[1] != "\x1b[32mworld" {
		t.Errorf("ANSI: %q", result)
	}

	// Long word (no space) - no break, let character wrapping handle it
	result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
	if len(result) != 1 || result[0] != "abcdefghij" {
		t.Errorf("Long word: %q", result)
	}

	// Multiple words with continuation wrapSignWidth
	result = term.wordWrapAnsiLine("aa bb cc dd", 5, 2)
	// max=5 for first line, max=3 for continuations (5-2)
	// "aa bb" (5 wide), split at second space -> "aa bb" | "cc" | "dd"
	if len(result) != 3 || result[0] != "aa bb" || result[1] != "cc" || result[2] != "dd" {
		t.Errorf("Multiple words: %q", result)
	}

	// Empty string
	result = term.wordWrapAnsiLine("", 10, 2)
	if len(result) != 1 || result[0] != "" {
		t.Errorf("Empty: %q", result)
	}

	// OSC 8 hyperlink preserved
	result = term.wordWrapAnsiLine("\x1b]8;;http://example.com\x1b\\click here\x1b]8;;\x1b\\", 8, 2)
	if len(result) != 2 {
		t.Errorf("Hyperlink split count: %d, %q", len(result), result)
	}

	// Tab handling: tab expands to tabstop-aligned width
	term.tabstop = 8
	// "\thi there" - tab at column 0 expands to 8, total "hi" starts at 8
	// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
	result = term.wordWrapAnsiLine("\thi there", 15, 2)
	if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
		t.Errorf("Tab: %q", result)
	}

	// Tab as word boundary: "hello"(5) + tab(3→col8) + "world"(5) = 13 total
	// maxWidth=13: fits without wrapping
	result = term.wordWrapAnsiLine("hello\tworld", 13, 2)
	if len(result) != 1 || result[0] != "hello\tworld" {
		t.Errorf("Tab no wrap: %q", result)
	}
	// maxWidth=12: 13 > 12, wraps at tab
	result = term.wordWrapAnsiLine("hello\tworld", 12, 2)
	if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
		t.Errorf("Tab wrap: %q", result)
	}
}

func TestSplitOnIND(t *testing.T) {
	term := &Terminal{tabstop: 8}
	for _, tc := range []struct {
		name string
		line string
		want []string
	}{
		{
			// Nothing to do, so the caller keeps the line as it read it
			name: "no IND",
			line: "foo\n",
			want: nil,
		},
		{
			// chafa: CUB returns to the column the row started on
			name: "rows at column 0",
			line: "AAA\x1b[3D\x1bDBBB\x1b[3D\x1bDCCC\n",
			want: []string{"AAA\x1b[3D\n", "BBB\x1b[3D\n", "CCC\n"},
		},
		{
			// 'printf "  "; chafa ...'
			name: "indented rows",
			line: "  AAA\x1b[3D\x1bDBBB\x1b[3D\x1bDCCC\n",
			want: []string{"  AAA\x1b[3D\n", "  BBB\x1b[3D\n", "  CCC\n"},
		},
		{
			name: "tab indent expands to the tab stop",
			line: "\tAAA\x1b[3D\x1bDBBB\x1b[3D\x1bDCCC\n",
			want: []string{"\tAAA\x1b[3D\n", "        BBB\x1b[3D\n", "        CCC\n"},
		},
		{
			// IND on its own keeps the column
			name: "bare IND",
			line: "AB\x1bDCD\n",
			want: []string{"AB\n", "  CD\n"},
		},
		{
			name: "CUB without a parameter moves back one",
			line: "AB\x1b[D\x1bDCD\n",
			want: []string{"AB\x1b[D\n", " CD\n"},
		},
		{
			name: "CUB past the left edge is clamped",
			line: "AB\x1b[9D\x1bDCD\n",
			want: []string{"AB\x1b[9D\n", "CD\n"},
		},
		{
			name: "SGR codes take no column",
			line: "\x1b[31mAB\x1b[m\x1b[2D\x1bDCD\n",
			want: []string{"\x1b[31mAB\x1b[m\x1b[2D\n", "CD\n"},
		},
		{
			name: "pass-throughs take no column",
			line: "\x1b_Ga=T,c=2,r=1\x1b\\AB\x1b[2D\x1bDCD\n",
			want: []string{"\x1b_Ga=T,c=2,r=1\x1b\\AB\x1b[2D\n", "CD\n"},
		},
	} {
		got := term.splitOnIND(tc.line)
		if len(got) != len(tc.want) {
			t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
			continue
		}
		for idx, line := range got {
			if line != tc.want[idx] {
				t.Errorf("%s: line %d: got %q, want %q", tc.name, idx, line, tc.want[idx])
			}
		}
	}
}

func TestNextValue(t *testing.T) {
	tests := []struct {
		expr    string
		current string
		want    string
		rotated string
	}{
		{"a", "x", "a", "a"},
		{"a", "a", "a", "a"},
		{"a|b|c", "x", "a", "b|c|a"},
		{"a|b|c", "a", "b", "c|a|b"},
		{"a|b|c", "b", "c", "a|b|c"},
		{"a|b|c", "c", "a", "b|c|a"},
		{"a|a|b", "a", "b", "a|a|b"},
		{"a|b|b", "b", "a", "b|b|a"},
		{"a|a", "a", "a", "a|a"},
		{"a|b|", "", "a", "b||a"},
		{"|a", "", "a", "|a"},
		{"a|bad|b", "a", "b", "a|bad|b"},
		{"bad|a", "x", "a", "bad|a"},
		{"a|bad", "a", "a", "a|bad"},
		// An invalid value is never current
		{"a|bad|b", "bad", "a", "bad|b|a"},
		// Nothing to pick: the first valid value
		{"bad|a", "a", "a", "bad|a"},
		{"bad", "x", "bad", "bad"},
		// The last value is checked first
		{"b|a|c|a", "a", "b", "a|c|a|b"},
	}
	isValid := func(v string) bool { return v != "bad" }
	for _, tc := range tests {
		got, rotated := nextValue(tc.expr, func(v string) bool { return v == tc.current }, isValid)
		if got != tc.want || rotated != tc.rotated {
			t.Errorf("nextValue(%q) with current %q: got (%q, %q), want (%q, %q)",
				tc.expr, tc.current, got, rotated, tc.want, tc.rotated)
		}
	}
}

func TestNextValueRepeated(t *testing.T) {
	// Presses with a rotated list reach every position, even with a repeated value
	expr := "1|2|1|3"
	current := "1"
	got := []string{}
	for range 6 {
		current, expr = nextValue(expr, func(v string) bool { return v == current }, nil)
		got = append(got, current)
	}
	if want := "2 1 3 1 2 1"; strings.Join(got, " ") != want {
		t.Errorf("got %q, want %q", strings.Join(got, " "), want)
	}
}