//go:build !go1.27

// Copyright 2022 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package cmd

import (
	"bytes"
	"encoding/json"
	"fmt"
	"maps"
	"os"
	"path"
	"path/filepath"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"

	"github.com/open-policy-agent/opa/internal/file/archive"
	pr "github.com/open-policy-agent/opa/internal/presentation"
	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/util/test"
)

func TestCheckRespectsCapabilities(t *testing.T) {
	tests := []struct {
		note       string
		caps       string
		policy     string
		err        string
		bundleMode bool // check with "-b" flag
	}{
		{
			note: "builtin defined in caps",
			caps: `{
			"builtins": [
				{
					"name": "is_foo",
					"decl": {
						"args": [
							{
								"type": "string"
							}
						],
						"result": {
							"type": "boolean"
						},
						"type": "function"
					}
				}
			]
		}`,
			policy: `package test
p { is_foo("bar") }`,
		},
		{
			note: "future kw NOT defined in caps",
			caps: func() string {
				c := ast.CapabilitiesForThisVersion()
				c.FutureKeywords = []string{"in"}
				c.Features = []string{}
				j, err := json.Marshal(c)
				if err != nil {
					panic(err)
				}
				return string(j)
			}(),
			policy: `package test
import future.keywords.if
import future.keywords.in
p if "opa" in input.tools`,
			err: "rego_parse_error: unexpected keyword, must be one of [in]",
		},
		{
			note: "future kw NOT defined in caps, rego-v1 feature",
			caps: func() string {
				c := ast.CapabilitiesForThisVersion()
				c.FutureKeywords = []string{"in"}
				c.Features = []string{ast.FeatureRegoV1}
				j, err := json.Marshal(c)
				if err != nil {
					panic(err)
				}
				return string(j)
			}(),
			policy: `package test
import future.keywords.if
import future.keywords.in
p if "opa" in input.tools`,
		},
		{
			note: "future kw are defined in caps",
			caps: func() string {
				c := ast.CapabilitiesForThisVersion()
				c.FutureKeywords = []string{"in", "if"}
				j, err := json.Marshal(c)
				if err != nil {
					panic(err)
				}
				return string(j)
			}(),
			policy: `package test
import future.keywords.if
import future.keywords.in
p if "opa" in input.tools`,
		},
		{
			note: "rego.v1 imported but NOT defined in capabilities",
			caps: func() string {
				c := ast.CapabilitiesForThisVersion()
				c.Features = []string{}
				j, err := json.Marshal(c)
				if err != nil {
					panic(err)
				}
				return string(j)
			}(),
			policy: `package test
import rego.v1`,
			err: "rego_parse_error: invalid import, `rego.v1` is not supported by current capabilities",
		},
		{
			note: "rego.v1 imported AND defined in capabilities",
			caps: func() string {
				c := ast.CapabilitiesForThisVersion()
				c.Features = []string{ast.FeatureRegoV1Import}
				j, err := json.Marshal(c)
				if err != nil {
					panic(err)
				}
				return string(j)
			}(),
			policy: `package test
import rego.v1`,
		},
		{
			note: "rego.v1 imported AND rego-v1 in capabilities",
			caps: func() string {
				c := ast.CapabilitiesForThisVersion()
				c.Features = []string{ast.FeatureRegoV1}
				j, err := json.Marshal(c)
				if err != nil {
					panic(err)
				}
				return string(j)
			}(),
			policy: `package test
import rego.v1`,
		},
	}

	// add same tests for bundle-mode == true:
	for i := range tests {
		tc := tests[i]
		tc.bundleMode = true
		tc.note += " (as bundle)"
		tests = append(tests, tc)
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			files := map[string]string{
				"capabilities.json": tc.caps,
				"test.rego":         tc.policy,
			}

			test.WithTempFS(files, func(root string) {
				caps := newCapabilitiesFlag()
				if err := caps.Set(path.Join(root, "capabilities.json")); err != nil {
					t.Fatal(err)
				}
				params := newCheckParams()
				params.capabilities = caps
				params.bundleMode = tc.bundleMode
				// Capabilities in test cases is pre v1
				params.v0Compatible = true

				err := checkModules(params, []string{root})
				switch {
				case err != nil && tc.err != "":
					if !strings.Contains(err.Error(), tc.err) {
						t.Fatalf("expected err %v, got %v", tc.err, err)
					}
					return // don't read back bundle below
				case err != nil && tc.err == "":
					t.Fatalf("unexpected error: %v", err)
				case err == nil && tc.err != "":
					t.Fatalf("expected error %v, got nil", tc.err)
				}
			})
		})
	}
}

