// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Benchmark results:
//
// BenchmarkMatcher-12    	 1000000	      1615 ns/op	  30.95 MB/s	       0 B/op	       0 allocs/op
package fuzzy_test

import (
	"bytes"
	"fmt"
	"math"
	"testing"

	"golang.org/x/tools/gopls/internal/fuzzy"
)

type comparator struct {
	f     func(val, ref float32) bool
	descr string
}

var (
	eq = comparator{
		f: func(val, ref float32) bool {
			return val == ref
		},
		descr: "==",
	}
	ge = comparator{
		f: func(val, ref float32) bool {
			return val >= ref
		},
		descr: ">=",
	}
	gt = comparator{
		f: func(val, ref float32) bool {
			return val > ref
		},
		descr: ">",
	}
)

func (c comparator) eval(val, ref float32) bool {
	return c.f(val, ref)
}

func (c comparator) String() string {
	return c.descr
}

type scoreTest struct {
	candidate string
	comparator
	ref float32
}

var matcherTests = []struct {
	pattern string
	tests   []scoreTest
}{
	{
		pattern: "",
		tests: []scoreTest{
			{"def", eq, 1},
			{"Ab stuff c", eq, 1},
		},
	},
	{
		pattern: "abc",
		tests: []scoreTest{
			{"def", eq, 0},
			{"abd", eq, 0},
			{"abc", ge, 0},
			{"Abc", ge, 0},
			{"Ab stuff c", ge, 0},
		},
	},
	{
		pattern: "Abc",
		tests: []scoreTest{
			{"def", eq, 0},
			{"abd", eq, 0},
			{"abc", ge, 0},
			{"Abc", ge, 0},
			{"Ab stuff c", ge, 0},
		},
	},
	{
		pattern: "U",
		tests: []scoreTest{
			{"ErrUnexpectedEOF", gt, 0},
			{"ErrUnexpectedEOF.Error", eq, 0},
		},
	},
}

func TestScore(t *testing.T) {
	for _, tc := range matcherTests {
		m := fuzzy.NewMatcher(tc.pattern)
		for _, sct := range tc.tests {
			score := m.Score(sct.candidate)
			if !sct.comparator.eval(score, sct.ref) {
				t.Errorf("m.Score(%q) = %.2g, want %s %v", sct.candidate, score, sct.comparator, sct.ref)
			}
		}
	}
}

var compareCandidatesTestCases = []struct {
	pattern string
	// In `[][]string{{"foo", "bar"}, {"baz"}}`,
	// "foo" and "bar" must have same score, "baz" must be strictly higher scoring.
	orderedCandidates [][]string
}{
	{
		pattern: "Foo",
		orderedCandidates: [][]string{
			{"Barfoo"},
			{"Faoo"},
			{"F_o_o"},
			{"FaoFooa", "BarFoo"},
			{"F__oo", "F_oo"},
			{"FooA", "FooBar", "Foo"},
		},
	},
	{
		pattern: "U",
		orderedCandidates: [][]string{
			{"ErrUnexpectedEOF.Error"},
			{"ErrUnexpectedEOF"},
		},
	},
	{
		pattern: "N",
		orderedCandidates: [][]string{
			{"name"},
			{"Name"},
		},
	},
}

func TestCompareCandidateScores(t *testing.T) {
	for _, tc := range compareCandidatesTestCases {
		m := fuzzy.NewMatcher(tc.pattern)

		var prevScore float32
		var prevCandGroup []string
		for i, candGroup := range tc.orderedCandidates {
			var groupScore float32
			for j, cand := range candGroup {
				score := m.Score(cand)
				if j > 0 && score != groupScore {
					t.Fatalf("score %f of %q different than group", score, cand)
				}
				groupScore = score
			}

			if i > 0 && prevScore >= groupScore {
				t.Errorf("%s[=%v] is not scored higher than %s[=%v]", candGroup, groupScore, prevCandGroup, prevScore)
			}
			if groupScore < 0 || groupScore > 1 {
				t.Errorf("%s score is %v; want value between [0, 1]", candGroup, groupScore)
			}
			prevScore = groupScore
			prevCandGroup = candGroup
		}
	}
}

