package filter_test

import (
	"bufio"
	"compress/bzip2"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/restic/restic/internal/filter"
)

var matchTests = []struct {
	pattern string
	path    string
	match   bool
}{
	{"", "", true},
	{"", "foo", true},
	{"", "/x/y/z/foo", true},
	{"*.go", "/foo/bar/test.go", true},
	{"*.c", "/foo/bar/test.go", false},
	{"*", "/foo/bar/test.go", true},
	{"**", "/foo/bar/test.go", true},
	{"foo*", "/foo/bar/test.go", true},
	{"bar*", "/foo/bar/test.go", true},
	{"/bar*", "/foo/bar/test.go", false},
	{"bar/*", "/foo/bar/test.go", true},
	{"baz/*", "/foo/bar/test.go", false},
	{"bar/test.go", "/foo/bar/test.go", true},
	{"bar/*.go", "/foo/bar/test.go", true},
	{"ba*/*.go", "/foo/bar/test.go", true},
	{"bb*/*.go", "/foo/bar/test.go", false},
	{"test.*", "/foo/bar/test.go", true},
	{"tesT.*", "/foo/bar/test.go", false},
	{"bar/*", "/foo/bar/baz", true},
	{"bar", "/foo/bar", true},
	{"/foo/bar", "/foo/bar", true},
	{"/foo/bar/", "/foo/bar", true},
	{"/foo/bar", "/foo/baz", false},
	{"/foo/bar", "/foo/baz/", false},
	{"/foo///bar", "/foo/bar", true},
	{"/foo/../bar", "/foo/bar", false},
	{"/foo/../bar", "/bar", true},
	{"/foo", "/foo/baz", true},
	{"/foo/", "/foo/baz", true},
	{"/foo/*", "/foo", false},
	{"/foo/*", "/foo/baz", true},
	{"bar", "/foo/bar/baz", true},
	{"bar", "/foo/bar/test.go", true},
	{"/foo/*test.*", "/foo/bar/test.go", false},
	{"/foo/*/test.*", "/foo/bar/test.go", true},
	{"/foo/*/bar/test.*", "/foo/bar/test.go", false},
	{"/*/*/bar/test.*", "/foo/bar/test.go", false},
	{"/*/*/bar/test.*", "/foo/bar/baz/test.go", false},
	{"/*/*/baz/test.*", "/foo/bar/baz/test.go", true},
	{"/*/foo/bar/test.*", "/foo/bar/baz/test.go", false},
	{"/*/foo/bar/test.*", "/foo/bar/baz/test.go", false},
	{"/foo/bar/test.*", "bar/baz/test.go", false},
	{"/x/y/bar/baz/test.*", "bar/baz/test.go", false},
	{"/x/y/bar/baz/test.c", "bar/baz/test.go", false},
	{"baz/test.*", "bar/baz/test.go", true},
	{"baz/tesT.*", "bar/baz/test.go", false},
	{"test.go", "bar/baz/test.go", true},
	{"*.go", "bar/baz/test.go", true},
	{"*.c", "bar/baz/test.go", false},
	{"sdk", "/foo/bar/sdk", true},
	{"sdk", "/foo/bar/sdk/test/sdk_foo.go", true},
	{
		"sdk/*/cpp/*/*vars*.html",
		"/usr/share/doc/libreoffice/sdk/docs/cpp/ref/a00517.html",
		false,
	},
	{"foo/**/bar/*.go", "/home/user/foo/work/special/project/bar/test.go", true},
	{"foo/**/bar/*.go", "/home/user/foo/bar/test.go", true},
	{"foo/**/bar/*.go", "x/foo/bar/test.go", true},
	{"foo/**/bar/*.go", "foo/bar/test.go", true},
	{"foo/**/bar/*.go", "foo/bar/baz/bar/test.go", true},
	{"foo/**/bar/*.go", "/home/user/foo/test.c", false},
	{"foo/**/bar/*.go", "bar/foo/main.go", false},
	{"foo/**/bar/*.go", "/foo/bar/main.go", true},
	{"foo/**/bar/*.go", "bar/main.go", false},
	{"foo/**/bar", "/home/user/foo/x/y/bar", true},
	{"foo/**/bar", "/home/user/foo/x/y/bar/main.go", true},
	{"foo/**/bar/**/x", "/home/user/foo/bar/x", true},
	{"foo/**/bar/**/x", "/home/user/foo/blaaa/blaz/bar/shared/work/x", true},
	{"user/**/important*", "/home/user/work/x/y/hidden/x", false},
	{"user/**/hidden*/**/c", "/home/user/work/x/y/hidden/z/a/b/c", true},
	{"c:/foo/*test.*", "c:/foo/bar/test.go", false},
	{"c:/foo", "c:/foo/bar", true},
	{"c:/foo/", "c:/foo/bar", true},
	{"c:/foo/*/test.*", "c:/foo/bar/test.go", true},
	{"c:/foo/*/bar/test.*", "c:/foo/bar/test.go", false},
}

