// Copyright 2025 Google LLC
//
// 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
//
//    https://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 celtest provides functions for testing CEL policies and expressions.
package celtest

import (
	"testing"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common/decls"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/common/types/ref"
	"github.com/google/cel-go/policy"
	"github.com/google/cel-go/test"
	"github.com/google/cel-go/tools/compiler"

	"go.yaml.in/yaml/v3"

	conformancepb "cel.dev/expr/conformance/test"
)

type testCase struct {
	name                  string
	celExpression         string
	testSuitePath         string
	fileDescriptorSetPath string
	configPath            string
	opts                  []any
}

func setupTests() []*testCase {
	testCases := []*testCase{
		{
			name:          "policy test with custom policy parser",
			celExpression: "../../policy/testdata/k8s/policy.yaml",
			testSuitePath: "../../policy/testdata/k8s/tests.yaml",
			configPath:    "../../policy/testdata/k8s/config.yaml",
			opts:          []any{k8sParserOpts()},
		},
		{
			name:          "policy test with function binding",
			celExpression: "../../policy/testdata/restricted_destinations/policy.yaml",
			testSuitePath: "../../policy/testdata/restricted_destinations/tests.yaml",
			configPath:    "../../policy/testdata/restricted_destinations/config.yaml",
			opts:          []any{locationCodeEnvOption()},
		},
		{
			name:          "policy test with custom policy metadata",
			celExpression: "testdata/custom_policy.celpolicy",
			testSuitePath: "testdata/custom_policy_tests.yaml",
			opts:          []any{customPolicyParserOption(), compiler.PolicyMetadataEnvOption(ParsePolicyVariables)},
		},
		{
			name:          "raw expression file test",
			celExpression: "testdata/raw_expr.cel",
			testSuitePath: "testdata/raw_expr_tests.yaml",
			configPath:    "testdata/config.yaml",
			opts:          []any{fnEnvOption()},
		},
		{
			name:          "raw expression test",
			celExpression: "a || i + fn(j) == 42",
			testSuitePath: "testdata/raw_expr_tests.yaml",
			configPath:    "testdata/config.yaml",
			opts:          []any{fnEnvOption()},
		},
	}
	return testCases
}

func locationCodeEnvOption() cel.EnvOption {
	return cel.Function("locationCode",
		cel.Overload("locationCode_string", []*cel.Type{cel.StringType}, cel.StringType,
			cel.UnaryBinding(locationCode)))
}

func locationCode(ip ref.Val) ref.Val {
	switch ip.(types.String) {
	case "10.0.0.1":
		return types.String("us")
	case "10.0.0.2":
		return types.String("de")
	default:
		return types.String("ir")
	}
}

func k8sParserOpts() policy.ParserOption {
	return func(p *policy.Parser) (*policy.Parser, error) {
		p.TagVisitor = policy.K8sTestTagHandler()
		return p, nil
	}
}

// TestTriggerTestsWithRunnerOptions tests the TriggerTestsFromCompiler function for a custom policy
// by providing test runner and compiler options without setting the flag variables.
func TestTriggerTestsWithRunnerOptions(t *testing.T) {
	t.Run("test trigger tests custom policy", func(t *testing.T) {
		envOpt := compiler.EnvironmentFile("../../policy/testdata/k8s/config.yaml")
		testSuite := TestSuite("../../policy/testdata/k8s/tests.yaml")
		testCELPolicy := TestExpression("../../policy/testdata/k8s/policy.yaml")
		c, err := compiler.NewCompiler(envOpt, k8sParserOpts())
		if err != nil {
			t.Fatalf("compiler.NewCompiler() failed: %v", err)
		}
		compilerOpt := CustomTestCompiler(c)
		opts := []TestRunnerOption{compilerOpt, testSuite, testCELPolicy}
		TriggerTests(t, opts...)
	})
}

func customPolicyParserOption() policy.ParserOption {
	return func(p *policy.Parser) (*policy.Parser, error) {
		p.TagVisitor = customTagHandler{TagVisitor: policy.DefaultTagVisitor()}
		return p, nil
	}
}

