// Copyright 2017 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 tester_test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"maps"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/cover"
	"github.com/open-policy-agent/opa/v1/rego"
	"github.com/open-policy-agent/opa/v1/storage"
	"github.com/open-policy-agent/opa/v1/storage/inmem"
	"github.com/open-policy-agent/opa/v1/tester"
	"github.com/open-policy-agent/opa/v1/topdown"
	"github.com/open-policy-agent/opa/v1/types"
	"github.com/open-policy-agent/opa/v1/util/test"
)

func TestRun(t *testing.T) {
	testRun(t, testRunConfig{})
}

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

	testRun(t, testRunConfig{bench: true})
}

func TestRunWithCoverage(t *testing.T) {
	cov := cover.New()
	modules := testRun(t, testRunConfig{coverTracer: cov})
	report := cov.Report(modules)
	if len(report.Files) != len(modules) {
		t.Errorf("Expected %d files in coverage report, got %d", len(modules), len(report.Files))
	}
	if report.Coverage == 0 {
		t.Error("Expected test coverage")
	}
}

type expectedTestResult struct {
	wantErr  bool
	wantFail bool
	// nolint: structcheck // The test doesn't check this value, but should.
	wantSkip bool
	cases    map[string]expectedTestResult
}

type testRunConfig struct {
	bench       bool
	filter      string
	coverTracer topdown.QueryTracer
}

type expectedTestResults map[[2]string]expectedTestResult

func testRun(t *testing.T, conf testRunConfig) map[string]*ast.Module {
	files := map[string]string{
		"/a.rego": `package foo
			import rego.v1

			allow if { true }
			`,
		"/a_test.rego": `package foo
			import rego.v1

			test_pass if { allow }
			non_test if { true }
			test_fail if { not allow }
			test_fail_non_bool = 100
			test_err if { conflict }
			conflict = true
			conflict = false
			test_duplicate if { false }
			test_duplicate if { true }
			test_duplicate if { true }
			todo_test_skip if { true }
			`,
		"/b_test.rego": `package bar
			import rego.v1

			test_duplicate if { true }`,
		"/c_test.rego": `package baz
			import rego.v1

			a.b.test_duplicate if { false }
			a.b.test_duplicate if { true }
			a.b.test_duplicate if { true }`,
		// Regression test for issue #5496.
		"/d_test.rego": `package test
			import rego.v1

			a[0] := 1
			test_pass if { true }`,
		"/e_test.rego": `package qux
			import rego.v1

			test_cases_pass[x] if { some x in ["foo", "bar"] }
			test_cases_fail[x] if { some x in ["foo", "bar"]; false }
			test_cases_partial_fail[x] if { some x in ["foo", "bar", "baz"]; x != "bar" }
			test_cases_nested[x][y] if { some x in ["foo", "bar"]; some y in ["do", "re", "mi"]; not f(x, y) }
			f(x, y) if { x == "foo"; y == "re" }`,
	}

	tests := expectedTestResults{
		{"data.foo", "test_pass"}:                  {false, false, false, nil},
		{"data.foo", "test_fail"}:                  {false, true, false, nil},
		{"data.foo", "test_fail_non_bool"}:         {false, true, false, nil},
		{"data.foo", "test_duplicate"}:             {false, true, false, nil},
		{"data.foo", "test_duplicate#01"}:          {false, false, false, nil},
		{"data.foo", "test_duplicate#02"}:          {false, false, false, nil},
		{"data.foo", "test_err"}:                   {true, false, false, nil},
		{"data.foo", "todo_test_skip"}:             {false, false, true, nil},
		{"data.bar", "test_duplicate"}:             {false, false, false, nil},
		{"data.baz", "a.b.test_duplicate"}:         {false, true, false, nil},
		{"data.baz", "a.b[\"test_duplicate#01\"]"}: {false, false, false, nil},
		{"data.baz", "a.b[\"test_duplicate#02\"]"}: {false, false, false, nil},
		{"data.test", "test_pass"}:                 {false, false, false, nil},
		{"data.qux", "test_cases_pass"}: {false, false, false, map[string]expectedTestResult{
			"foo": {false, false, false, nil},
			"bar": {false, false, false, nil},
		}},
		{"data.qux", "test_cases_fail"}: {false, true, false, map[string]expectedTestResult{
			"foo": {false, true, false, nil},
			"bar": {false, true, false, nil},
		}},
		{"data.qux", "test_cases_partial_fail"}: {false, true, false, map[string]expectedTestResult{
			"foo": {false, false, false, nil},
			"bar": {false, true, false, nil},
			"baz": {false, false, false, nil},
		}},
		{"data.qux", "test_cases_nested"}: {false, true, false, map[string]expectedTestResult{
			"foo": {false, true, false, map[string]expectedTestResult{
				"do": {false, false, false, nil},
				"re": {false, true, false, nil},
				"mi": {false, false, false, nil},
			}},
			"bar": {false, false, false, map[string]expectedTestResult{
				"do": {false, false, false, nil},
				"re": {false, false, false, nil},
				"mi": {false, false, false, nil},
			}},
		}},
	}

	var modules map[string]*ast.Module
	test.WithTempFS(files, func(d string) {
		var rs []*tester.Result
		rs, modules = doTestRunWithTmpDir(t, d, conf)
		validateTestResults(t, tests, rs, conf)
	})
	return modules
}

