// Copyright 2020 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"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"maps"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/open-policy-agent/opa/cmd/formats"
	"github.com/open-policy-agent/opa/internal/presentation"
	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/bundle"
	"github.com/open-policy-agent/opa/v1/rego"
	"github.com/open-policy-agent/opa/v1/util"
	"github.com/open-policy-agent/opa/v1/util/test"
)

// Minimize the number of tests that *actually* run the benchmarks, they are pretty slow.
// Have one test that exercises the whole flow.
func TestRunBenchmark(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()

	args := []string{"1 + 1"}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}

	// Expect a json serialized benchmark result with histogram fields
	var br testing.BenchmarkResult
	err = util.UnmarshalJSON(buf.Bytes(), &br)
	if err != nil {
		t.Fatalf("Unexpected error unmarshalling output: %s", err)
	}

	if br.N == 0 || br.T == 0 || br.MemAllocs == 0 || br.MemBytes == 0 {
		t.Fatalf("Expected benchmark results to be non-zero, got: %+v", br)
	}

	if _, ok := br.Extra["histogram_timer_rego_query_eval_ns_count"]; !ok {
		t.Fatalf("Expected benchmark results to contain histogram_timer_rego_query_eval_ns_count, got: %+v", br)
	}

	if float64(br.N) != br.Extra["histogram_timer_rego_query_eval_ns_count"] {
		t.Fatalf("Expected 'histogram_timer_rego_query_eval_ns_count' to be equal to N")
	}
}

func TestRunBenchmarkWithQueryImport(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	// We add the rego.v1 import ..
	params.imports = newrepeatedStringFlag([]string{"rego.v1"})

	// .. which provides the 'in' keyword
	args := []string{`"a" in ["a", "b", "c"]`}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}

	// Expect a json serialized benchmark result with histogram fields
	var br testing.BenchmarkResult
	err = util.UnmarshalJSON(buf.Bytes(), &br)
	if err != nil {
		t.Fatalf("Unexpected error unmarshalling output: %s", err)
	}

	if br.N == 0 || br.T == 0 || br.MemAllocs == 0 || br.MemBytes == 0 {
		t.Fatalf("Expected benchmark results to be non-zero, got: %+v", br)
	}

	if _, ok := br.Extra["histogram_timer_rego_query_eval_ns_count"]; !ok {
		t.Fatalf("Expected benchmark results to contain histogram_timer_rego_query_eval_ns_count, got: %+v", br)
	}

	if float64(br.N) != br.Extra["histogram_timer_rego_query_eval_ns_count"] {
		t.Fatalf("Expected 'histogram_timer_rego_query_eval_ns_count' to be equal to N")
	}
}

func TestRunBenchmarkE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.e2e = true

	args := []string{"1 + 1"}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}

	// Expect a json serialized benchmark result with histogram fields
	var br testing.BenchmarkResult
	err = util.UnmarshalJSON(buf.Bytes(), &br)
	if err != nil {
		t.Fatalf("Unexpected error unmarshalling output: %s", err)
	}

	if br.N == 0 || br.T == 0 || br.MemAllocs == 0 || br.MemBytes == 0 {
		t.Fatalf("Expected benchmark results to be non-zero, got: %+v", br)
	}

	if _, ok := br.Extra["histogram_timer_rego_query_eval_ns_count"]; !ok {
		t.Fatalf("Expected benchmark results to contain 'histogram_timer_rego_query_eval_ns_count', got: %+v", br)
	}

	if float64(br.N) != br.Extra["histogram_timer_rego_query_eval_ns_count"] {
		t.Fatalf("Expected 'histogram_timer_rego_query_eval_ns_count' to be equal to N")
	}

	if _, ok := br.Extra["histogram_timer_server_handler_ns_count"]; !ok {
		t.Fatalf("Expected benchmark results to contain 'histogram_timer_server_handler_ns_count', got: %+v", br)
	}

	if float64(br.N) != br.Extra["histogram_timer_server_handler_ns_count"] {
		t.Fatalf("Expected 'histogram_timer_server_handler_ns_count' to be equal to N")
	}
}