func testCheckWithSchemasAnnotationButNoSchemaFlag(policy string) error {
	files := map[string]string{
		"test.rego": policy,
	}

	var err error
	test.WithTempFS(files, func(path string) {
		params := newCheckParams()

		err = checkModules(params, []string{path})
	})

	return err
}

func TestCheckIgnoresNonRegoFiles(t *testing.T) {
	files := map[string]string{
		"test.rego": `package test`,
		"test.json": `{"foo": "bar"}`,
		"test.yaml": `foo: bar`,
	}

	test.WithTempFS(files, func(root string) {
		params := newCheckParams()

		err := checkModules(params, []string{root})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
	})
}

func TestCheckIgnoreBundleMode(t *testing.T) {
	t.Parallel()

	files := map[string]string{
		"ignore.rego":  `invalid rego`,
		"include.rego": `package valid`,
	}

	test.WithTempFS(files, func(root string) {
		params := newCheckParams()

		params.ignore = []string{"ignore.rego"}
		params.bundleMode = true

		err := checkModules(params, []string{root})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
	})
}

func TestCheckBundleReportsPolicyVsDataConflict(t *testing.T) {
	t.Parallel()

	files := map[string]string{
		"policy.rego": "package p\nallow := false\n",
		"data.json":   `{"p":{"allow":false}}`,
	}

	test.WithTempFS(files, func(root string) {
		params := newCheckParams()
		// Bundle mode required as the check command *should* ignore data entirely otherwise
		params.bundleMode = true

		err := checkModules(params, []string{root})
		if err == nil {
			t.Fatal("expected error but received none")
		}

		exp := fmt.Sprintf(
			"1 error occurred: %s:2: rego_compile_error: conflicting rule for data path p/allow found",
			filepath.Join(root, "policy.rego"),
		)
		if err.Error() != exp {
			t.Fatalf("expected error %q, got %q", exp, err.Error())
		}
	})
}

func TestCheckFailsOnInvalidRego(t *testing.T) {
	files := map[string]string{
		"test.rego": `package test
{}`,
		"test.json": `{"foo": "bar"}`,
	}
	expectedError := "rego_parse_error: object cannot be used for rule name"

	test.WithTempFS(files, func(root string) {
		params := newCheckParams()

		err := checkModules(params, []string{root})
		if err == nil {
			t.Fatalf("expected error %v but received none", expectedError)
		}
		if !strings.Contains(err.Error(), expectedError) {
			t.Fatalf("expected error %v but received %v", expectedError, err)
		}
	})
}

func TestCheckStrictReportsAllViolations(t *testing.T) {
	files := map[string]string{
		"test.rego": `package test

import data.foo
import data.foo

a := any([foo])
b := all([true])

f(x) if {
	input.bar
}`,
	}

	expected := []string{
		"test.rego:4: rego_compile_error: import must not shadow import data.foo",
		"test.rego:6: rego_type_error: deprecated built-in function calls in expression: any",
		"test.rego:7: rego_type_error: deprecated built-in function calls in expression: all",
		"test.rego:9: rego_compile_error: unused argument x. (hint: use _ (wildcard variable) instead)",
	}

	test.WithTempFS(files, func(root string) {
		params := newCheckParams()
		params.strict = true

		err := checkModules(params, []string{root})
		if err == nil {
			t.Fatal("expected error but received none")
		}

		exp := "4 errors occurred:\n" + strings.Join(expected, "\n")
		if got := strings.ReplaceAll(err.Error(), root+string(filepath.Separator), ""); got != exp {
			t.Errorf("expected:\n\n%v\n\ngot:\n\n%v", exp, got)
		}
	})
}

