package fzf

import (
	"fmt"
	"testing"

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

func TestDelimiterRegex(t *testing.T) {
	// Valid regex
	delim := delimiterRegexp(".")
	if delim.regex == nil || delim.str != nil {
		t.Error(delim)
	}
	// Broken regex -> string
	delim = delimiterRegexp("[0-9")
	if delim.regex != nil || *delim.str != "[0-9" {
		t.Error(delim)
	}
	// Valid regex
	delim = delimiterRegexp("[0-9]")
	if delim.regex.String() != "[0-9]" || delim.str != nil {
		t.Error(delim)
	}
	// Tab character
	delim = delimiterRegexp("\t")
	if delim.regex != nil || *delim.str != "\t" {
		t.Error(delim)
	}
	// Tab expression
	delim = delimiterRegexp("\\t")
	if delim.regex != nil || *delim.str != "\t" {
		t.Error(delim)
	}
	// Tabs -> regex
	delim = delimiterRegexp("\t+")
	if delim.regex == nil || delim.str != nil {
		t.Error(delim)
	}
}

func TestDelimiterRegexString(t *testing.T) {
	delim := delimiterRegexp("*")
	tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
	if delim.regex != nil ||
		tokens[0].text.ToString() != "-*" ||
		tokens[1].text.ToString() != "--*" ||
		tokens[2].text.ToString() != "---*" ||
		tokens[3].text.ToString() != "*" ||
		tokens[4].text.ToString() != "---" {
		t.Errorf("%s %s %d", delim, tokens, len(tokens))
	}
}

func TestDelimiterRegexRegex(t *testing.T) {
	delim := delimiterRegexp("--\\*")
	tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
	if delim.str != nil ||
		tokens[0].text.ToString() != "-*--*" ||
		tokens[1].text.ToString() != "---*" ||
		tokens[2].text.ToString() != "*---" {
		t.Errorf("%s %d", tokens, len(tokens))
	}
}

func TestSplitNth(t *testing.T) {
	{
		ranges := splitNth("..")
		if len(ranges) != 1 ||
			ranges[0].begin != rangeEllipsis ||
			ranges[0].end != rangeEllipsis {
			t.Errorf("%s", ranges)
		}
	}
	{
		ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
		if len(ranges) != 10 ||
			ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
			ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
			ranges[2].begin != 2 || ranges[2].end != 3 ||
			ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
			ranges[4].begin != -3 || ranges[4].end != -2 ||
			ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
			ranges[6].begin != 2 || ranges[6].end != 2 ||
			ranges[7].begin != -2 || ranges[7].end != -2 ||
			ranges[8].begin != 2 || ranges[8].end != -2 ||
			ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
			t.Errorf("%s", ranges)
		}
	}
}

func TestIrrelevantNth(t *testing.T) {
	{
		opts := defaultOptions()
		words := []string{"--nth", "..", "-x"}
		parseOptions(opts, words)
		postProcessOptions(opts)
		if len(opts.Nth) != 0 {
			t.Errorf("nth should be empty: %s", opts.Nth)
		}
	}
	for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
		{
			opts := defaultOptions()
			parseOptions(opts, words)
			postProcessOptions(opts)
			if len(opts.Nth) != 0 {
				t.Errorf("nth should be empty: %s", opts.Nth)
			}
		}
		{
			opts := defaultOptions()
			words = append(words, "-x")
			parseOptions(opts, words)
			postProcessOptions(opts)
			if len(opts.Nth) != 2 {
				t.Errorf("nth should not be empty: %s", opts.Nth)
			}
		}
	}
}