func doTestRunWithTmpDir(t *testing.T, dir string, conf testRunConfig) ([]*tester.Result, map[string]*ast.Module) {
	t.Helper()

	ctx := t.Context()

	paths := []string{dir}
	modules, store, err := tester.Load(paths, nil)
	if err != nil {
		t.Fatal(err)
	}

	txn := storage.NewTransactionOrDie(ctx, store)
	defer store.Abort(ctx, txn)

	runner := tester.NewRunner().
		SetStore(store).
		SetModules(modules).
		Filter(conf.filter).
		SetTimeout(60 * time.Second).
		SetCoverageQueryTracer(conf.coverTracer)

	var ch chan *tester.Result
	if conf.bench {
		ch, err = runner.RunBenchmarks(ctx, txn, tester.BenchmarkOptions{})
	} else {
		ch, err = runner.RunTests(ctx, txn)
	}
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}
	var rs []*tester.Result
	for r := range ch {
		rs = append(rs, r)
	}

	return rs, modules
}

func validateTestResults(t *testing.T, tests expectedTestResults, rs []*tester.Result, conf testRunConfig) {
	t.Helper()
	seen := map[[2]string]struct{}{}
	for _, r := range rs {
		k := [2]string{r.Package, r.Name}
		seen[k] = struct{}{}
		exp, ok := tests[k]
		if !ok {
			t.Errorf("Unexpected result for %v", k)
			continue
		} else if exp.wantErr != (r.Error != nil) || exp.wantFail != r.Fail {
			t.Errorf("Expected %+v for %v but got: %v", exp, k, r)
		} else {
			// Test passed
			if conf.bench && r.BenchmarkResult == nil {
				t.Errorf("Expected BenchmarkResult for test %v, got nil", k)
			} else if !conf.bench && r.BenchmarkResult != nil {
				t.Errorf("Unexpected BenchmarkResult for test %v, expected nil", k)
			}
		}

		if exp.cases != nil {
			validateSubTestResults(t, exp.cases, r.SubResults)
		}
	}
	for k := range tests {
		if _, ok := seen[k]; !ok {
			t.Errorf("Expected result for %v", k)
		}
	}
}

func validateSubTestResults(t *testing.T, tests map[string]expectedTestResult, srs tester.SubResultMap) {
	t.Helper()
	seen := map[string]struct{}{}
	for k, exp := range tests {
		seen[k] = struct{}{}
		sr, ok := srs[k]
		if !ok {
			t.Errorf("Expected sub-result for %v", k)
			continue
		}

		if exp.wantFail != sr.Fail {
			t.Errorf("Expected %+v for %v but got: %v", exp, k, sr)
		}
	}
	for k, v := range srs {
		if _, ok := seen[k]; !ok {
			t.Errorf("Expected sub-result for %v", k)
		}

		if v.SubResults != nil {
			validateSubTestResults(t, tests[k].cases, v.SubResults)
		}
	}
}

