@ 261102.1
current
package fzf

import (
	"math"
	"math/rand"
	"slices"
	"sort"
	"testing"

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

func withIndex(i *Item, index int) *Item {
	(*i).text.Index = int32(index)
	return i
}

func TestOffsetSort(t *testing.T) {
	offsets := []Offset{
		{3, 5}, {2, 7},
		{1, 3}, {2, 9}}
	slices.SortFunc(offsets, compareOffsets)

	if offsets[0][0] != 1 || offsets[0][1] != 3 ||
		offsets[1][0] != 2 || offsets[1][1] != 7 ||
		offsets[2][0] != 2 || offsets[2][1] != 9 ||
		offsets[3][0] != 3 || offsets[3][1] != 5 {
		t.Error("Invalid order:", offsets)
	}
}

func TestRankComparison(t *testing.T) {
	rank := func(vals ...uint16) Result {
		return Result{
			points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
			item:   &Item{text: util.Chars{Index: int32(vals[4])}}}
	}
	if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
		!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
		!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) ||
		!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
		t.Error("Invalid order")
	}

	if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) ||
		!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
		!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) ||
		!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
		t.Error("Invalid order (tac)")
	}
}

// Match length, string length, index
func TestResultRank(t *testing.T) {
	// FIXME global
	sortCriteria = []criterion{byScore, byLength}

	str := []rune("foo")
	item1 := buildResult(
		withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2)
	if item1.points[3] != math.MaxUint16-2 || // Bonus
		item1.points[2] != 3 || // Length
		item1.points[1] != 0 || // Unused
		item1.points[0] != 0 || // Unused
		item1.item.Index() != 1 {
		t.Error(item1)
	}
	// Only differ in index
	item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2)

	items := []Result{item1, item2}
	sort.Sort(ByRelevance(items))
	if items[0] != item2 || items[1] != item1 {
		t.Error(items)
	}

	items = []Result{item2, item1, item1, item2}
	sort.Sort(ByRelevance(items))
	if items[0] != item2 || items[1] != item2 ||
		items[2] != item1 || items[3] != item1 {
		t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
	}

	// Sort by relevance
	item3 := buildResult(
		withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
	item4 := buildResult(
		withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
	item5 := buildResult(
		withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
	item6 := buildResult(
		withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
	items = []Result{item1, item2, item3, item4, item5, item6}
	sort.Sort(ByRelevance(items))
	if !(items[0] == item6 && items[1] == item5 &&
		items[2] == item4 && items[3] == item3 &&
		items[4] == item2 && items[5] == item1) {
		t.Error(items, item1, item2, item3, item4, item5, item6)
	}
}

func TestChunkTiebreak(t *testing.T) {
	// FIXME global
	sortCriteria = []criterion{byScore, byChunk}

	score := 100
	test := func(input string, offset Offset, chunk string) {
		item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score)
		if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) {
			t.Error(item.points)
		}
	}
	test("hello foobar goodbye", Offset{8, 9}, "foobar")
	test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye")
	test("hello foobar goodbye", Offset{0, 1}, "hello")
	test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD
}

func TestColorOffset(t *testing.T) {
	// ------------ 20 ----  --  ----
	//   ++++++++        ++++++++++
	// --++++++++--    --++++++++++---

	offsets := []Offset{{5, 15}, {10, 12}, {25, 35}}
	item := Result{
		item: &Item{
			colors: &[]ansiOffset{
				{[2]int32{0, 20}, ansiState{1, 5, -1, 0, -1, nil}},
				{[2]int32{22, 27}, ansiState{2, 6, -1, tui.Bold, -1, nil}},
				{[2]int32{30, 32}, ansiState{3, 7, -1, 0, -1, nil}},
				{[2]int32{33, 40}, ansiState{4, 8, -1, tui.Bold, -1, nil}}}}}

	colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
	colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
	colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, 0, false)
	assert := func(idx int, b int32, e int32, c tui.ColorPair) {
		o := colors[idx]
		if o.offset[0] != b || o.offset[1] != e || o.color != c {
			t.Error(o, b, e, c)
		}
	}
	// [{[0 5] {1 5 0}} {[5 15] {99 199 0}} {[15 20] {1 5 0}}
	//  {[22 25] {2 6 1}} {[25 27] {99 199 1}} {[27 30] {99 199 0}}
	//  {[30 32] {99 199 0}} {[32 33] {99 199 0}} {[33 35] {99 199 1}}
	//  {[35 40] {4 8 1}}]
	assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
	assert(1, 5, 15, colMatch)
	assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
	assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
	assert(4, 25, 27, colMatch.WithAttr(tui.Bold))
	assert(5, 27, 30, colMatch)
	assert(6, 30, 32, colMatch)
	assert(7, 32, 33, colMatch) // TODO: Should we merge consecutive blocks?
	assert(8, 33, 35, colMatch.WithAttr(tui.Bold))
	assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))

	colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
	colUnderline := tui.NewColorPair(-1, -1, tui.Underline)

	nthOffsets := []Offset{{37, 39}, {42, 45}}
	for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
		colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, 0, false)

		// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
		//  {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
		//  {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
		//  {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}]
		assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
		assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
		assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
		assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
		assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
		assert(5, 27, 30, colUnderline)
		assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
		assert(7, 32, 33, colUnderline)
		assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
		assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
		expected := tui.Bold | attr
		if attr == tui.AttrRegular {
			expected = tui.Bold
		}
		assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
		assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
	}

	// Test nthOverlay: simulates nth:regular with current-fg:underline
	// The overlay (underline) should survive even though nth:regular clears attrs.
	// Precedence: fg < nth < current-fg
	colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.AttrRegular, tui.Underline, false)

	// nth regions should have Underline (from overlay), not cleared by AttrRegular
	// Non-nth regions keep colBase attrs (AttrUndefined)
	assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
	assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
	assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
	assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
	assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
	assert(5, 27, 30, colUnderline)
	assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
	assert(7, 32, 33, colUnderline)
	assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
	assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
	// nth region within ANSI bold: AttrRegular clears, overlay adds Underline back
	assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
	assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))

	// Test nthOverlay with additive attrs: nth:strikethrough with selected-fg:bold
	colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.StrikeThrough, tui.Bold, false)

	// Non-nth entries unchanged from overlay=0 case
	assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
	assert(5, 27, 30, colUnderline) // match only, no nth
	assert(7, 32, 33, colUnderline) // match only, no nth
	// nth region within ANSI bold: StrikeThrough|Bold merged with ANSI Bold
	assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.StrikeThrough))
}