func TestParseKeys(t *testing.T) {
	pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "")
	check := func(i int, s string) {
		if pairs[i] != s {
			t.Errorf("%s != %s", pairs[i], s)
		}
	}
	if len(pairs) != 11 {
		t.Error(11)
	}
	check(curses.CtrlZ, "ctrl-z")
	check(curses.AltZ, "alt-z")
	check(curses.F2, "f2")
	check(curses.AltZ+'@', "@")
	check(curses.AltA, "Alt-a")
	check(curses.AltZ+'!', "!")
	check(curses.CtrlA+'g'-'a', "ctrl-G")
	check(curses.AltZ+'J', "J")
	check(curses.AltZ+'g', "g")
	check(curses.AltEnter, "ALT-enter")
	check(curses.AltSpace, "alt-SPACE")

	// Synonyms
	pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
	if len(pairs) != 9 {
		t.Error(9)
	}
	check(curses.CtrlM, "Return")
	check(curses.AltZ+' ', "space")
	check(curses.Tab, "tab")
	check(curses.BTab, "btab")
	check(curses.ESC, "esc")
	check(curses.Up, "up")
	check(curses.Down, "down")
	check(curses.Left, "left")
	check(curses.Right, "right")

	pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
	if len(pairs) != 11 {
		t.Error(11)
	}
	check(curses.Tab, "Ctrl-I")
	check(curses.PgUp, "page-up")
	check(curses.PgDn, "Page-Down")
	check(curses.Home, "Home")
	check(curses.End, "End")
	check(curses.AltBS, "Alt-BSpace")
	check(curses.SLeft, "shift-left")
	check(curses.SRight, "shift-right")
	check(curses.BTab, "shift-tab")
	check(curses.CtrlM, "Enter")
	check(curses.BSpace, "bspace")
}

func TestParseKeysWithComma(t *testing.T) {
	checkN := func(a int, b int) {
		if a != b {
			t.Errorf("%d != %d", a, b)
		}
	}
	check := func(pairs map[int]string, i int, s string) {
		if pairs[i] != s {
			t.Errorf("%s != %s", pairs[i], s)
		}
	}

	pairs := parseKeyChords(",", "")
	checkN(len(pairs), 1)
	check(pairs, curses.AltZ+',', ",")

	pairs = parseKeyChords(",,a,b", "")
	checkN(len(pairs), 3)
	check(pairs, curses.AltZ+'a', "a")
	check(pairs, curses.AltZ+'b', "b")
	check(pairs, curses.AltZ+',', ",")

	pairs = parseKeyChords("a,b,,", "")
	checkN(len(pairs), 3)
	check(pairs, curses.AltZ+'a', "a")
	check(pairs, curses.AltZ+'b', "b")
	check(pairs, curses.AltZ+',', ",")

	pairs = parseKeyChords("a,,,b", "")
	checkN(len(pairs), 3)
	check(pairs, curses.AltZ+'a', "a")
	check(pairs, curses.AltZ+'b', "b")
	check(pairs, curses.AltZ+',', ",")

	pairs = parseKeyChords("a,,,b,c", "")
	checkN(len(pairs), 4)
	check(pairs, curses.AltZ+'a', "a")
	check(pairs, curses.AltZ+'b', "b")
	check(pairs, curses.AltZ+'c', "c")
	check(pairs, curses.AltZ+',', ",")

	pairs = parseKeyChords(",,,", "")
	checkN(len(pairs), 1)
	check(pairs, curses.AltZ+',', ",")
}

func TestBind(t *testing.T) {
	check := func(action actionType, expected actionType) {
		if action != expected {
			t.Errorf("%d != %d", action, expected)
		}
	}
	checkString := func(action string, expected string) {
		if action != expected {
			t.Errorf("%d != %d", action, expected)
		}
	}
	keymap := defaultKeymap()
	execmap := make(map[int]string)
	check(actBeginningOfLine, keymap[curses.CtrlA])
	parseKeymap(keymap, execmap,
		"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
			"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
			"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
			",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
	check(actKillLine, keymap[curses.CtrlA])
	check(actToggleSort, keymap[curses.CtrlB])
	check(actPageUp, keymap[curses.AltZ+'c'])
	check(actAbort, keymap[curses.AltZ+','])
	check(actAccept, keymap[curses.AltZ+':'])
	check(actPageDown, keymap[curses.AltZ])
	check(actExecute, keymap[curses.F1])
	check(actExecute, keymap[curses.F2])
	check(actExecute, keymap[curses.F3])
	check(actExecute, keymap[curses.F4])
	checkString("ls {}", execmap[curses.F1])
	checkString("echo {}, {}, {}", execmap[curses.F2])
	checkString("echo '({})'", execmap[curses.F3])
	checkString("less {}", execmap[curses.F4])
	checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA])
	checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB])
	checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X'])

	for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
		parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
		checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
	}

	parseKeymap(keymap, execmap, "f1:abort")
	check(actAbort, keymap[curses.F1])
}