func ParsePolicyVariables(metadata map[string]any) cel.EnvOption {
	var variables []*decls.VariableDecl
	for n, t := range metadata {
		variables = append(variables, decls.NewVariable(n, parseCustomPolicyVariableType(t.(string))))
	}
	return cel.VariableDecls(variables...)
}

func parseCustomPolicyVariableType(t string) *types.Type {
	switch t {
	case "int":
		return types.IntType
	case "string":
		return types.StringType
	default:
		return types.UnknownType
	}
}

type variableType struct {
	VariableName string `yaml:"variable_name"`
	VariableType string `yaml:"variable_type"`
}

type customTagHandler struct {
	policy.TagVisitor
}

func (customTagHandler) PolicyTag(ctx policy.ParserContext, id int64, tagName string, node *yaml.Node, p *policy.Policy) {
	switch tagName {
	case "variable_types":
		var varList []*variableType
		if err := node.Decode(&varList); err != nil {
			ctx.ReportErrorAtID(id, "invalid yaml variable_types node: %v, error: %w", node, err)
			return
		}
		for _, v := range varList {
			p.SetMetadata(v.VariableName, v.VariableType)
		}
	default:
		ctx.ReportErrorAtID(id, "unsupported policy tag: %s", tagName)
	}
}

func fnEnvOption() cel.EnvOption {
	return cel.Function("fn",
		cel.Overload("fn_int", []*cel.Type{cel.IntType}, cel.IntType,
			cel.UnaryBinding(func(in ref.Val) ref.Val {
				i := in.(types.Int)
				return i / types.Int(2)
			})))
}

// TestTriggerTests tests different scenarios of the TriggerTestsFromCompiler function.
func TestTriggerTests(t *testing.T) {
	for _, tc := range setupTests() {
		t.Run(tc.name, func(t *testing.T) {
			var testOpts []TestRunnerOption
			compileOpts := make([]any, 0, len(tc.opts)+2)
			for _, opt := range tc.opts {
				compileOpts = append(compileOpts, opt)
			}
			if tc.fileDescriptorSetPath != "" {
				compileOpts = append(compileOpts, compiler.TypeDescriptorSetFile(tc.fileDescriptorSetPath))
			}
			if tc.configPath != "" {
				compileOpts = append(compileOpts, compiler.EnvironmentFile(tc.configPath))
			}
			testOpts = append(testOpts,
				TestCompiler(compileOpts...),
				FileDescriptorSet(tc.fileDescriptorSetPath),
				TestSuite(tc.testSuitePath),
				TestExpression(tc.celExpression),
				PartialEvalProgramOption(),
			)
			TriggerTests(t, testOpts...)
		})
	}
}

// TestCustomTestSuiteParser triggers the test runner where the tests are provided by a custom
// test suite parser configured using TestSuiteParserOption.
func TestCustomTestSuiteParser(t *testing.T) {
	t.Run("test custom test suite parser", func(t *testing.T) {
		celExpr := "a || i + fn(j) == 42"
		compilerOpts := []any{compiler.EnvironmentFile("testdata/config.yaml"), fnEnvOption()}
		testRunnerOpts := []TestRunnerOption{
			TestCompiler(compilerOpts...),
			TestExpression(celExpr),
			TestSuiteParserOption(&tsparser{}),
		}
		TriggerTests(t, testRunnerOpts...)
	})
}

type tsparser struct {
	TestSuiteParser
}

// ParseTextproto implements the ParseTextproto method of the TestSuiteParser interface.
func (p *tsparser) ParseTextproto(_ string) (*conformancepb.TestSuite, error) {
	return nil, nil
}

// ParseYAML implements the ParseYAML method of the TestSuiteParser interface.
func (p *tsparser) ParseYAML(_ string) (*test.Suite, error) {
	testCase := &test.Case{
		Name: "sample test case",
		Input: map[string]*test.InputValue{
			"i": {Value: 21},
			"j": {Value: 42},
			"a": {Value: false},
		},
		Output: &test.Output{Value: true},
	}
	testSection := &test.Section{
		Name:  "sample test section",
		Tests: []*test.Case{testCase},
	}
	suite := &test.Suite{
		Description: "sample test suite",
		Sections:    []*test.Section{testSection},
	}
	return suite, nil
}