func TestCheckJSONOutputBytes(t *testing.T) {
	files := map[string]string{
		"test.rego": `package test
{}`,
	}

	test.WithTempFS(files, func(root string) {
		params := newCheckParams()

		checkErr := checkModules(params, []string{root})
		if checkErr == nil {
			t.Fatal("expected error but received none")
		}

		var buf bytes.Buffer
		if err := pr.JSON(&buf, pr.Output{Errors: pr.NewOutputErrors(checkErr)}); err != nil {
			t.Fatalf("unexpected error: %v", err)
		}

		expected := strings.ReplaceAll(`{
  "errors": [
    {
      "message": "object cannot be used for rule name",
      "code": "rego_parse_error",
      "location": {
        "file": "TEMPDIR/test.rego",
        "row": 2,
        "col": 1
      }
    }
  ]
}
`, "TEMPDIR", root)

		if diff := cmp.Diff(expected, buf.String()); diff != "" {
			t.Errorf("unexpected result (-want, +got):\n%s", diff)
		}
	})
}

// Assert that 'schemas' annotations with schema refs are only informing the type checker when the --schema flag is used
func TestCheckWithSchemasAnnotationButNoSchemaFlag(t *testing.T) {
	policiesWithSchemaRef := []string{`
package test
import rego.v1
# METADATA
# schemas:
#   - input: schema["input"]
p if { 
	rego.metadata.rule() # presence of rego.metadata.* calls must not trigger unwanted schema evaluation 
	input.foo == 42 # type mismatch with schema that should be ignored
}`,
		`
package p

# METADATA
# schemas:
# - data.p.x: schema["nope"]
bug := data.p.x
`}

	for i, pol := range policiesWithSchemaRef {
		err := testCheckWithSchemasAnnotationButNoSchemaFlag(pol)
		if err != nil {
			t.Fatalf("unexpected error from eval policy %d with schema ref: %v", i, err)
		}
	}

	policyWithInlinedSchema := `
package test
import rego.v1
# METADATA
# schemas:
#   - input.foo: {"type": "boolean"}
p if { 
	rego.metadata.rule() # presence of rego.metadata.* calls must not trigger unwanted schema evaluation 
	input.foo == 42 # type mismatch with schema that should be ignored
}`

	err := testCheckWithSchemasAnnotationButNoSchemaFlag(policyWithInlinedSchema)
	// We expect an error here, as inlined schemas are always used for type checking
	if !strings.Contains(err.Error(), "rego_type_error: match error") {
		t.Fatalf("unexpected error from eval with inlined schema, got: %v", err)
	}
}