func TestRunBenchmarkE2EWithOPAConfigFile(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	fs := map[string]string{
		"/config.yaml": `{"decision_logs": {"console": true}}`,
	}

	test.WithTempFS(fs, func(testDirRoot string) {

		params := testBenchParams()
		params.e2e = true
		params.configFile = filepath.Join(testDirRoot, "config.yaml")

		args := []string{"1 + 1"}
		var buf bytes.Buffer

		rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		if rc != 0 {
			t.Fatalf("Unexpected return code %d, expected 0", rc)
		}

		// Expect a json serialized benchmark result with histogram fields
		var br testing.BenchmarkResult
		err = util.UnmarshalJSON(buf.Bytes(), &br)
		if err != nil {
			t.Fatalf("Unexpected error unmarshalling output: %s", err)
		}

		if br.N == 0 || br.T == 0 || br.MemAllocs == 0 || br.MemBytes == 0 {
			t.Fatalf("Expected benchmark results to be non-zero, got: %+v", br)
		}

		if _, ok := br.Extra["histogram_timer_rego_query_eval_ns_count"]; !ok {
			t.Fatalf("Expected benchmark results to contain 'histogram_timer_rego_query_eval_ns_count', got: %+v", br)
		}

		if float64(br.N) != br.Extra["histogram_timer_rego_query_eval_ns_count"] {
			t.Fatalf("Expected 'histogram_timer_rego_query_eval_ns_count' to be equal to N")
		}

		if _, ok := br.Extra["histogram_timer_server_handler_ns_count"]; !ok {
			t.Fatalf("Expected benchmark results to contain 'histogram_timer_server_handler_ns_count', got: %+v", br)
		}

		if float64(br.N) != br.Extra["histogram_timer_server_handler_ns_count"] {
			t.Fatalf("Expected 'histogram_timer_server_handler_ns_count' to be equal to N")
		}
	})
}

func TestRunBenchmarkFailFastE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.fail = true // configured to fail on undefined results
	params.e2e = true

	args := []string{"a := 1; a > 2"}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 1 {
		t.Fatalf("Unexpected return code %d, expected 1", rc)
	}

	// Expect a json serialized benchmark result with histogram fields
	var pr presentation.Output
	err = util.UnmarshalJSON(buf.Bytes(), &pr)
	if err != nil {
		t.Fatalf("Unexpected error unmarshalling output: %s", err)
	}

	if len(pr.Errors) != 1 {
		t.Fatalf("Expected 1 error in result, got:\n\n%s\n", buf.String())
	}
}

func TestBenchPartialE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.partial = true
	params.fail = true
	params.e2e = true
	params.unknowns = []string{"input"}
	args := []string{"input.x > 0"}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}

	var br testing.BenchmarkResult
	err = util.UnmarshalJSON(buf.Bytes(), &br)
	if err != nil {
		t.Fatalf("Unexpected error unmarshalling output: %s", err)
	}

	if br.N == 0 || br.T == 0 || br.MemAllocs == 0 || br.MemBytes == 0 {
		t.Fatalf("Expected benchmark results to be non-zero, got: %+v", br)
	}

	if _, ok := br.Extra["histogram_timer_rego_partial_eval_ns_count"]; !ok {
		t.Fatalf("Expected benchmark results to contain 'histogram_timer_rego_partial_eval_ns_count', got: %+v", br)
	}

	if float64(br.N) != br.Extra["histogram_timer_rego_partial_eval_ns_count"] {
		t.Fatalf("Expected 'histogram_timer_rego_partial_eval_ns_count' to be equal to N")
	}

	if _, ok := br.Extra["histogram_timer_server_handler_ns_count"]; !ok {
		t.Fatalf("Expected benchmark results to contain 'histogram_timer_server_handler_ns_count', got: %+v", br)
	}

	if float64(br.N) != br.Extra["histogram_timer_server_handler_ns_count"] {
		t.Fatalf("Expected 'histogram_timer_server_handler_ns_count' to be equal to N")
	}
}

func TestRunBenchmarkPartialFailFastE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.partial = true
	params.unknowns = []string{}
	params.fail = true
	params.e2e = true
	args := []string{"1 == 2"}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 1 {
		t.Fatalf("Unexpected return code %d, expected 1", rc)
	}

	actual := buf.String()
	expected := `{
  "errors": [
    {
      "message": "undefined result"
    }
  ]
}
`

	if actual != expected {
		t.Fatalf("\nExpected:\n%s\n\nGot:\n%s\n", expected, actual)
	}
}

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

	params := testBenchParams()
	params.fail = true // configured to fail on undefined results

	args := []string{"a := 1; a > 2"}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 1 {
		t.Fatalf("Unexpected return code %d, expected 1", rc)
	}

	// Expect a json serialized benchmark result with histogram fields
	var pr presentation.Output
	err = util.UnmarshalJSON(buf.Bytes(), &pr)
	if err != nil {
		t.Fatalf("Unexpected error unmarshalling output: %s", err)
	}

	if len(pr.Errors) != 1 {
		t.Fatalf("Expected 1 error in result, got:\n\n%s\n", buf.String())
	}
}