func TestRunWithFilterRegex(t *testing.T) {
	files := map[string]string{
		"/a.rego": `package foo
			import rego.v1

			allow if { true }
			`,
		"/a_test.rego": `package foo
			import rego.v1

			test_pass if { allow }
			non_test if { true }
			test_fail if { not allow }
			test_fail_non_bool = 100
			test_err if { conflict }
			conflict = true
			conflict = false
			test_duplicate if { false }
			test_duplicate if { true }
			test_duplicate if { true }
			todo_test_skip if { true }
			todo_test_skip_too if { false }
			test_cases[x][y] if { x := "foo"; y := "bar" }
			test_duplicate.foo[y] if { x := "foo"; y := "bar" }
			test_duplicate[x][y] if { x := "foo"; y := "bar" }
			`,
		"/b_test.rego": `package bar
			import rego.v1

			test_duplicate if { true }`,
		"/c_test.rego": `package baz
			import rego.v1

			a.b.test_duplicate if { false }
			a.b.test_duplicate if { true }
			a.b.test_duplicate if { true }`,
	}

	cases := []struct {
		note  string
		regex string
		tests expectedTestResults
	}{
		{
			note:  "all tests match",
			regex: ".*",
			tests: expectedTestResults{
				{"data.foo", "test_pass"}:                  {false, false, false, nil},
				{"data.foo", "test_fail"}:                  {false, true, false, nil},
				{"data.foo", "test_fail_non_bool"}:         {false, true, false, nil},
				{"data.foo", "test_duplicate"}:             {false, true, false, nil},
				{"data.foo", "test_duplicate#01"}:          {false, false, false, nil},
				{"data.foo", "test_duplicate#02"}:          {false, false, false, nil},
				{"data.foo", "test_err"}:                   {true, false, false, nil},
				{"data.foo", "todo_test_skip"}:             {false, false, true, nil},
				{"data.foo", "todo_test_skip_too"}:         {false, false, true, nil},
				{"data.foo", "test_cases"}:                 {false, false, false, nil},
				{"data.foo", "test_duplicate#03"}:          {false, false, false, nil},
				{"data.foo", "test_duplicate#04"}:          {false, false, false, nil},
				{"data.bar", "test_duplicate"}:             {false, false, false, nil},
				{"data.baz", "a.b.test_duplicate"}:         {false, true, false, nil},
				{"data.baz", "a.b[\"test_duplicate#01\"]"}: {false, false, false, nil},
				{"data.baz", "a.b[\"test_duplicate#02\"]"}: {false, false, false, nil},
			},
		},
		{
			note:  "no filter",
			regex: "",
			tests: expectedTestResults{
				{"data.foo", "test_pass"}:                  {false, false, false, nil},
				{"data.foo", "test_fail"}:                  {false, true, false, nil},
				{"data.foo", "test_fail_non_bool"}:         {false, true, false, nil},
				{"data.foo", "test_duplicate"}:             {false, true, false, nil},
				{"data.foo", "test_duplicate#01"}:          {false, false, false, nil},
				{"data.foo", "test_duplicate#02"}:          {false, false, false, nil},
				{"data.foo", "test_err"}:                   {true, false, false, nil},
				{"data.foo", "todo_test_skip"}:             {false, false, true, nil},
				{"data.foo", "todo_test_skip_too"}:         {false, false, true, nil},
				{"data.foo", "test_cases"}:                 {false, false, false, nil},
				{"data.foo", "test_duplicate#03"}:          {false, false, false, nil},
				{"data.foo", "test_duplicate#04"}:          {false, false, false, nil},
				{"data.bar", "test_duplicate"}:             {false, false, false, nil},
				{"data.baz", "a.b.test_duplicate"}:         {false, true, false, nil},
				{"data.baz", "a.b[\"test_duplicate#01\"]"}: {false, false, false, nil},
				{"data.baz", "a.b[\"test_duplicate#02\"]"}: {false, false, false, nil},
			},
		},
		{
			note:  "no tests match",
			regex: "^$",
			tests: nil,
		},
		{
			note:  "single package name",
			regex: "bar",
			tests: expectedTestResults{
				{"data.bar", "test_duplicate"}: {false, false, false, nil},
			},
		},
		{
			note:  "single package explicit",
			regex: "data.bar.test_duplicate",
			tests: expectedTestResults{
				{"data.bar", "test_duplicate"}: {false, false, false, nil},
			},
		},
		{
			note:  "single test",
			regex: "test_pass",
			tests: expectedTestResults{
				{"data.foo", "test_pass"}: {false, false, false, nil},
			},
		},
		{
			note:  "single test explicit",
			regex: "data.foo.test_pass",
			tests: expectedTestResults{
				{"data.foo", "test_pass"}: {false, false, false, nil},
			},
		},
		{
			note:  "single test skipped explicit",
			regex: "data.foo.todo_test_skip_too",
			tests: expectedTestResults{
				{"data.foo", "todo_test_skip_too"}: {false, false, true, nil},
			},
		},
		{
			note:  "wildcards",
			regex: "^.*foo.*_fail.*$",
			tests: expectedTestResults{
				{"data.foo", "test_fail"}:          {false, true, false, nil},
				{"data.foo", "test_fail_non_bool"}: {false, true, false, nil},
			},
		},
		{
			note:  "mixed",
			regex: "(bar|data.foo.test_pass)",
			tests: expectedTestResults{
				{"data.foo", "test_pass"}:      {false, false, false, nil},
				{"data.bar", "test_duplicate"}: {false, false, false, nil},
			},
		},
		{
			note:  "case insensitive",
			regex: "(?i)DATA.BAR",
			tests: expectedTestResults{
				{"data.bar", "test_duplicate"}: {false, false, false, nil},
			},
		},
		{
			note:  "matching ref rule halfways",
			regex: "data.baz.a",
			tests: expectedTestResults{
				{"data.baz", "a.b.test_duplicate"}:         {false, true, false, nil},
				{"data.baz", "a.b[\"test_duplicate#01\"]"}: {false, false, false, nil},
				{"data.baz", "a.b[\"test_duplicate#02\"]"}: {false, false, false, nil},
			},
		},
		{
			note:  "matching sub-test rule",
			regex: "data.foo.test_cases",
			tests: expectedTestResults{
				{"data.foo", "test_cases"}: {false, false, false, nil},
			},
		},
	}

	test.WithTempFS(files, func(d string) {

		for _, tc := range cases {
			t.Run(tc.note, func(t *testing.T) {
				conf := testRunConfig{filter: tc.regex}
				rs, _ := doTestRunWithTmpDir(t, d, conf)
				validateTestResults(t, tc.tests, rs, conf)
			})
		}
	})
}