func TestCheckRegoV1(t *testing.T) {
	cases := []struct {
		note    string
		policy  string
		expErrs []string
	}{
		{
			note: "rego.v1 imported, v1 compliant",
			policy: `package test
import rego.v1
p contains x if {
	x := [1,2,3]
}`,
		},
		{
			note: "rego.v1 imported, NOT v1 compliant (parser)",
			policy: `package test
import rego.v1
p contains x {
	x := [1,2,3]
}

q.r`,
			expErrs: []string{
				"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "rego.v1 imported, NOT v1 compliant (compiler)",
			policy: `package test
import rego.v1

import data.foo
import data.bar as foo
`,
			expErrs: []string{
				"test.rego:5: rego_compile_error: import must not shadow import data.foo",
			},
		},
		{
			note: "keywords imported, v1 compliant",
			policy: `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
	x := [1,2,3]
}`,
		},
		{
			note: "keywords imported, NOT v1 compliant",
			policy: `package test
import future.keywords.contains
p contains x {
	x := [1,2,3]
}

q.r`,
			expErrs: []string{
				"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "keywords imported, NOT v1 compliant (compiler)",
			policy: `package test
import future.keywords.if

input := 1 if {
	1 == 2
}`,
			expErrs: []string{
				"test.rego:4: rego_compile_error: rules must not shadow input (use a different rule name)",
			},
		},
		{
			note: "no imports, v1 compliant",
			policy: `package test
p := 1
`,
		},
		{
			note: "no imports, NOT v1 compliant but v0 compliant (compiler)",
			policy: `package test
p.x`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "no imports, v1 compliant but NOT v0 compliant",
			policy: `package test
p contains x if {
	x := [1,2,3]
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: var cannot be used for rule name", // This error actually appears three times: once for 'p'; once for 'contains'; and once for 'x'. All are interpreted as [invalid] rule declarations with no value and body.
				"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			files := map[string]string{
				"test.rego": tc.policy,
			}

			test.WithTempFS(files, func(root string) {
				params := newCheckParams()
				params.regoV1 = true

				err := checkModules(params, []string{root})
				switch {
				case err != nil && len(tc.expErrs) > 0:
					for _, expErr := range tc.expErrs {
						if !strings.Contains(err.Error(), expErr) {
							t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
						}
					}
					return // don't read back bundle below
				case err != nil && len(tc.expErrs) == 0:
					t.Fatalf("unexpected error: %v", err)
				case err == nil && len(tc.expErrs) > 0:
					t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
				}
			})
		})
	}
}

func TestCheck_DefaultRegoVersion(t *testing.T) {
	cases := []struct {
		note    string
		policy  string
		expErrs []string
	}{
		{
			note: "v0 module",
			policy: `package test
a[x] {
	x := 42
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "v1 module",
			policy: `package test
a contains x if {
	x := 42
}`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			files := map[string]string{
				"test.rego": tc.policy,
			}

			test.WithTempFS(files, func(root string) {
				params := newCheckParams()

				err := checkModules(params, []string{root})
				switch {
				case err != nil && len(tc.expErrs) > 0:
					for _, expErr := range tc.expErrs {
						if !strings.Contains(err.Error(), expErr) {
							t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
						}
					}
					return // don't read back bundle below
				case err != nil && len(tc.expErrs) == 0:
					t.Fatalf("unexpected error: %v", err)
				case err == nil && len(tc.expErrs) > 0:
					t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
				}
			})
		})
	}
}

func TestCheckWithRegoV1Capability(t *testing.T) {
	cases := []struct {
		note         string
		v0Compatible bool
		capabilities *ast.Capabilities
		policy       string
		expErrs      []string
	}{
		{
			note:         "v0 module, v0-compatible, no capabilities",
			v0Compatible: true,
			policy: `package test
a[x] {
	x := 42
}`,
		},
		{
			note:         "v0 module, v0-compatible, v0 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			policy: `package test
a[x] {
	x := 42
}`,
		},
		{
			note:         "v0 module, v0-compatible, v1 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			policy: `package test
a[x] {
	x := 42
}`,
		},

		{
			note: "v0 module, not v0-compatible, no capabilities",
			policy: `package test
a[x] {
	x := 42
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note:         "v0 module, not v0-compatible, v0 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			policy: `package test
a[x] {
	x := 42
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note:         "v0 module, not v0-compatible, v0 capabilities without rego_v1 feature",
			capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
			policy: `package test
a[x] {
	x := 42
}`,
			expErrs: []string{
				"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
			},
		},
		{
			note:         "v0 module, not v0-compatible, v1 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			policy: `package test
a[x] {
	x := 42
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},

		{
			note:         "v1 module, v0-compatible, no capabilities",
			v0Compatible: true,
			policy: `package test
a contains x if {
	x := 42
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: var cannot be used for rule name",
			},
		},
		{
			note:         "v1 module, v0-compatible, v0 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			policy: `package test
a contains x if {
	x := 42
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: var cannot be used for rule name",
			},
		},
		{
			note:         "v1 module, v0-compatible, v1 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			policy: `package test
a contains x if {
	x := 42
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: var cannot be used for rule name",
			},
		},

		{
			note: "v1 module, not v0-compatible, no capabilities",
			policy: `package test
a contains x if {
	x := 42
}`,
		},
		{
			note:         "v1 module, not v0-compatible, v0 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			policy: `package test
a contains x if {
	x := 42
}`,
		},
		{
			note:         "v1 module, not v0-compatible, v0 capabilities without rego_v1 feature",
			capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
			policy: `package test
a contains x if {
	x := 42
}`,
			expErrs: []string{
				"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
			},
		},
		{
			note:         "v1 module, not v0-compatible, v1 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			policy: `package test
a contains x if {
	x := 42
}`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			files := map[string]string{
				"test.rego": tc.policy,
			}

			test.WithTempFS(files, func(root string) {
				params := newCheckParams()
				params.v0Compatible = tc.v0Compatible
				params.capabilities.C = tc.capabilities

				err := checkModules(params, []string{root})
				switch {
				case err != nil && len(tc.expErrs) > 0:
					for _, expErr := range tc.expErrs {
						if !strings.Contains(err.Error(), expErr) {
							t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
						}
					}
					return // don't read back bundle below
				case err != nil && len(tc.expErrs) == 0:
					t.Fatalf("unexpected error: %v", err)
				case err == nil && len(tc.expErrs) > 0:
					t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
				}
			})
		})
	}
}

func TestCheckCompatibleFlags(t *testing.T) {
	cases := []struct {
		note         string
		v0Compatible bool
		v1Compatible bool
		policy       string
		expErrs      []string
	}{
		{
			note:         "v0, no illegal keywords",
			v0Compatible: true,
			policy: `package test
p[x] {
	x := [1,2,3]
}`,
		},
		{
			note:         "v0, illegal keywords",
			v0Compatible: true,
			policy: `package test
p contains x if {
	x := [1,2,3]
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: var cannot be used for rule name",
			},
		},
		{
			note:         "v0, future.keywords imported",
			v0Compatible: true,
			policy: `package test
import future.keywords
p contains x if {
	x := [1,2,3]
}`,
		},
		{
			note:         "v0, rego.v1 imported",
			v0Compatible: true,
			policy: `package test
import rego.v1
p contains x if {
	x := [1,2,3]
}`,
		},
		{
			note:         "v1, rego.v1 imported, v1 compliant",
			v1Compatible: true,
			policy: `package test
import rego.v1
p contains x if {
	x := [1,2,3]
}`,
		},
		{
			note:         "v1, rego.v1 imported, NOT v1 compliant (parser)",
			v1Compatible: true,
			policy: `package test
import rego.v1
p contains x {
	x := [1,2,3]
}

q.r`,
			expErrs: []string{
				"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note:         "v1, rego.v1 imported, NOT v1 compliant (compiler)",
			v1Compatible: true,
			policy: `package test
import rego.v1

import data.foo
import data.bar as foo
`,
			expErrs: []string{
				"test.rego:5: rego_compile_error: import must not shadow import data.foo",
			},
		},
		{
			note:         "v1, keywords imported, v1 compliant",
			v1Compatible: true,
			policy: `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
	x := [1,2,3]
}`,
		},
		{
			note:         "v1, keywords imported, NOT v1 compliant",
			v1Compatible: true,
			policy: `package test
import future.keywords.contains
p contains x {
	x := [1,2,3]
}

q.r`,
			expErrs: []string{
				"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note:         "v1, keywords imported, NOT v1 compliant (compiler)",
			v1Compatible: true,
			policy: `package test
import future.keywords.if

input := 1 if {
	1 == 2
}`,
			expErrs: []string{
				"test.rego:4: rego_compile_error: rules must not shadow input (use a different rule name)",
			},
		},
		{
			note:         "v1, no imports, v1 compliant",
			v1Compatible: true,
			policy: `package test
p := 1
`,
		},
		{
			note:         "v1, no imports, NOT v1 compliant but v0 compliant (compiler)",
			v1Compatible: true,
			policy: `package test
p.x`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note:         "v1, no imports, v1 compliant but NOT v0 compliant",
			v1Compatible: true,
			policy: `package test
p contains x if {
	x := [1,2,3]
}`,
		},
		// v0 takes precedence over v1
		{
			note:         "v0+v1, no illegal keywords",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
p[x] {
	x := [1,2,3]
}`,
		},
		{
			note:         "v0+v1, illegal keywords",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
p contains x if {
	x := [1,2,3]
}`,
			expErrs: []string{
				"test.rego:2: rego_parse_error: var cannot be used for rule name",
			},
		},
		{
			note:         "v0+v1, future.keywords imported",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
import future.keywords
p contains x if {
	x := [1,2,3]
}`,
		},
		{
			note:         "v0+v1, rego.v1 imported",
			v0Compatible: true,
			v1Compatible: true,
			policy: `package test
import rego.v1
p contains x if {
	x := [1,2,3]
}`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			files := map[string]string{
				"test.rego": tc.policy,
			}

			test.WithTempFS(files, func(root string) {
				params := newCheckParams()
				params.v0Compatible = tc.v0Compatible
				params.v1Compatible = tc.v1Compatible

				err := checkModules(params, []string{root})
				switch {
				case err != nil && len(tc.expErrs) > 0:
					for _, expErr := range tc.expErrs {
						if !strings.Contains(err.Error(), expErr) {
							t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
						}
					}
					return // don't read back bundle below
				case err != nil && len(tc.expErrs) == 0:
					t.Fatalf("unexpected error: %v", err)
				case err == nil && len(tc.expErrs) > 0:
					t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
				}
			})
		})
	}
}

func TestCheckWithBundleRegoVersion(t *testing.T) {
	cases := []struct {
		note    string
		files   map[string]string
		expErrs []string
	}{
		{
			note: "v0.x bundle, illegal keywords",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test
p contains x if {
	x := [1,2,3]
}`,
			},
			expErrs: []string{
				"rego_parse_error: var cannot be used for rule name",
			},
		},
		{
			note: "v0.x bundle, rego.v1 imported, v1 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test
import rego.v1
p contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v0.x bundle, rego.v1 imported, NOT v1 compliant (parser)",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test
import rego.v1
p contains x {
	x := [1,2,3]
}

q.r`,
			},
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
				"rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "v0.x bundle, rego.v1 imported, NOT v1 compliant (compiler)",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test
import rego.v1

import data.foo
import data.bar as foo
`,
			},
			expErrs: []string{
				"rego_compile_error: import must not shadow import data.foo",
			},
		},
		{
			note: "v0.x bundle, keywords imported, v1 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v0.x bundle, no imports, v1 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test
p := 1
`,
			},
		},
		{
			note: "v0 bundle, v1 per-file overrides, compliant",
			files: map[string]string{
				".manifest": `{
	"rego_version": 0,
	"file_rego_versions": {
		"/policy2.rego": 1
	}
}`,
				"policy1.rego": `package test
p[x] {
	x := [1,2,3]
}`,
				"policy2.rego": `package test
q contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v0 bundle, v1 per-file overrides (glob), compliant",
			files: map[string]string{
				".manifest": `{
	"rego_version": 0,
	"file_rego_versions": {
		"*/policy2.rego": 1
	}
}`,
				"policy1.rego": `package test
p[x] {
	x := [1,2,3]
}`,
				"policy2.rego": `package test
q contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v0 bundle, v1 per-file overrides, incompliant",
			files: map[string]string{
				".manifest": `{
	"rego_version": 0,
	"file_rego_versions": {
		"/policy2.rego": 1
	}
}`,
				"policy1.rego": `package test
p[x] {
	x := [1,2,3]
}`,
				"policy2.rego": `package test
q[x] {
	x := [1,2,3]
}`,
			},
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
				"rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},

		{
			note: "v1.0 bundle, keywords used but not imported",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
p contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v1.0 bundle, rego.v1 imported, v1 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
import rego.v1
p contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v1.0 bundle, rego.v1 imported, NOT v1 compliant (parser)",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
import rego.v1
p contains x {
	x := [1,2,3]
}

q.r`,
			},
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
				"rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "v1.0 bundle, rego.v1 imported, NOT v1 compliant (compiler)",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
import rego.v1

import data.foo
import data.bar as foo
`,
			},
			expErrs: []string{
				"rego_compile_error: import must not shadow import data.foo",
			},
		},
		{
			note: "v1.0 bundle, keywords imported, v1 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v1.0 bundle, keywords imported, NOT v1 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
import future.keywords.contains
p contains x {
	x := [1,2,3]
}

q.r`,
			},
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
				"rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "v1.0 bundle, keywords imported, NOT v1 compliant (compiler)",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
import future.keywords.if

input := 1 if {
	1 == 2
}`,
			},
			expErrs: []string{
				"rego_compile_error: rules must not shadow input (use a different rule name)",
			},
		},
		{
			note: "v1.0 bundle, no imports, v1 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
p := 1
`,
			},
		},
		{
			note: "v1.0 bundle, no imports, NOT v1 compliant but v0 compliant (compiler)",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
p.x`,
			},
			expErrs: []string{
				"rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "v1.0 bundle, no imports, v1 compliant but NOT v0 compliant",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test
p contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v1 bundle, v0 per-file overrides, compliant",
			files: map[string]string{
				".manifest": `{
	"rego_version": 1,
	"file_rego_versions": {
		"/policy1.rego": 0
	}
}`,
				"policy1.rego": `package test
p[x] {
	x := [1,2,3]
}`,
				"policy2.rego": `package test
q contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v1 bundle, v0 per-file overrides (glob), compliant",
			files: map[string]string{
				".manifest": `{
	"rego_version": 1,
	"file_rego_versions": {
		"*/policy1.rego": 0
	}
}`,
				"policy1.rego": `package test
p[x] {
	x := [1,2,3]
}`,
				"policy2.rego": `package test
q contains x if {
	x := [1,2,3]
}`,
			},
		},
		{
			note: "v1 bundle, v0 per-file overrides, incompliant",
			files: map[string]string{
				".manifest": `{
	"rego_version": 1,
	"file_rego_versions": {
		"/policy1.rego": 0
	}
}`,
				"policy1.rego": `package test
p contains x if {
	x := [1,2,3]
}`,
				"policy2.rego": `package test
q contains x if {
	x := [1,2,3]
}`,
			},
			expErrs: []string{
				"rego_parse_error: var cannot be used for rule name",
			},
		},
	}

	bundleTypeCases := []struct {
		note string
		tar  bool
	}{
		{
			"bundle dir", false,
		},
		{
			"bundle tar", true,
		},
	}

	v1CompatibleFlagCases := []struct {
		note string
		used bool
	}{
		{
			"no --v1-compatible", false,
		},
		{
			"--v1-compatible", true,
		},
	}

	for _, bundleType := range bundleTypeCases {
		for _, v1CompatibleFlag := range v1CompatibleFlagCases {
			for _, tc := range cases {
				t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, v1CompatibleFlag.note, tc.note), func(t *testing.T) {
					files := map[string]string{}

					if bundleType.tar {
						files["bundle.tar.gz"] = ""
					} else {
						maps.Copy(files, tc.files)
					}

					test.WithTempFS(files, func(root string) {
						p := root
						if bundleType.tar {
							p = filepath.Join(root, "bundle.tar.gz")
							files := make([][2]string, 0, len(tc.files))
							for k, v := range tc.files {
								files = append(files, [2]string{k, v})
							}
							buf := archive.MustWriteTarGz(files)
							bf, err := os.Create(p)
							if err != nil {
								t.Fatalf("Unexpected error: %v", err)
							}
							_, err = bf.Write(buf.Bytes())
							if err != nil {
								t.Fatalf("Unexpected error: %v", err)
							}
						}

						params := newCheckParams()
						params.bundleMode = true
						params.v1Compatible = v1CompatibleFlag.used

						err := checkModules(params, []string{p})
						switch {
						case err != nil && len(tc.expErrs) > 0:
							for _, expErr := range tc.expErrs {
								if !strings.Contains(err.Error(), expErr) {
									t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
								}
							}
							return // don't read back bundle below
						case err != nil && len(tc.expErrs) == 0:
							t.Fatalf("unexpected error: %v", err)
						case err == nil && len(tc.expErrs) > 0:
							t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
						}
					})
				})
			}
		}
	}
}