// mockBenchRunner lets us test the bench CLI operations without having to wait ~10 seconds
// while the actual benchmark runner does its thing.
type mockBenchRunner struct {
	onRun func(ctx context.Context, ectx *evalContext, params benchmarkCommandParams, f func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error)
}

func (r *mockBenchRunner) run(ctx context.Context, ectx *evalContext, params benchmarkCommandParams, f func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error) {
	if r.onRun != nil {
		return r.onRun(ctx, ectx, params, f)
	}
	return testing.BenchmarkResult{}, nil
}

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

	params := testBenchParams()
	params.partial = true
	params.fail = true
	args := []string{"input=1"}
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &mockBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}
}

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

	params := testBenchParams()
	args := []string{"???"} // query compile error
	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &mockBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 1 {
		t.Fatalf("Unexpected return code %d, expected 1", rc)
	}
}

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

	params := testBenchParams()
	args := []string{"1+1"}
	var buf bytes.Buffer

	mockRunner := &mockBenchRunner{}
	mockRunner.onRun = func(_ context.Context, _ *evalContext, _ benchmarkCommandParams, _ func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error) {
		return testing.BenchmarkResult{}, errors.New("error error error")
	}

	rc, err := benchMain(args, params, &buf, nil, mockRunner)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 1 {
		t.Fatalf("Unexpected return code %d, expected 1", rc)
	}
}

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

	params := testBenchParams()
	args := []string{"1+1"}
	var buf bytes.Buffer

	mockRunner := &mockBenchRunner{}

	params.count = 25
	actualCount := 0
	mockRunner.onRun = func(_ context.Context, _ *evalContext, _ benchmarkCommandParams, _ func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error) {
		actualCount++
		return testing.BenchmarkResult{}, nil
	}

	rc, err := benchMain(args, params, &buf, nil, mockRunner)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}

	if actualCount != params.count {
		t.Fatalf("Expected benchmark to be run %d times, only ran %d", params.count, actualCount)
	}
}

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

	params := testBenchParams()
	args := []string{"1+1"}
	var buf bytes.Buffer

	mockRunner := &mockBenchRunner{}

	params.count = -1
	actualCount := 0
	mockRunner.onRun = func(_ context.Context, _ *evalContext, _ benchmarkCommandParams, _ func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error) {
		actualCount++
		return testing.BenchmarkResult{}, nil
	}

	rc, err := benchMain(args, params, &buf, nil, mockRunner)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}

	if actualCount != 0 {
		t.Fatalf("Expected benchmark to not be run, instead ran %d times", actualCount)
	}
}

func validateBenchMainPrep(t *testing.T, args []string, params benchmarkCommandParams) {
	t.Helper()

	var buf bytes.Buffer

	mockRunner := &mockBenchRunner{}

	mockRunner.onRun = func(ctx context.Context, ectx *evalContext, _ benchmarkCommandParams, _ func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error) {

		// cheat and use the ectx to evalute the query to ensure the input setup on it was valid
		r := rego.New(ectx.regoArgs...)
		pq, err := r.PrepareForEval(ctx)
		if err != nil {
			return testing.BenchmarkResult{}, err
		}

		rs, err := pq.Eval(ctx, ectx.evalArgs...)
		if err != nil {
			return testing.BenchmarkResult{}, err
		}

		if len(rs) == 0 {
			return testing.BenchmarkResult{}, errors.New("expected result, got none")
		}

		return testing.BenchmarkResult{}, nil
	}

	rc, err := benchMain(args, params, &buf, nil, mockRunner)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}
	if rc != 0 {
		t.Fatalf("Unexpected return code %d, expected 0", rc)
	}
}

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

	params := testBenchParams()
	files := map[string]string{
		"/input.json": `{"x": 42}`,
	}
	args := []string{"input.x == 42"}
	test.WithTempFS(files, func(path string) {
		params.inputPath = filepath.Join(path, "input.json")

		validateBenchMainPrep(t, args, params)
	})
}

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

	params := testBenchParams()
	files := map[string]string{
		"/input.yaml": `x: 42`,
	}
	args := []string{"input.x == 42"}
	test.WithTempFS(files, func(path string) {
		params.inputPath = filepath.Join(path, "input.yaml")

		validateBenchMainPrep(t, args, params)
	})
}

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

	params := testBenchParams()
	files := map[string]string{
		"/input.yaml": `x: 42`,
	}
	args := []string{"1+1"}
	test.WithTempFS(files, func(path string) {
		params.inputPath = filepath.Join(path, "definitely", "not", "input.yaml")

		var buf bytes.Buffer

		rc, err := benchMain(args, params, &buf, nil, &mockBenchRunner{})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}
		if rc != 1 {
			t.Fatalf("Unexpected return code %d, expected 1", rc)
		}
	})
}

