/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package filters

import (
	"reflect"
	"strings"
	"testing"
)

func TestFilters(t *testing.T) {
	type cEntry struct {
		Name   string
		Other  string
		Labels map[string]string
	}

	corpusS := []cEntry{
		{
			Name: "foo",
			Labels: map[string]string{
				"foo": "true",
			},
		},
		{
			Name: "bar",
		},
		{
			Name: "foo",
			Labels: map[string]string{
				"foo":                "present",
				"more complex label": "present",
			},
		},
		{
			Name: "bar",
			Labels: map[string]string{
				"bar": "true",
			},
		},
		{
			Name: "fooer",
			Labels: map[string]string{
				"more complex label with \\ and \"": "present",
			},
		},
		{
			Name: "fooer",
			Labels: map[string]string{
				"more complex label with \\ and \".post": "present",
			},
		},
		{
			Name:  "baz",
			Other: "too complex, yo",
		},
		{
			Name:  "bazo",
			Other: "abc",
		},
		{
			Name: "compound",
			Labels: map[string]string{
				"foo": "omg_asdf.asdf-qwer",
			},
		},
	}

	var corpus []any
	for _, entry := range corpusS {
		corpus = append(corpus, entry)
	}

	// adapt shows an example of how to build an adaptor function for a type.
	adapt := func(o any) Adaptor {
		obj := o.(cEntry)
		return AdapterFunc(func(fieldpath []string) (string, bool) {
			switch fieldpath[0] {
			case "name":
				return obj.Name, len(obj.Name) > 0
			case "other":
				return obj.Other, len(obj.Other) > 0
			case "labels":
				value, ok := obj.Labels[strings.Join(fieldpath[1:], ".")]
				return value, ok
			}

			return "", false
		})
	}

	for _, testcase := range []struct {
		name      string
		input     string
		expected  []any
		errString string
	}{
		{
			name:     "Empty",
			input:    "",
			expected: corpus,
		},
		{
			name:     "Present",
			input:    "name",
			expected: corpus,
		},
		{
			name:  "LabelPresent",
			input: "labels.foo",
			expected: []any{
				corpus[0],
				corpus[2],
				corpus[8],
			},
		},
		{
			name:  "NameAndLabelPresent",
			input: "labels.foo,name",
			expected: []any{
				corpus[0],
				corpus[2],
				corpus[8],
			},
		},
		{
			name:  "LabelValue",
			input: "labels.foo==true",
			expected: []any{
				corpus[0],
			},
		},
		{
			name:  "LabelValuePunctuated",
			input: "labels.foo==omg_asdf.asdf-qwer",
			expected: []any{
				corpus[8],
			},
		},
		{
			name:      "LabelValueNoAltQuoting",
			input:     "labels.|foo|==omg_asdf.asdf-qwer",
			errString: "filters: parse error: [labels. >|||< foo|==omg_asdf.asdf-qwer]: invalid quote encountered",
		},
		{
			name:  "Name",
			input: "name==bar",
			expected: []any{
				corpus[1],
				corpus[3],
			},
		},
		{
			name:  "NameNotEqual",
			input: "name!=bar",
			expected: []any{
				corpus[0],
				corpus[2],
				corpus[4],
				corpus[5],
				corpus[6],
				corpus[7],
				corpus[8],
			},
		},
		{
			name:  "NameAndLabelPresent",
			input: "name==bar,labels.bar",
			expected: []any{
				corpus[3],
			},
		},
		{
			name:  "QuotedValue",
			input: "other==\"too complex, yo\"",
			expected: []any{
				corpus[6],
			},
		},
		{
			name:  "RegexpValue",
			input: "other~=[abc]+,name!=foo",
			expected: []any{
				corpus[6],
				corpus[7],
			},
		},
		{
			name:  "RegexpQuotedValue",
			input: "other~=/[abc]+/,name!=foo",
			expected: []any{
				corpus[6],
				corpus[7],
			},
		},
		{
			name:  "RegexpQuotedValue",
			input: "other~=/[abc]{1,2}/,name!=foo",
			expected: []any{
				corpus[6],
				corpus[7],
			},
		},
		{
			name:  "RegexpQuotedValueGarbage",
			input: "other~=/[abc]{0,1}\"\\//,name!=foo",
			// valid syntax, but doesn't match anything
		},
		{
			name:  "NameAndLabelValue",
			input: "name==bar,labels.bar==true",
			expected: []any{
				corpus[3],
			},
		},
		{
			name:  "NameAndLabelValueNoMatch",
			input: "name==bar,labels.bar==wrong",
		},
		{
			name:  "LabelQuotedFieldPathPresent",
			input: `name==foo,labels."more complex label"`,
			expected: []any{
				corpus[2],
			},
		},
		{
			name:  "LabelQuotedFieldPathPresentWithQuoted",
			input: `labels."more complex label with \\ and \""==present`,
			expected: []any{
				corpus[4],
			},
		},
		{
			name:  "LabelQuotedFieldPathPresentWithQuotedEmbed",
			input: `labels."more complex label with \\ and \"".post==present`,
			expected: []any{
				corpus[5],
			},
		},
		{
			name:      "LabelQuotedFieldPathPresentWithQuotedEmbedInvalid",
			input:     `labels.?"more complex label with \\ and \"".post==present`,
			errString: `filters: parse error: [labels. >|?|< "more complex label with \\ and \"".post==present]: expected field or quoted`,
		},
		{
			name:      "TrailingComma",
			input:     "name==foo,",
			errString: `filters: parse error: [name==foo,]: expected field or quoted`,
		},
		{
			name:      "TrailingFieldSeparator",
			input:     "labels.",
			errString: `filters: parse error: [labels.]: expected field or quoted`,
		},
		{
			name:      "MissingValue",
			input:     "image~=,id?=?fbaq",
			errString: `filters: parse error: [image~= >|,|< id?=?fbaq]: expected value or quoted`,
		},
		{
			name:      "FieldQuotedLiteralNotTerminated",
			input:     "labels.ns/key==value",
			errString: `filters: parse error: [labels.ns >|/|< key==value]: quoted literal not terminated`,
		},
		{
			name:      "ValueQuotedLiteralNotTerminated",
			input:     "labels.key==/value",
			errString: `filters: parse error: [labels.key== >|/|< value]: quoted literal not terminated`,
		},
	} {
		t.Run(testcase.name, func(t *testing.T) {
			filter, err := Parse(testcase.input)
			if testcase.errString != "" {
				if err == nil {
					t.Fatalf("expected an error, but received nil")
				}
				if err.Error() != testcase.errString {
					t.Fatalf("error %v != %v", err, testcase.errString)
				}

				return
			}
			if err != nil {
				t.Fatal(err)
			}

			if filter == nil {
				t.Fatal("filter should not be nil")
			}

			var results []any
			for _, item := range corpus {
				adaptor := adapt(item)
				if filter.Match(adaptor) {
					results = append(results, item)
				}
			}

			if !reflect.DeepEqual(results, testcase.expected) {
				t.Fatalf("%q: %#v != %#v", testcase.input, results, testcase.expected)
			}
		})
	}
}

func TestOperatorStrings(t *testing.T) {
	for _, testcase := range []struct {
		op       operator
		expected string
	}{
		{operatorPresent, "?"},
		{operatorEqual, "=="},
		{operatorNotEqual, "!="},
		{operatorMatches, "~="},
		{10, "unknown"},
	} {
		if !reflect.DeepEqual(testcase.op.String(), testcase.expected) {
			t.Fatalf("return value unexpected: %v != %v", testcase.op.String(), testcase.expected)
		}
	}
}

func FuzzFiltersParse(f *testing.F) {
	f.Add("foo=bar")
	f.Fuzz(func(t *testing.T, expr string) {
		filter, err := Parse(expr)
		if filter != nil && err != nil {
			t.Fatal("either filter or err must be non-nil")
		}
	})
}