func TestColorSpec(t *testing.T) {
	theme := curses.Dark256
	dark := parseTheme(theme, "dark")
	if *dark != *theme {
		t.Errorf("colors should be equivalent")
	}
	if dark == theme {
		t.Errorf("point should not be equivalent")
	}

	light := parseTheme(theme, "dark,light")
	if *light == *theme {
		t.Errorf("should not be equivalent")
	}
	if *light != *curses.Light256 {
		t.Errorf("colors should be equivalent")
	}
	if light == theme {
		t.Errorf("point should not be equivalent")
	}

	customized := parseTheme(theme, "fg:231,bg:232")
	if customized.Fg != 231 || customized.Bg != 232 {
		t.Errorf("color not customized")
	}
	if *curses.Dark256 == *customized {
		t.Errorf("colors should not be equivalent")
	}
	customized.Fg = curses.Dark256.Fg
	customized.Bg = curses.Dark256.Bg
	if *curses.Dark256 == *customized {
		t.Errorf("colors should now be equivalent")
	}

	customized = parseTheme(theme, "fg:231,dark,bg:232")
	if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
		t.Errorf("color not customized")
	}
	if customized.UseDefault {
		t.Errorf("not using default colors")
	}
	if !curses.Dark256.UseDefault {
		t.Errorf("using default colors")
	}
}

func TestParseNilTheme(t *testing.T) {
	var theme *curses.ColorTheme
	newTheme := parseTheme(theme, "prompt:12")
	if newTheme != nil {
		t.Errorf("color is disabled. keep it that way.")
	}
	newTheme = parseTheme(theme, "prompt:12,dark,prompt:13")
	if newTheme.Prompt != 13 {
		t.Errorf("color should now be enabled and customized")
	}
}

func TestDefaultCtrlNP(t *testing.T) {
	check := func(words []string, key int, expected actionType) {
		opts := defaultOptions()
		parseOptions(opts, words)
		postProcessOptions(opts)
		if opts.Keymap[key] != expected {
			t.Error()
		}
	}
	check([]string{}, curses.CtrlN, actDown)
	check([]string{}, curses.CtrlP, actUp)

	check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept)
	check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)

	hist := "--history=/tmp/foo"
	check([]string{hist}, curses.CtrlN, actNextHistory)
	check([]string{hist}, curses.CtrlP, actPreviousHistory)

	check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept)
	check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory)

	check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory)
	check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
}

func optsFor(words ...string) *Options {
	opts := defaultOptions()
	parseOptions(opts, words)
	postProcessOptions(opts)
	return opts
}

func TestToggle(t *testing.T) {
	opts := optsFor()
	if opts.ToggleSort {
		t.Error()
	}

	opts = optsFor("--bind=a:toggle-sort")
	if !opts.ToggleSort {
		t.Error()
	}

	opts = optsFor("--bind=a:toggle-sort", "--bind=a:up")
	if opts.ToggleSort {
		t.Error()
	}
}

func TestPreviewOpts(t *testing.T) {
	opts := optsFor()
	if !(opts.Preview.command == "" &&
		opts.Preview.hidden == false &&
		opts.Preview.position == posRight &&
		opts.Preview.size.percent == true &&
		opts.Preview.size.size == 50) {
		t.Error()
	}
	opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden")
	if !(opts.Preview.command == "cat {}" &&
		opts.Preview.hidden == true &&
		opts.Preview.position == posLeft &&
		opts.Preview.size.percent == false &&
		opts.Preview.size.size == 15+2) {
		t.Error(opts.Preview)
	}

	opts = optsFor("--preview-window=left:15:hidden", "--preview-window=down")
	if !(opts.Preview.command == "" &&
		opts.Preview.hidden == false &&
		opts.Preview.position == posDown &&
		opts.Preview.size.percent == true &&
		opts.Preview.size.size == 50) {
		t.Error(opts.Preview)
	}
}