func TestBenchMainWithJSONInputFileE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.e2e = true
	files := map[string]string{
		"/input.json": `{"x": 42}`,
	}
	args := []string{"input.x == 42"}
	test.WithTempFS(files, func(path string) {
		params.inputPath = filepath.Join(path, "input.json")

		var buf bytes.Buffer

		rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		if rc != 0 {
			t.Fatalf("Unexpected return code %d, expected 0", rc)
		}
	})
}

func TestBenchMainWithYAMLInputFileE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.e2e = true
	files := map[string]string{
		"/input.yaml": `x: 42`,
	}
	args := []string{"input.x == 42"}
	test.WithTempFS(files, func(path string) {
		params.inputPath = filepath.Join(path, "input.yaml")

		var buf bytes.Buffer

		rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		if rc != 0 {
			t.Fatalf("Unexpected return code %d, expected 0", rc)
		}
	})
}

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

	params := testBenchParams()
	params.e2e = true
	files := map[string]string{
		"/input.yaml": `x: 42`,
	}
	args := []string{"1+1"}
	test.WithTempFS(files, func(path string) {
		params.inputPath = filepath.Join(path, "definitely", "not", "input.yaml")

		var buf bytes.Buffer

		rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		if rc != 1 {
			t.Fatalf("Unexpected return code %d, expected 1", rc)
		}
	})
}

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

	params := testBenchParams()

	b := testBundle()

	files := map[string]string{
		"bundle.tar.gz": "",
	}

	test.WithTempFS(files, func(path string) {
		bundlePath := filepath.Join(path, "bundle.tar.gz")
		f, err := os.OpenFile(bundlePath, os.O_WRONLY, os.ModePerm)
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		err = bundle.Write(f, b)
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		err = params.bundlePaths.Set(bundlePath)
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		args := []string{"data.a.b.x"}

		validateBenchMainPrep(t, args, params)
	})
}

func TestBenchMainWithBundleDataE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.e2e = true

	b := testBundle()

	files := map[string]string{
		"bundle.tar.gz": "",
	}

	test.WithTempFS(files, func(path string) {
		bundlePath := filepath.Join(path, "bundle.tar.gz")
		f, err := os.OpenFile(bundlePath, os.O_WRONLY, os.ModePerm)
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		err = bundle.Write(f, b)
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		err = params.bundlePaths.Set(bundlePath)
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		args := []string{"data.a.b.x"}

		var buf bytes.Buffer

		rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		if rc != 0 {
			t.Fatalf("Unexpected return code %d, expected 0", rc)
		}
	})
}

func TestBenchMainWithDataE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.e2e = true

	mod := `package a.b
	import rego.v1

	x if {
	   data.a.b.c == 42
	}
	`

	files := map[string]string{
		"p.rego": mod,
	}

	test.WithTempFS(files, func(path string) {
		err := params.dataPaths.Set(filepath.Join(path, "p.rego"))
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		args := []string{"data.a.b.x"}

		var buf bytes.Buffer

		rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		if rc != 0 {
			t.Fatalf("Unexpected return code %d, expected 0", rc)
		}
	})
}

func TestBenchMainBadQueryE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	params := testBenchParams()
	params.e2e = true
	args := []string{"foo.bar"}

	var buf bytes.Buffer

	rc, err := benchMain(args, params, &buf, nil, &goBenchRunner{})
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	if rc != 1 {
		t.Fatalf("Unexpected return code %d, expected 1", rc)
	}
}