func TestRunnerCancel(t *testing.T) {
	testCancel(t, false)
}

func TestRunnerCancelBenchmark(t *testing.T) {
	testCancel(t, true)
}

func testCancel(t *testing.T, bench bool) {

	registerSleepBuiltin()

	ctx, cancel := context.WithCancel(t.Context())

	module := `package foo
	import rego.v1

	test_1 if { test.sleep("100ms") }
	test_2 if { true }`

	files := map[string]string{
		"/a_test.rego": module,
	}

	test.WithTempFS(files, func(d string) {
		paths := []string{d}
		modules, store, err := tester.Load(paths, nil)
		if err != nil {
			t.Fatal(err)
		}

		txn := storage.NewTransactionOrDie(ctx, store)
		runner := tester.NewRunner().SetStore(store).SetModules(modules)

		// Everything below uses a canceled context..
		cancel()

		var ch chan *tester.Result
		if bench {
			ch, err = runner.RunBenchmarks(ctx, txn, tester.BenchmarkOptions{})
		} else {
			ch, err = runner.RunTests(ctx, txn)
		}
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}
		var results []*tester.Result
		for r := range ch {
			results = append(results, r)
		}

		if len(results) != 0 {
			t.Fatalf("Expected no tests to be run but, got: %d", len(results))
		}
	})
}

func TestRunnerTimeout(t *testing.T) {
	test.Skip(t)
	testTimeout(t, false)
}

func TestRunnerTimeoutBenchmark(t *testing.T) {
	testTimeout(t, true)
}

func testTimeout(t *testing.T, bench bool) {
	registerSleepBuiltin()

	ctx := t.Context()

	files := map[string]string{
		"/a_test.rego": `package foo
		import rego.v1

		test_1 if { test.sleep("100ms") }

		# 1ms is low enough for a single test to pass,
		# but long enough for benchmark to timeout
		test_2 if { test.sleep("1ms") }`,
	}

	test.WithTempFS(files, func(d string) {
		paths := []string{d}
		modules, store, err := tester.Load(paths, nil)
		if err != nil {
			t.Fatal(err)
		}
		duration, err := time.ParseDuration("15ms")
		if err != nil {
			t.Fatal(err)
		}

		txn := storage.NewTransactionOrDie(ctx, store)
		runner := tester.NewRunner().SetTimeout(duration).SetStore(store).SetModules(modules)

		var ch chan *tester.Result
		if bench {
			ch, err = runner.RunBenchmarks(ctx, txn, tester.BenchmarkOptions{})
		} else {
			ch, err = runner.RunTests(ctx, txn)
		}
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}
		var results []*tester.Result
		for r := range ch {
			results = append(results, r)
		}

		if bench {
			if !topdown.IsCancel(results[1].Error) {
				t.Fatalf("Expected cancel error for second test but got: %v", results[1].Error)
			}
		} else {
			if !topdown.IsCancel(results[1].Error) {
				t.Fatalf("Expected test to have timed out")
			}
		}
	})
}