var fuzzyMatcherTestCases = []struct {
	p    string
	str  string
	want string
}{
	{p: "foo", str: "abc::foo", want: "abc::[foo]"},
	{p: "foo", str: "foo.foo", want: "foo.[foo]"},
	{p: "foo", str: "fo_oo.o_oo", want: "[fo]_oo.[o]_oo"},
	{p: "foo", str: "fo_oo.fo_oo", want: "fo_oo.[fo]_[o]o"},
	{p: "fo_o", str: "fo_oo.o_oo", want: "[f]o_oo.[o_o]o"},
	{p: "fOO", str: "fo_oo.o_oo", want: "[f]o_oo.[o]_[o]o"},
	{p: "tedit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]"},
	{p: "TEdit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]"},
	{p: "Tedit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]"},
	{p: "Tedit", str: "foo.Textedit", want: "foo.[Te]xte[dit]"},
	{p: "TEdit", str: "foo.Textedit", want: ""},
	{p: "te", str: "foo.Textedit", want: "foo.[Te]xtedit"},
	{p: "ee", str: "foo.Textedit", want: ""}, // short middle of the word match
	{p: "ex", str: "foo.Textedit", want: "foo.T[ex]tedit"},
	{p: "exdi", str: "foo.Textedit", want: ""},  // short middle of the word match
	{p: "exdit", str: "foo.Textedit", want: ""}, // short middle of the word match
	{p: "extdit", str: "foo.Textedit", want: "foo.T[ext]e[dit]"},
	{p: "e", str: "foo.Textedit", want: "foo.T[e]xtedit"},
	{p: "E", str: "foo.Textedit", want: "foo.T[e]xtedit"},
	{p: "ed", str: "foo.Textedit", want: "foo.Text[ed]it"},
	{p: "edt", str: "foo.Textedit", want: ""}, // short middle of the word match
	{p: "edit", str: "foo.Textedit", want: "foo.Text[edit]"},
	{p: "edin", str: "foo.TexteditNum", want: "foo.Text[edi]t[N]um"},
	{p: "n", str: "node.GoNodeMax", want: "[n]ode.GoNodeMax"},
	{p: "N", str: "node.GoNodeMax", want: "[n]ode.GoNodeMax"},
	{p: "completio", str: "completion", want: "[completio]n"},
	{p: "completio", str: "completion.None", want: "[completio]n.None"},
}

func TestFuzzyMatcherRanges(t *testing.T) {
	for _, tc := range fuzzyMatcherTestCases {
		matcher := fuzzy.NewMatcher(tc.p)
		score := matcher.Score(tc.str)
		if tc.want == "" {
			if score > 0 {
				t.Errorf("Score(%s, %s) = %v; want: <= 0", tc.p, tc.str, score)
			}
			continue
		}
		if score < 0 {
			t.Errorf("Score(%s, %s) = %v, want: > 0", tc.p, tc.str, score)
			continue
		}
		got := highlightMatches(tc.str, matcher)
		if tc.want != got {
			t.Errorf("highlightMatches(%s, %s) = %v, want: %v", tc.p, tc.str, got, tc.want)
		}
	}
}

var scoreTestCases = []struct {
	p    string
	str  string
	want float64
}{
	// Score precision up to five digits. Modify if changing the score, but make sure the new values
	// are reasonable.
	{p: "abc", str: "abc", want: 1},
	{p: "abc", str: "Abc", want: 1},
	{p: "abc", str: "Abcdef", want: 1},
	{p: "strc", str: "StrCat", want: 1},
	{p: "abc_def", str: "abc_def_xyz", want: 1},
	{p: "abcdef", str: "abc_def_xyz", want: 0.91667},
	{p: "abcxyz", str: "abc_def_xyz", want: 0.91667},
	{p: "sc", str: "StrCat", want: 0.75},
	{p: "abc", str: "AbstrBasicCtor", want: 0.83333},
	{p: "foo", str: "abc::foo", want: 0.91667},
	{p: "afoo", str: "abc::foo", want: 0.9375},
	{p: "abr", str: "abc::bar", want: 0.5},
	{p: "br", str: "abc::bar", want: 0.25},
	{p: "aar", str: "abc::bar", want: 0.41667},
	{p: "edin", str: "foo.TexteditNum", want: 0.125},
	{p: "ediu", str: "foo.TexteditNum", want: 0},
	// We want the next two items to have roughly similar scores.
	{p: "up", str: "unique_ptr", want: 0.75},
	{p: "up", str: "upper_bound", want: 1},
}

func TestScores(t *testing.T) {
	for _, tc := range scoreTestCases {
		matcher := fuzzy.NewMatcher(tc.p)
		got := math.Round(float64(matcher.Score(tc.str))*1e5) / 1e5
		if got != tc.want {
			t.Errorf("Score(%s, %s) = %v, want: %v", tc.p, tc.str, got, tc.want)
		}
	}
}

func highlightMatches(str string, matcher *fuzzy.Matcher) string {
	matches := matcher.MatchedRanges()

	var buf bytes.Buffer
	index := 0
	for i := 0; i < len(matches)-1; i += 2 {
		s, e := matches[i], matches[i+1]
		fmt.Fprintf(&buf, "%s[%s]", str[index:s], str[s:e])
		index = e
	}
	buf.WriteString(str[index:])
	return buf.String()
}

func BenchmarkMatcher(b *testing.B) {
	pattern := "Foo"
	candidates := []string{
		"F_o_o",
		"Barfoo",
		"Faoo",
		"F__oo",
		"F_oo",
		"FaoFooa",
		"BarFoo",
		"FooA",
		"FooBar",
		"Foo",
	}

	matcher := fuzzy.NewMatcher(pattern)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for _, c := range candidates {
			matcher.Score(c)
		}
	}
	var numBytes int
	for _, c := range candidates {
		numBytes += len(c)
	}
	b.SetBytes(int64(numBytes))
}