func TestBenchMain_DefaultRegoVersion(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	tests := []struct {
		note    string
		module  string
		query   string
		expErrs []string
	}{
		// These tests are slow, so we're not being completely exhaustive here.
		{
			note: "v0 module",
			module: `package test
a[x] {
	x := 42
}`,
			query: `data.test.a`,
			expErrs: []string{
				"mod.rego:2: rego_parse_error: `if` keyword is required before rule body",
				"mod.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},
		{
			note: "v1 module",
			module: `package test
a contains x if {
	x := 42
}`,
			query: `data.test.a`,
		},
	}

	modes := []struct {
		name string
		e2e  bool
	}{
		{
			name: "run",
		},
		{
			name: "e2e",
			e2e:  true,
		},
	}

	for _, mode := range modes {
		for _, tc := range tests {
			t.Run(fmt.Sprintf("%s, %s", tc.note, mode.name), func(t *testing.T) {
				t.Parallel()

				files := map[string]string{
					"mod.rego": tc.module,
				}

				test.WithTempFS(files, func(path string) {
					params := testBenchParams()
					_ = params.outputFormat.Set(formats.Pretty)
					params.e2e = mode.e2e

					for n := range files {
						err := params.dataPaths.Set(filepath.Join(path, n))
						if err != nil {
							t.Fatalf("Unexpected error: %s", err)
						}
					}

					args := []string{tc.query}

					var buf, errBuf bytes.Buffer
					rc, err := benchMain(args, params, &buf, &errBuf, &goBenchRunner{})

					if len(tc.expErrs) > 0 {
						if rc == 0 {
							t.Fatalf("Expected non-zero return code")
						}

						output := errBuf.String()
						for _, expErr := range tc.expErrs {
							if !strings.Contains(output, expErr) {
								t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, output)
							}
						}
					} else {
						if err != nil {
							t.Fatalf("Unexpected error: %s", err)
						}
						if rc != 0 {
							t.Fatalf("Unexpected return code %d, expected 0", rc)
						}
					}
				})
			})
		}
	}
}

func TestBenchMainCompatibleFlags(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	tests := []struct {
		note         string
		v0Compatible bool
		v1Compatible bool
		module       string
		query        string
		expErrs      []string
	}{
		// These tests are slow, so we're not being completely exhaustive here.
		{
			note:         "v0, keywords not used",
			v0Compatible: true,
			module: `package test
a[4] {
	1 == 1
}`,
			query: `data.test.a`,
		},
		{
			note:         "v0, no keywords imported",
			v0Compatible: true,
			module: `package test
a contains 4 if {
	1 == 1
}`,
			query: `data.test.a`,
			expErrs: []string{
				"rego_parse_error: var cannot be used for rule name",
				"rego_parse_error: number cannot be used for rule name",
			},
		},
		{
			note:         "v1, keywords not used",
			v1Compatible: true,
			module: `package test
a[4] {
	1 == 1
}`,
			query: `data.test.a`,
			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, no keywords imported",
			v1Compatible: true,
			module: `package test
a contains 4 if {
	1 == 1
}`,
			query: `data.test.a`,
		},
		{
			note:         "v0+v1, keywords not used (v0 takes precedence)",
			v0Compatible: true,
			v1Compatible: true,
			module: `package test
a[4] {
	1 == 1
}`,
			query: `data.test.a`,
		},
	}

	modes := []struct {
		name string
		e2e  bool
	}{
		{
			name: "run",
		},
		{
			name: "e2e",
			e2e:  true,
		},
	}

	for _, mode := range modes {
		for _, tc := range tests {
			t.Run(fmt.Sprintf("%s, %s", tc.note, mode.name), func(t *testing.T) {
				t.Parallel()

				files := map[string]string{
					"mod.rego": tc.module,
				}

				test.WithTempFS(files, func(path string) {
					params := testBenchParams()
					_ = params.outputFormat.Set(formats.Pretty)
					params.v0Compatible = tc.v0Compatible
					params.v1Compatible = tc.v1Compatible
					params.e2e = mode.e2e

					for n := range files {
						err := params.dataPaths.Set(filepath.Join(path, n))
						if err != nil {
							t.Fatalf("Unexpected error: %s", err)
						}
					}

					args := []string{tc.query}

					var buf, errBuf bytes.Buffer
					rc, err := benchMain(args, params, &buf, &errBuf, &goBenchRunner{})

					if len(tc.expErrs) > 0 {
						if rc == 0 {
							t.Fatalf("Expected non-zero return code")
						}

						output := errBuf.String()
						for _, expErr := range tc.expErrs {
							if !strings.Contains(output, expErr) {
								t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, output)
							}
						}
					} else {
						if err != nil {
							t.Fatalf("Unexpected error: %s", err)
						}
						if rc != 0 {
							t.Fatalf("Unexpected return code %d, expected 0", rc)
						}
					}
				})
			})
		}
	}
}