func TestRunnerPrintOutput(t *testing.T) {

	files := map[string]string{
		"/test.rego": `package test
		import rego.v1

		test_a if { print("A") }
		test_b if { false; print("B") }
		test_c if { print("C"); false }
		p.q.r.test_d if { print("D") }`,
		"/test2.rego": `package test
		import rego.v1

		test_d if { print("D") }
		test_e if { false; print("E") }
		test_f if { print("F"); false }
		p.q.r.test_g if { print("G") }`,
		"/test3.rego": `package test
		import rego.v1

		test_h if { print("H") }
		test_i if { false; print("I") }
		test_j if { print("J"); false }
		p.q.r.test_k if { print("K") }`,
	}

	ctx := t.Context()

	test.WithTempFS(files, func(d string) {
		paths := []string{d}
		modules, store, err := tester.Load(paths, nil)
		if err != nil {
			t.Fatal(err)
		}

		txn := storage.NewTransactionOrDie(ctx, store)
		runner := tester.NewRunner().SetStore(store).SetModules(modules).CapturePrintOutput(true)
		ch, err := runner.RunTests(ctx, txn)
		if err != nil {
			t.Fatal(err)
		}

		exp := map[string]map[string]string{
			"test.rego": {
				"test_a":       "A\n",
				"test_b":       "",
				"test_c":       "C\n",
				"p.q.r.test_d": "D\n",
			},
			"test2.rego": {
				"test_d":       "D\n",
				"test_e":       "",
				"test_f":       "F\n",
				"p.q.r.test_g": "G\n",
			},
			"test3.rego": {
				"test_h":       "H\n",
				"test_i":       "",
				"test_j":       "J\n",
				"p.q.r.test_k": "K\n",
			},
		}

		got := map[string]string{}
		var lastFile string
		for r := range ch {
			if lastFile == "" {
				lastFile = filepath.Base(r.Location.File)
			} else if lastFile != filepath.Base(r.Location.File) {
				// assert that all expected results for the file has been received
				// the individual files could be out of order, but it has to be grouped by file
				if !maps.Equal(exp[lastFile], got) {
					t.Fatal("expected:", exp, "got:", got)
				}

				// clear got for the next file
				got = map[string]string{}
				lastFile = filepath.Base(r.Location.File)
			}

			got[r.Name] = string(r.Output)
		}

		// check the last file
		if !maps.Equal(exp[lastFile], got) {
			t.Fatal("expected:", exp, "got:", got)
		}
	})
}

func registerSleepBuiltin() {
	ast.RegisterBuiltin(&ast.Builtin{
		Name: "test.sleep",
		Decl: types.NewFunction(
			types.Args(types.S),
			types.Nl,
		),
	})

	topdown.RegisterBuiltinFunc("test.sleep", func(_ topdown.BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
		d, _ := time.ParseDuration(string(operands[0].Value.(ast.String)))
		time.Sleep(d)
		return iter(ast.NullTerm())
	})
}