func testpattern(t *testing.T, pattern, path string, shouldMatch bool) {
	match, err := filter.Match(pattern, path)
	if err != nil {
		t.Errorf("test pattern %q failed: expected no error for path %q, but error returned: %v",
			pattern, path, err)
	}

	if match != shouldMatch {
		t.Errorf("test: filter.Match(%q, %q): expected %v, got %v",
			pattern, path, shouldMatch, match)
	}
}

func TestMatch(t *testing.T) {
	for _, test := range matchTests {
		t.Run("", func(t *testing.T) {
			testpattern(t, test.pattern, test.path, test.match)
		})

		// Test with native path separator
		if filepath.Separator != '/' {
			pattern := strings.Replace(test.pattern, "/", string(filepath.Separator), -1)
			// Test with pattern as native
			t.Run("pattern-native", func(t *testing.T) {
				testpattern(t, pattern, test.path, test.match)
			})

			path := strings.Replace(test.path, "/", string(filepath.Separator), -1)
			t.Run("path-native", func(t *testing.T) {
				// Test with path as native
				testpattern(t, test.pattern, path, test.match)
			})

			t.Run("both-native", func(t *testing.T) {
				// Test with both pattern and path as native
				testpattern(t, pattern, path, test.match)
			})
		}
	}
}

var childMatchTests = []struct {
	pattern string
	path    string
	match   bool
}{
	{"", "", true},
	{"", "/foo", true},
	{"", "/x/y/z/foo", true},
	{"foo/bar", "/foo", true},
	{"baz/bar", "/foo", true},
	{"foo", "/foo/bar", true},
	{"bar", "/foo", true},
	{"baz", "/foo/bar", true},
	{"*", "/foo", true},
	{"*", "/foo/bar", true},
	{"/foo/bar", "/foo", true},
	{"/foo/bar/baz", "/foo", true},
	{"/foo/bar/baz", "/foo/bar", true},
	{"/foo/bar/baz", "/foo/baz", false},
	{"/foo/**/baz", "/foo/bar/baz", true},
	{"/foo/**/baz", "/foo/bar/baz/blah", true},
	{"/foo/**/qux", "/foo/bar/baz/qux", true},
	{"/foo/**/qux", "/foo/bar/baz", true},
	{"/foo/**/qux", "/foo/bar/baz/boo", true},
	{"/foo/**", "/foo/bar/baz", true},
	{"/foo/**", "/foo/bar", true},
	{"foo/**/bar/**/x", "/home/user/foo", true},
	{"foo/**/bar/**/x", "/home/user/foo/bar", true},
	{"foo/**/bar/**/x", "/home/user/foo/blaaa/blaz/bar/shared/work/x", true},
	{"/foo/*/qux", "/foo/bar", true},
	{"/foo/*/qux", "/foo/bar/boo", false},
	{"/foo/*/qux", "/foo/bar/boo/xx", false},
	{"/baz/bar", "/foo", false},
	{"/foo", "/foo/bar", true},
	{"/*", "/foo", true},
	{"/*", "/foo/bar", true},
	{"/foo", "/foo/bar", true},
	{"/**", "/foo", true},
	{"/*/**", "/foo", true},
	{"/*/**", "/foo/bar", true},
	{"/*/bar", "/foo", true},
	{"/bar/*", "/foo", false},
	{"/foo/*/baz", "/foo/bar", true},
	{"/foo/*/baz", "/foo/baz", true},
	{"/foo/*/baz", "/bar/baz", false},
	{"/**/*", "/foo", true},
	{"/**/bar", "/foo/bar", true},
}

func testchildpattern(t *testing.T, pattern, path string, shouldMatch bool) {
	match, err := filter.ChildMatch(pattern, path)
	if err != nil {
		t.Errorf("test child pattern %q failed: expected no error for path %q, but error returned: %v",
			pattern, path, err)
	}

	if match != shouldMatch {
		t.Errorf("test: filter.ChildMatch(%q, %q): expected %v, got %v",
			pattern, path, shouldMatch, match)
	}
}