func TestRadixSortResults(t *testing.T) {
	sortCriteria = []criterion{byScore, byLength}

	rng := rand.New(rand.NewSource(42))

	for _, n := range []int{128, 256, 500, 1000} {
		for _, tac := range []bool{false, true} {
			// Build items with random points and indices
			items := make([]*Item, n)
			for i := range items {
				items[i] = &Item{text: util.Chars{Index: int32(i)}}
			}

			results := make([]Result, n)
			for i := range results {
				results[i] = Result{
					item: items[i],
					points: [4]uint16{
						uint16(rng.Intn(256)),
						uint16(rng.Intn(256)),
						uint16(rng.Intn(256)),
						uint16(rng.Intn(256)),
					},
				}
			}

			// Make some duplicates to test stability
			for i := 0; i < n/4; i++ {
				j := rng.Intn(n)
				k := rng.Intn(n)
				results[j].points = results[k].points
			}

			// Copy for reference sort
			expected := make([]Result, n)
			copy(expected, results)
			if tac {
				sort.Sort(ByRelevanceTac(expected))
			} else {
				sort.Sort(ByRelevance(expected))
			}

			// Radix sort
			var scratch []Result
			scratch = radixSortResults(results, tac, scratch)

			for i := range results {
				if results[i] != expected[i] {
					t.Errorf("n=%d tac=%v: mismatch at index %d: got item %d, want item %d",
						n, tac, i, results[i].item.Index(), expected[i].item.Index())
					break
				}
			}
		}
	}
}

func TestPathnameTiebreak(t *testing.T) {
	// FIXME global
	sortCriteria = []criterion{byScore, byPathname}

	score := 100
	test := func(input string, offset Offset, expected uint16) {
		for _, chars := range []util.Chars{util.ToChars([]byte(input)), util.RunesToChars([]rune(input))} {
			item := buildResult(withIndex(&Item{text: chars}, 1), []Offset{offset}, score)
			if item.points[3] != math.MaxUint16-uint16(score) || item.points[2] != expected {
				t.Error(input, item.points, expected)
			}
		}
	}

	// Match in the file name
	test("x/foo/foo.txt", Offset{6, 9}, 1)
	// Match in the directory path
	test("x/foo/aa.txt", Offset{2, 5}, math.MaxUint16)

	// Offsets are rune indexes, so a multi-byte character before the last
	// delimiter must not shift the delimiter position
	test("一x/foo/foo.txt", Offset{7, 10}, 1)
	test("一x/foo/aa.txt", Offset{3, 6}, math.MaxUint16)
}