func TestRunnerWithCustomBuiltin(t *testing.T) {

	var myBuiltinDecl = &ast.Builtin{
		Name: "my_sum",
		Decl: types.NewFunction(
			types.Args(
				types.N,
				types.N,
			),
			types.N,
		),
	}

	var myBuiltin = &tester.Builtin{
		Decl: myBuiltinDecl,
		Func: rego.Function2(
			&rego.Function{
				Name: myBuiltinDecl.Name,
				Decl: myBuiltinDecl.Decl,
			},
			func(_ rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
				var num1, num2 int
				if err := ast.As(a.Value, &num1); err != nil {
					return nil, err
				}
				if err := ast.As(b.Value, &num2); err != nil {
					return nil, err
				}
				return ast.IntNumberTerm(num1 + num2), nil
			},
		),
	}

	files := map[string]string{
		"/test.rego": `package test
		import rego.v1

		test_a if { my_sum(2,3) == 5 }
		test_b if { my_sum(5,4) == 1 }
		test_c if { my_sum(4,1.0) == 5 }`,
	}

	ctx := t.Context()

	test.WithTempFS(files, func(d string) {
		paths := []string{d}
		modules, store, err := tester.Load(paths, nil)
		if err != nil {
			t.Fatal(err)
		}
		txn := storage.NewTransactionOrDie(ctx, store)
		runner := tester.NewRunner().SetStore(store).SetModules(modules).AddCustomBuiltins([]*tester.Builtin{myBuiltin})
		ch, err := runner.RunTests(ctx, txn)
		if err != nil {
			t.Fatal(err)
		}

		var results []*tester.Result

		for r := range ch {
			results = append(results, r)
		}

		exp := map[string]bool{
			"test_a": true,
			"test_b": false,
			"test_c": false,
		}

		got := map[string]bool{}

		for _, tr := range results {
			got[tr.Name] = tr.Pass()
		}

		if !maps.Equal(exp, got) {
			t.Fatal("expected:", exp, "got:", got)
		}
	})
}

func TestRunnerWithBuiltinErrors(t *testing.T) {
	const ruleTemplate = `package test
	import rego.v1

	test_json_parsing if {
      x := json.unmarshal("%s")
	  x.test == 123
	}`

	testCases := []struct {
		desc          string
		json          string
		builtinErrors bool
		wantErr       bool
	}{
		{
			desc:          "Valid JSON with flag enabled does not raise an error",
			json:          `{\"test\": 123}`,
			builtinErrors: true,
		},
		{
			desc:          "Invalid JSON with flag enabled raises an error",
			json:          `test: 123`,
			builtinErrors: true,
			wantErr:       true,
		},
		{
			desc: "Invalid JSON with flag disabled does not raise an error",
			json: `test: 123`,
		},
	}

	ctx := t.Context()

	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			files := map[string]string{
				"builtin_error_test.rego": fmt.Sprintf(ruleTemplate, tc.json),
			}

			test.WithTempFS(files, func(d string) {
				paths := []string{d}
				modules, store, err := tester.Load(paths, nil)
				if err != nil {
					t.Fatal(err)
				}
				txn := storage.NewTransactionOrDie(ctx, store)
				runner := tester.
					NewRunner().
					SetStore(store).
					SetModules(modules).
					RaiseBuiltinErrors(tc.builtinErrors)

				ch, err := runner.RunTests(ctx, txn)
				if err != nil {
					t.Fatal(err)
				}
				for result := range ch {
					if gotErr := result.Error != nil; gotErr != tc.wantErr {
						t.Errorf("wantErr = %v, gotErr = %v", tc.wantErr, gotErr)
					}
				}
			})
		})
	}
}

func TestLoad_DefaultRegoVersion(t *testing.T) {
	tests := []struct {
		note    string
		module  string
		expErrs []string
	}{
		{
			note: "v0 module", // NOT default rego-version
			module: `package test

p[x] {
	x = "a"
}

test_p {
	p["a"]
}`,
			expErrs: []string{
				"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules",
				"test.rego:7: rego_parse_error: `if` keyword is required before rule body",
			},
		},
		{
			note: "import rego.v1",
			module: `package test
import rego.v1

p contains x if {
	x := "a"
}

test_p if {
	"a" in p
}`,
		},
		{
			note: "v1 module", // default rego-version
			module: `package test

p contains x if {
	x := "a"
}

test_p if {
	"a" in p
}`,
		},
	}

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

			test.WithTempFS(files, func(root string) {
				modules, store, err := tester.Load([]string{root}, nil)
				if len(tc.expErrs) > 0 {
					if err == nil {
						t.Fatalf("Expected error but got nil")
					}

					for _, expErr := range tc.expErrs {
						if !strings.Contains(err.Error(), expErr) {
							t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", expErr, err)
						}
					}
				} else {
					if err != nil {
						t.Fatalf("Unexpected error: %v", err)
					}

					if modules == nil {
						t.Fatalf("Expected modules to be non-nil")
					}

					if store == nil {
						t.Fatalf("Expected store to be non-nil")
					}
				}
			})
		})
	}
}