func TestBenchMainWithBundleRegoVersion(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	tests := []struct {
		note                   string
		bundleRegoVersion      int
		bundleFileRegoVersions map[string]int
		modules                map[string]string
		query                  string
		expErrs                []string
	}{
		// These tests are slow, so we're not being completely exhaustive here.
		{
			note:              "v0 bundle",
			bundleRegoVersion: 0,
			modules: map[string]string{
				"test.rego": `package test
a[4] {
	1 == 1
}`,
			},
			query: `data.test.a`,
		},
		{
			note:              "v0 bundle, no keywords imported",
			bundleRegoVersion: 0,
			modules: map[string]string{
				"test.rego": `package test
a contains 4 if {
	1 == 1
}`,
			},
			query: `data.test.a`,
			expErrs: []string{
				"rego_parse_error: var cannot be used for rule name",
				"rego_parse_error: number cannot be used for rule name",
			},
		},
		{
			note:              "v0 bundle, v1 per-file override",
			bundleRegoVersion: 0,
			bundleFileRegoVersions: map[string]int{
				"*/test2.rego": 1,
			},
			modules: map[string]string{
				"test1.rego": `package test
a[4] {
	1 == 1
}`,
				"test2.rego": `package test
b contains 4 if {
	1 == 1
}`,
			},
			query: `data.test.a`,
		},
		{
			note:              "v1 bundle, keywords not used",
			bundleRegoVersion: 1,
			modules: map[string]string{
				"test.rego": `package test
a[4] {
	1 == 1
}`,
			},
			query: `data.test.a`,
			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, no keywords imported",
			bundleRegoVersion: 1,
			modules: map[string]string{
				"test.rego": `package test
a contains 4 if {
	1 == 1
}`,
			},
			query: `data.test.a`,
		},
	}

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

	modes := []struct {
		name string
		e2e  bool
	}{
		{
			name: "run",
		},
		{
			name: "e2e",
			e2e:  true,
		},
	}

	for _, bundleType := range bundleTypeCases {
		for _, mode := range modes {
			for _, tc := range tests {
				t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, tc.note, mode.name), func(t *testing.T) {
					t.Parallel()

					files := map[string]string{}

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

						manifest := bundle.Manifest{
							RegoVersion:      &tc.bundleRegoVersion,
							FileRegoVersions: tc.bundleFileRegoVersions,
						}
						manifest.Init()
						if b, err := json.Marshal(manifest); err != nil {
							t.Fatalf("Unexpected error: %s", err)
						} else {
							files[".manifest"] = string(b)
						}
					}

					test.WithTempFS(files, func(root string) {
						p := root
						if bundleType.tar {
							b := bundle.Bundle{
								Manifest: bundle.Manifest{
									RegoVersion:      &tc.bundleRegoVersion,
									FileRegoVersions: tc.bundleFileRegoVersions,
								},
								Data: map[string]any{},
							}
							for k, v := range tc.modules {
								b.Modules = append(b.Modules, bundle.ModuleFile{
									Path: k,
									Raw:  []byte(v),
								})
							}
							p = filepath.Join(root, "bundle.tar.gz")
							f, err := os.OpenFile(p, os.O_WRONLY, os.ModePerm)
							if err != nil {
								t.Fatalf("Unexpected error: %s", err)
							}
							err = bundle.Write(f, b)
							if err != nil {
								t.Fatalf("Unexpected error: %s", err)
							}
						}

						params := testBenchParams()
						_ = params.outputFormat.Set(formats.Pretty)

						params.e2e = mode.e2e
						err := params.bundlePaths.Set(p)
						if err != nil {
							t.Fatalf("Unexpected error: %s", err)
						}

						args := []string{tc.query}

						var buf, errBuf bytes.Buffer
						rc, err := benchMain(args, params, &buf, &errBuf, &goBenchRunner{})

						if len(tc.expErrs) > 0 {
							if rc == 0 {
								t.Fatalf("Expected non-zero return code")
							}

							output := errBuf.String()
							for _, expErr := range tc.expErrs {
								if !strings.Contains(output, expErr) {
									t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, output)
								}
							}
						} else {
							if err != nil {
								t.Fatalf("Unexpected error: %s", err)
							}
							if rc != 0 {
								t.Fatalf("Unexpected return code %d, expected 0", rc)
							}
						}
					})
				})
			}
		}
	}
}

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

	params := testBenchParams()
	err := params.outputFormat.Set(formats.JSON)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	br := fakeBenchResults()

	var buf bytes.Buffer
	renderBenchmarkResult(params, br, &buf)

	actual := buf.String()

	expected := `{
  "N": 134844,
  "T": 1088294120,
  "Bytes": 0,
  "MemAllocs": 8360721,
  "MemBytes": 449906736,
  "Extra": {
    "histogram_timer_rego_query_eval_ns_75%": 4953.75,
    "histogram_timer_rego_query_eval_ns_90%": 6309.6,
    "histogram_timer_rego_query_eval_ns_95%": 7872.55,
    "histogram_timer_rego_query_eval_ns_99%": 14947.34000000001,
    "histogram_timer_rego_query_eval_ns_99.9%": 174377.08200000023,
    "histogram_timer_rego_query_eval_ns_99.99%": 176301,
    "histogram_timer_rego_query_eval_ns_count": 134844,
    "histogram_timer_rego_query_eval_ns_max": 176301,
    "histogram_timer_rego_query_eval_ns_mean": 5118.3706225680935,
    "histogram_timer_rego_query_eval_ns_median": 4312,
    "histogram_timer_rego_query_eval_ns_min": 3553,
    "histogram_timer_rego_query_eval_ns_stddev": 6587.830963916497
  }
}
`
	if actual != expected {
		t.Fatalf("\nExpected:\n%s\n\nGot:\n%s\n", expected, actual)
	}
}

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

	params := testBenchParams()
	params.benchMem = false
	err := params.outputFormat.Set(formats.Pretty)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	br := fakeBenchResults()

	var buf bytes.Buffer
	renderBenchmarkResult(params, br, &buf)

	actual := buf.String()

	expected := `┌───────────────────────────────────────────┬────────┐
│ samples                                   │ 134844 │
│ ns/op                                     │   8071 │
│ histogram_timer_rego_query_eval_ns_75%    │   4954 │
│ histogram_timer_rego_query_eval_ns_90%    │   6310 │
│ histogram_timer_rego_query_eval_ns_95%    │   7873 │
│ histogram_timer_rego_query_eval_ns_99%    │  14947 │
│ histogram_timer_rego_query_eval_ns_99.9%  │ 174377 │
│ histogram_timer_rego_query_eval_ns_99.99% │ 176301 │
│ histogram_timer_rego_query_eval_ns_count  │ 134844 │
│ histogram_timer_rego_query_eval_ns_max    │ 176301 │
│ histogram_timer_rego_query_eval_ns_mean   │   5118 │
│ histogram_timer_rego_query_eval_ns_median │   4312 │
│ histogram_timer_rego_query_eval_ns_min    │   3553 │
│ histogram_timer_rego_query_eval_ns_stddev │   6588 │
└───────────────────────────────────────────┴────────┘
`
	if actual != expected {
		t.Fatalf("\nExpected:\n%s\n\nGot:\n%s\n", expected, actual)
	}
}

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

	params := testBenchParams()
	params.benchMem = true
	err := params.outputFormat.Set(formats.Pretty)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	br := fakeBenchResults()

	var buf bytes.Buffer
	renderBenchmarkResult(params, br, &buf)

	actual := buf.String()

	expected := `┌───────────────────────────────────────────┬────────┐
│ samples                                   │ 134844 │
│ ns/op                                     │   8071 │
│ B/op                                      │   3336 │
│ allocs/op                                 │     62 │
│ histogram_timer_rego_query_eval_ns_75%    │   4954 │
│ histogram_timer_rego_query_eval_ns_90%    │   6310 │
│ histogram_timer_rego_query_eval_ns_95%    │   7873 │
│ histogram_timer_rego_query_eval_ns_99%    │  14947 │
│ histogram_timer_rego_query_eval_ns_99.9%  │ 174377 │
│ histogram_timer_rego_query_eval_ns_99.99% │ 176301 │
│ histogram_timer_rego_query_eval_ns_count  │ 134844 │
│ histogram_timer_rego_query_eval_ns_max    │ 176301 │
│ histogram_timer_rego_query_eval_ns_mean   │   5118 │
│ histogram_timer_rego_query_eval_ns_median │   4312 │
│ histogram_timer_rego_query_eval_ns_min    │   3553 │
│ histogram_timer_rego_query_eval_ns_stddev │   6588 │
└───────────────────────────────────────────┴────────┘
`
	if actual != expected {
		t.Fatalf("\nExpected:\n%s\n\nGot:\n%s\n", expected, actual)
	}
}

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

	params := testBenchParams()
	params.benchMem = true
	err := params.outputFormat.Set(formats.GoBench)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	br := fakeBenchResults()

	var buf bytes.Buffer
	renderBenchmarkResult(params, br, &buf)

	actual := buf.String()

	if !strings.HasPrefix(actual, "Benchmark") {
		t.Fatalf("Expected line output to start with 'Benchmark', got: \n\n%s\n", actual)
	}

	if len(strings.Split(strings.TrimSpace(actual), "\n")) != 1 {
		t.Fatalf("Expected only a single line of output")
	}
}

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

	params := testBenchParams()
	err := params.outputFormat.Set(formats.JSON)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	var buf bytes.Buffer

	_, err = ast.ParseBody("???")

	err = renderBenchmarkError(params, err, &buf, nil)
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}

	actual := buf.String()
	expected := `{
  "errors": [
    {
      "message": "illegal token",
      "code": "rego_parse_error",
      "location": {
        "file": "",
        "row": 1,
        "col": 1
      },
      "details": {
        "line": "???",
        "idx": 0
      }
    }
  ]
}
`

	if actual != expected {
		t.Fatalf("\nExpected:\n%s\n\nGot:\n%s\n", expected, actual)
	}
}

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

	params := testBenchParams()
	err := params.outputFormat.Set(formats.Pretty)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	testPrettyBenchmarkOutput(t, params)
}

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

	params := testBenchParams()
	err := params.outputFormat.Set(formats.GoBench)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}

	testPrettyBenchmarkOutput(t, params)
}