func TestChildMatch(t *testing.T) {
	for _, test := range childMatchTests {
		t.Run("", func(t *testing.T) {
			testchildpattern(t, test.pattern, test.path, test.match)
		})

		// Test with native path separator
		if filepath.Separator != '/' {
			pattern := strings.Replace(test.pattern, "/", string(filepath.Separator), -1)
			// Test with pattern as native
			t.Run("pattern-native", func(t *testing.T) {
				testchildpattern(t, pattern, test.path, test.match)
			})

			path := strings.Replace(test.path, "/", string(filepath.Separator), -1)
			t.Run("path-native", func(t *testing.T) {
				// Test with path as native
				testchildpattern(t, test.pattern, path, test.match)
			})

			t.Run("both-native", func(t *testing.T) {
				// Test with both pattern and path as native
				testchildpattern(t, pattern, path, test.match)
			})
		}
	}
}

func ExampleMatch() {
	match, _ := filter.Match("*.go", "/home/user/file.go")
	fmt.Printf("match: %v\n", match)
	// Output:
	// match: true
}

func ExampleMatch_wildcards() {
	match, _ := filter.Match("/home/[uU]ser/?.go", "/home/user/F.go")
	fmt.Printf("match: %v\n", match)
	// Output:
	// match: true
}

var filterListTests = []struct {
	patterns   []string
	path       string
	match      bool
	childMatch bool
}{
	{[]string{}, "/foo/bar/test.go", false, false},
	{[]string{"*.go"}, "/foo/bar/test.go", true, true},
	{[]string{"*.go"}, "/foo/bar", false, true},
	{[]string{"*.c"}, "/foo/bar/test.go", false, true},
	{[]string{"*.go", "*.c"}, "/foo/bar/test.go", true, true},
	{[]string{"*"}, "/foo/bar/test.go", true, true},
	{[]string{"x"}, "/foo/bar/test.go", false, true},
	{[]string{"?"}, "/foo/bar/test.go", false, true},
	{[]string{"?", "x"}, "/foo/bar/x", true, true},
	{[]string{"/*/*/bar/test.*"}, "/foo/bar/test.go", false, false},
	{[]string{"/*/*/bar/test.*"}, "/foo/bar/bar", false, true},
	{[]string{"/*/*/bar/test.*", "*.go"}, "/foo/bar/test.go", true, true},
	{[]string{"", "*.c"}, "/foo/bar/test.go", false, true},
	{[]string{"!**", "*.go"}, "/foo/bar/test.go", true, true},
	{[]string{"!**", "*.c"}, "/foo/bar/test.go", false, true},
	{[]string{"/foo/*/test.*", "!*.c"}, "/foo/bar/test.c", false, false},
	{[]string{"/foo/*/test.*", "!*.c"}, "/foo/bar/test.go", true, true},
	{[]string{"/foo/*/*", "!test.*", "*.c"}, "/foo/bar/test.go", false, true},
	{[]string{"/foo/*/*", "!test.*", "*.c"}, "/foo/bar/test.c", true, true},
	{[]string{"/foo/*/*", "!test.*", "*.c"}, "/foo/bar/file.go", true, true},
	{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/other/test.go", true, true},
	{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/bar", false, false},
	{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/bar/test.go", false, false},
	{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/bar/test.go/child", false, false},
	{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar", "/foo/bar/test*"}, "/foo/bar/test.go/child", true, true},
	{[]string{"/foo/bar/*"}, "/foo", false, true},
	{[]string{"/foo/bar/*", "!/foo/bar/[a-m]*"}, "/foo", false, true},
	{[]string{"/foo/**/test.c"}, "/foo/bar/foo/bar/test.c", true, true},
	{[]string{"/foo/*/test.c"}, "/foo/bar/foo/bar/test.c", false, false},
}

func TestList(t *testing.T) {
	for i, test := range filterListTests {
		patterns := filter.ParsePatterns(test.patterns)
		match, err := filter.List(patterns, test.path)
		if err != nil {
			t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v",
				i, test.patterns, err)
			continue
		}

		if match != test.match {
			t.Errorf("test %d: filter.List(%q, %q): expected %v, got %v",
				i, test.patterns, test.path, test.match, match)
		}

		match, childMatch, err := filter.ListWithChild(patterns, test.path)
		if err != nil {
			t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v",
				i, test.patterns, err)
			continue
		}

		if match != test.match || childMatch != test.childMatch {
			t.Errorf("test %d: filter.ListWithChild(%q, %q): expected %v, %v, got %v, %v",
				i, test.patterns, test.path, test.match, test.childMatch, match, childMatch)
		}
	}
}

func ExampleList() {
	patterns := filter.ParsePatterns([]string{"*.c", "*.go"})
	match, _ := filter.List(patterns, "/home/user/file.go")
	fmt.Printf("match: %v\n", match)
	// Output:
	// match: true
}

func TestInvalidStrs(t *testing.T) {
	_, err := filter.Match("test", "")
	if err == nil {
		t.Error("Match accepted invalid path")
	}

	_, err = filter.ChildMatch("test", "")
	if err == nil {
		t.Error("ChildMatch accepted invalid path")
	}

	patterns := []string{"test"}
	_, err = filter.List(filter.ParsePatterns(patterns), "")
	if err == nil {
		t.Error("List accepted invalid path")
	}
}

func TestInvalidPattern(t *testing.T) {
	patterns := []string{"test/["}
	_, err := filter.List(filter.ParsePatterns(patterns), "test/example")
	if err == nil {
		t.Error("List accepted invalid pattern")
	}

	patterns = []string{"test/**/["}
	_, err = filter.List(filter.ParsePatterns(patterns), "test/example")
	if err == nil {
		t.Error("List accepted invalid pattern")
	}
}

func extractTestLines(t testing.TB) (lines []string) {
	f, err := os.Open("testdata/libreoffice.txt.bz2")
	if err != nil {
		t.Fatal(err)
	}

	defer func() {
		if err := f.Close(); err != nil {
			t.Fatal(err)
		}
	}()

	sc := bufio.NewScanner(bzip2.NewReader(f))
	for sc.Scan() {
		lines = append(lines, sc.Text())
	}

	return lines
}

func TestFilterPatternsFile(t *testing.T) {
	lines := extractTestLines(t)

	var testPatterns = []struct {
		pattern string
		hits    uint
	}{
		{"*.html", 18249},
		{"sdk", 22186},
		{"sdk/*/cpp/*/*vars.html", 3},
	}

	for _, test := range testPatterns {
		var c uint
		for _, line := range lines {
			match, err := filter.Match(test.pattern, line)
			if err != nil {
				t.Error(err)
				continue
			}

			if match {
				c++
				// fmt.Printf("pattern %q, line %q\n", test.pattern, line)
			}
		}

		if c != test.hits {
			t.Errorf("wrong number of hits for pattern %q: want %d, got %d",
				test.pattern, test.hits, c)
		}
	}
}

func BenchmarkFilterLines(b *testing.B) {
	pattern := "sdk/*/cpp/*/*vars.html"
	lines := extractTestLines(b)
	var c uint

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		c = 0
		for _, line := range lines {
			match, err := filter.Match(pattern, line)
			if err != nil {
				b.Fatal(err)
			}

			if match {
				c++
			}
		}

		if c != 3 {
			b.Fatalf("wrong number of matches: expected 3, got %d", c)
		}
	}
}

func BenchmarkFilterPatterns(b *testing.B) {
	lines := extractTestLines(b)
	modlines := make([]string, 200)
	for i, line := range lines {
		if i >= len(modlines) {
			break
		}
		modlines[i] = line + "-does-not-match"
	}
	tests := []struct {
		name     string
		patterns []filter.Pattern
		matches  uint
	}{
		{"Relative", filter.ParsePatterns([]string{
			"does-not-match",
			"sdk/*",
			"*.html",
		}), 22185},
		{"Absolute", filter.ParsePatterns([]string{
			"/etc",
			"/home/*/test",
			"/usr/share/doc/libreoffice/sdk/docs/java",
		}), 150},
		{"Wildcard", filter.ParsePatterns([]string{
			"/etc/**/example",
			"/home/**/test",
			"/usr/**/java",
		}), 150},
		{"ManyNoMatch", filter.ParsePatterns(modlines), 0},
	}

	for _, test := range tests {
		b.Run(test.name, func(b *testing.B) {
			var c uint
			b.ReportAllocs()
			b.ResetTimer()

			for i := 0; i < b.N; i++ {
				c = 0
				for _, line := range lines {
					match, err := filter.List(test.patterns, line)
					if err != nil {
						b.Fatal(err)
					}

					if match {
						c++
					}
				}

				if c != test.matches {
					b.Fatalf("wrong number of matches: expected %d, got %d", test.matches, c)
				}
			}
		})
	}
}