func TestRun_DefaultRegoVersion(t *testing.T) {
	tests := []struct {
		note    string
		module  ast.Module
		expErrs []string
	}{
		{
			note: "no v1 violations",
			module: ast.Module{
				Package: ast.MustParsePackage(`package test`),
				Rules: []*ast.Rule{
					ast.MustParseRule(`p[x] { x = "a" }`),
					ast.MustParseRule(`test_p { p["a"] }`),
				},
			},
		},
		{
			note: "v1 violations",
			module: ast.Module{
				Package: ast.MustParsePackage(`package test`),
				Imports: ast.MustParseImports(`
					import data.foo
					import data.bar as foo
				`),
				Rules: []*ast.Rule{
					ast.MustParseRule(`p[x] { x = "a" }`),
					ast.MustParseRule(`test_p { p["a"] }`),
				},
			},
			expErrs: []string{
				"rego_compile_error: import must not shadow import data.foo",
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			ctx := t.Context()

			modules := map[string]*ast.Module{
				"test": &tc.module,
			}

			store := inmem.New()
			txn := storage.NewTransactionOrDie(ctx, store)
			defer store.Abort(ctx, txn)

			runner := tester.NewRunner().
				SetStore(store).
				SetModules(modules).
				SetTimeout(10 * time.Second)

			ch, err := runner.RunTests(ctx, txn)

			if len(tc.expErrs) > 0 {
				if err == nil {
					t.Fatalf("Expected error but got nil")
				}

				for _, expErr := range tc.expErrs {
					if !strings.Contains(err.Error(), expErr) {
						t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", expErr, err)
					}
				}
			} else {
				if err != nil {
					t.Fatalf("Unexpected error: %v", err)
				}

				var rs []*tester.Result
				for r := range ch {
					rs = append(rs, r)
				}

				if len(rs) != 1 {
					t.Fatalf("Expected exactly one result but got: %v", rs)
				}

				if rs[0].Fail {
					t.Fatalf("Expected test to pass but it failed")
				}
			}
		})
	}
}

func TestReporterFormatsWithExplicitParallel(t *testing.T) {
	tests := []struct {
		note     string
		parallel int
		r        func(writer io.Writer) tester.Reporter
		exp      func(string)
	}{
		{
			note:     "Pretty Format",
			parallel: 10,
			r: func(w io.Writer) tester.Reporter {
				return tester.PrettyReporter{
					Output: w,
				}
			},
			exp: func(output string) {
				exp := `PASS: 4/4
`
				if exp != output {
					t.Fatalf("Expected (%d bytes):\n\n%v\n\nGot (%d bytes):\n\n%v", len(exp), exp, len(output), output)
				}
			},
		},
		{
			note:     "JSON Format",
			parallel: 10,
			r: func(w io.Writer) tester.Reporter {
				return tester.JSONReporter{
					Output: w,
				}
			},
			exp: func(output string) {
				// the order of the tests and filepath and duration will be different each execution
				var r []*tester.Result
				if err := json.Unmarshal([]byte(output), &r); err != nil {
					t.Fatalf("Unexpected error: %v", err)
				}
				if len(r) != 4 {
					t.Fatalf("Expected exactly 4 results but got: %v", r)
				}
			},
		},
		{
			note:     "Go Bench Format",
			parallel: 10,
			r: func(w io.Writer) tester.Reporter {
				return tester.PrettyReporter{
					Output:                 w,
					BenchMarkGoBenchFormat: true,
				}
			},
			exp: func(output string) {
				exp := `PASS: 4/4
`
				if exp != output {
					t.Fatalf("Expected (%d bytes):\n\n%v\n\nGot (%d bytes):\n\n%v", len(exp), exp, len(output), output)
				}
			},
		},
	}

	files := map[string]string{
		"/test.rego": `package test
		import rego.v1

		test_a if { print("A") }
		test_a if { print("A") }
		test_a if { print("A") }
		test_a if { print("A") }`,
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			ctx := t.Context()

			test.WithTempFS(files, func(d string) {
				paths := []string{d}
				modules, store, err := tester.Load(paths, nil)
				if err != nil {
					t.Fatal(err)
				}

				txn := storage.NewTransactionOrDie(ctx, store)
				runner := tester.NewRunner().SetStore(store).SetModules(modules).CapturePrintOutput(true)
				ch, err := runner.RunTests(ctx, txn)
				if err != nil {
					t.Fatal(err)
				}

				var buf bytes.Buffer

				r := tc.r(&buf)

				if err := r.Report(ch); err != nil {
					t.Fatal(err)
				}

				str := buf.String()

				tc.exp(str)
			})
		})
	}
}