func testPrettyBenchmarkOutput(t *testing.T, params benchmarkCommandParams) {
	var buf bytes.Buffer

	_, err := ast.ParseBody("???")

	err = renderBenchmarkError(params, err, &buf, &buf)
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}

	actual := buf.String()
	expected := `1 error occurred: 1:1: rego_parse_error: illegal token
	???
	^
`
	if actual != expected {
		t.Fatalf("\nExpected:\n%s\n\nGot:\n%s\n", expected, actual)
	}
}

func testBenchParams() benchmarkCommandParams {
	params := newBenchmarkEvalParams()
	params.benchMem = true
	params.metrics = true
	_ = params.outputFormat.Set(formats.JSON)
	params.count = 1
	return params
}

func fakeBenchResults() testing.BenchmarkResult {
	return testing.BenchmarkResult{
		N:         134844,
		T:         1088294120,
		Bytes:     0,
		MemAllocs: 8360721,
		MemBytes:  449906736,
		Extra: map[string]float64{
			"histogram_timer_rego_query_eval_ns_75%":    4953.75,
			"histogram_timer_rego_query_eval_ns_90%":    6309.6,
			"histogram_timer_rego_query_eval_ns_95%":    7872.55,
			"histogram_timer_rego_query_eval_ns_99%":    14947.34000000001,
			"histogram_timer_rego_query_eval_ns_99.9%":  174377.08200000023,
			"histogram_timer_rego_query_eval_ns_99.99%": 176301,
			"histogram_timer_rego_query_eval_ns_count":  134844,
			"histogram_timer_rego_query_eval_ns_max":    176301,
			"histogram_timer_rego_query_eval_ns_mean":   5118.3706225680935,
			"histogram_timer_rego_query_eval_ns_median": 4312,
			"histogram_timer_rego_query_eval_ns_min":    3553,
			"histogram_timer_rego_query_eval_ns_stddev": 6587.830963916497,
		},
	}
}

func testBundle() bundle.Bundle {
	mod := `package a.b
	import rego.v1

	x if {
	   data.a.b.c == 42
	}
	`

	return bundle.Bundle{
		Manifest: bundle.Manifest{},
		Data: map[string]any{
			"a": map[string]any{
				"b": map[string]any{
					"c": 42,
				},
			},
		},
		Modules: []bundle.ModuleFile{
			{
				Path:   "/a/b/policy.rego",
				Raw:    []byte(mod),
				Parsed: ast.MustParseModule(mod),
			},
		},
	}
}
