/*
Copyright 2021 The Kubernetes 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 cel

import (
	"fmt"
	"regexp"
	"testing"

	fuzz "github.com/google/gofuzz"
)

// TestEscaping tests that property names are escaped as expected.
func TestEscaping(t *testing.T) {
	cases := []struct {
		unescaped   string
		escaped     string
		unescapable bool
	}{
		// '.', '-', '/' and '__' are escaped since
		// CEL only allows identifiers of the form: [a-zA-Z_][a-zA-Z0-9_]*
		{unescaped: "a.a", escaped: "a__dot__a"},
		{unescaped: "a-a", escaped: "a__dash__a"},
		{unescaped: "a__a", escaped: "a__underscores__a"},
		{unescaped: "a.-/__a", escaped: "a__dot____dash____slash____underscores__a"},
		{unescaped: "a._a", escaped: "a__dot___a"},
		{unescaped: "a__.__a", escaped: "a__underscores____dot____underscores__a"},
		{unescaped: "a___a", escaped: "a__underscores___a"},
		{unescaped: "a____a", escaped: "a__underscores____underscores__a"},
		{unescaped: "a__dot__a", escaped: "a__underscores__dot__underscores__a"},
		{unescaped: "a__underscores__a", escaped: "a__underscores__underscores__underscores__a"},
		// CEL lexer RESERVED keywords must be escaped
		{unescaped: "true", escaped: "__true__"},
		{unescaped: "false", escaped: "__false__"},
		{unescaped: "null", escaped: "__null__"},
		{unescaped: "in", escaped: "__in__"},
		{unescaped: "as", escaped: "__as__"},
		{unescaped: "break", escaped: "__break__"},
		{unescaped: "const", escaped: "__const__"},
		{unescaped: "continue", escaped: "__continue__"},
		{unescaped: "else", escaped: "__else__"},
		{unescaped: "for", escaped: "__for__"},
		{unescaped: "function", escaped: "__function__"},
		{unescaped: "if", escaped: "__if__"},
		{unescaped: "import", escaped: "__import__"},
		{unescaped: "let", escaped: "__let__"},
		{unescaped: "loop", escaped: "__loop__"},
		{unescaped: "package", escaped: "__package__"},
		{unescaped: "namespace", escaped: "__namespace__"},
		{unescaped: "return", escaped: "__return__"},
		{unescaped: "var", escaped: "__var__"},
		{unescaped: "void", escaped: "__void__"},
		{unescaped: "while", escaped: "__while__"},
		// Not all property names are escapable
		{unescaped: "@", unescapable: true},
		{unescaped: "1up", unescapable: true},
		{unescaped: "👑", unescapable: true},
		// CEL macro and function names do not need to be escaped because the parser keeps identifiers in a
		// different namespace than function and  macro names.
		{unescaped: "has", escaped: "has"},
		{unescaped: "all", escaped: "all"},
		{unescaped: "exists", escaped: "exists"},
		{unescaped: "exists_one", escaped: "exists_one"},
		{unescaped: "filter", escaped: "filter"},
		{unescaped: "size", escaped: "size"},
		{unescaped: "contains", escaped: "contains"},
		{unescaped: "startsWith", escaped: "startsWith"},
		{unescaped: "endsWith", escaped: "endsWith"},
		{unescaped: "matches", escaped: "matches"},
		{unescaped: "duration", escaped: "duration"},
		{unescaped: "timestamp", escaped: "timestamp"},
		{unescaped: "getDate", escaped: "getDate"},
		{unescaped: "getDayOfMonth", escaped: "getDayOfMonth"},
		{unescaped: "getDayOfWeek", escaped: "getDayOfWeek"},
		{unescaped: "getFullYear", escaped: "getFullYear"},
		{unescaped: "getHours", escaped: "getHours"},
		{unescaped: "getMilliseconds", escaped: "getMilliseconds"},
		{unescaped: "getMinutes", escaped: "getMinutes"},
		{unescaped: "getMonth", escaped: "getMonth"},
		{unescaped: "getSeconds", escaped: "getSeconds"},
		// we don't escape a single _
		{unescaped: "_if", escaped: "_if"},
		{unescaped: "_has", escaped: "_has"},
		{unescaped: "_int", escaped: "_int"},
		{unescaped: "_anything", escaped: "_anything"},
	}

	for _, tc := range cases {
		t.Run(tc.unescaped, func(t *testing.T) {
			e, escapable := Escape(tc.unescaped)
			if tc.unescapable {
				if escapable {
					t.Errorf("Expected escapable=false, but got %t", escapable)
				}
				return
			}
			if !escapable {
				t.Fatalf("Expected escapable=true, but got %t", escapable)
			}
			if tc.escaped != e {
				t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e)
			}

			if !validCelIdent.MatchString(e) {
				t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", tc.unescaped, e)
			}

			u, ok := Unescape(tc.escaped)
			if !ok {
				t.Fatalf("Expected %s to be escapable, but it was not", tc.escaped)
			}
			if tc.unescaped != u {
				t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, u)
			}
		})
	}
}

func TestUnescapeMalformed(t *testing.T) {
	for _, s := range []string{"__int__extra", "__illegal__"} {
		t.Run(s, func(t *testing.T) {
			e, ok := Unescape(s)
			if ok {
				t.Fatalf("Expected %s to be unescapable, but it escaped to: %s", s, e)
			}
		})
	}
}

func TestEscapingFuzz(t *testing.T) {
	fuzzer := fuzz.New()
	for i := 0; i < 1000; i++ {
		var unescaped string
		fuzzer.Fuzz(&unescaped)
		t.Run(fmt.Sprintf("%d - '%s'", i, unescaped), func(t *testing.T) {
			if len(unescaped) == 0 {
				return
			}
			escaped, ok := Escape(unescaped)
			if !ok {
				return
			}

			if !validCelIdent.MatchString(escaped) {
				t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", unescaped, escaped)
			}
			u, ok := Unescape(escaped)
			if !ok {
				t.Fatalf("Expected %s to be unescapable, but it was not", escaped)
			}
			if unescaped != u {
				t.Errorf("Expected %s to unescape to %s, but got %s", escaped, unescaped, u)
			}
		})
	}
}

var validCelIdent = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)

func TestCanSkipRegex(t *testing.T) {
	cases := []struct {
		unescaped        string
		canSkip          bool
		invalidCharFound bool
	}{
		{unescaped: "a.a", canSkip: false},
		{unescaped: "a-a", canSkip: false},
		{unescaped: "a__a", canSkip: false},
		{unescaped: "a.-/__a", canSkip: false},
		{unescaped: "a_a", canSkip: true},
		{unescaped: "a_a_a", canSkip: true},
		{unescaped: "@", invalidCharFound: true},
		{unescaped: "👑", invalidCharFound: true},
		// if escape keyword is detected before invalid character, invalidCharFound
		{unescaped: "/👑", canSkip: false},
	}

	for _, tc := range cases {
		t.Run(tc.unescaped, func(t *testing.T) {
			escapeCheck := skipRegexCheck(tc.unescaped)
			if escapeCheck.invalidCharFound {
				if escapeCheck.invalidCharFound != tc.invalidCharFound {
					t.Errorf("Expected input validation to be %v, but got %t", tc.invalidCharFound, escapeCheck.invalidCharFound)
				}
			} else {
				if escapeCheck.canSkipRegex != tc.canSkip {
					t.Errorf("Expected %v, but got %t", tc.canSkip, escapeCheck.canSkipRegex)
				}
			}
		})
	}
}
