// 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 contains utilities for executing Rego tests.
package tester

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"maps"
	"regexp"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	wasm_errors "github.com/open-policy-agent/opa/internal/wasm/sdk/opa/errors"
	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/bundle"
	"github.com/open-policy-agent/opa/v1/cover"
	"github.com/open-policy-agent/opa/v1/loader"
	"github.com/open-policy-agent/opa/v1/metrics"
	"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/topdown"
	"github.com/open-policy-agent/opa/v1/topdown/builtins"
	"github.com/open-policy-agent/opa/v1/util"
	"github.com/open-policy-agent/opa/v1/util/channel"
)

// TestPrefix declares the prefix for all test rules.
const TestPrefix = "test_"

// SkipTestPrefix declares the prefix for tests that should be skipped.
const SkipTestPrefix = "todo_test_"

// Run executes all test cases found under files in path.
func Run(ctx context.Context, paths ...string) ([]*Result, error) {
	return RunWithFilter(ctx, nil, paths...)
}

// RunWithFilter executes all test cases found under files in path. The filter
// will be applied to exclude files that should not be included.
func RunWithFilter(ctx context.Context, _ loader.Filter, paths ...string) ([]*Result, error) {
	modules, store, err := Load(paths, nil)
	if err != nil {
		return nil, err
	}
	ch, err := NewRunner().SetStore(store).Run(ctx, modules)
	if err != nil {
		return nil, err
	}
	return channel.Collect(ch), nil
}

type SubResult struct {
	Name       string           `json:"name,omitempty"`
	Fail       bool             `json:"fail,omitempty"`
	Trace      []*topdown.Event `json:"-"`
	SubResults SubResultMap     `json:"sub_results,omitempty"`
}

type SubResultMap map[string]*SubResult

func (srm SubResultMap) Update(path ast.Array, trace []*topdown.Event) bool {
	buf := new(bytes.Buffer)
	strPath := make([]string, path.Len())
	for i := range path.Len() {
		strPath[i] = termToString(buf, path.Elem(i))
	}
	return srm.update(strPath, 0, trace)
}

func (srm SubResultMap) update(path []string, i int, trace []*topdown.Event) bool {
	if i >= len(path) {
		return true
	}

	k := path[i]
	entry, ok := srm[k]
	if !ok {
		entry = &SubResult{
			Name:       path[i],
			Fail:       true,
			SubResults: SubResultMap{},
		}
		srm[k] = entry
	}

	if i == len(path)-1 {
		entry.Trace = trace
		return entry.Fail
	}

	fail := entry.SubResults.update(path, i+1, trace)
	if fail {
		entry.Fail = true
	}

	return fail
}

type unknownResolver struct{}

func (unknownResolver) Resolve(_ ast.Ref) (any, error) {
	return "UNKNOWN", nil
}

type prefixMatchers []ast.Ref

// Match returns true if any of it's prefixes matches ref, or it contains no prefixes.
func (pm prefixMatchers) Match(ref ast.Ref) bool {
	return len(pm) == 0 || slices.ContainsFunc(pm, ref.HasPrefix)
}

// AnyPrefixMatcher returns true if prefix is a prefix of any of the matchers, or it contains no prefixes.
func (pm prefixMatchers) AnyPrefixMatcher(prefix ast.Ref) bool {
	return len(pm) == 0 || slices.ContainsFunc(pm, func(matcher ast.Ref) bool {
		return matcher.HasPrefix(prefix)
	})
}

func termToString(buf *bytes.Buffer, t *ast.Term) string {
	if ti, err := ast.ValueToInterface(t.Value, unknownResolver{}); err == nil {
		if str, ok := ti.(string); ok {
			return str
		}
		buf.Reset()
		if err := json.NewEncoder(buf).Encode(ti); err == nil {
			return strings.TrimSpace(buf.String())
		}
	}
	return "INVALID"
}

// Result represents a single test case result.
type Result struct {
	Location        *ast.Location            `json:"location"`
	Package         string                   `json:"package"`
	Name            string                   `json:"name"`
	Fail            bool                     `json:"fail,omitempty"`
	Error           error                    `json:"error,omitempty"`
	Skip            bool                     `json:"skip,omitempty"`
	Duration        time.Duration            `json:"duration"`
	Trace           []*topdown.Event         `json:"trace,omitempty"`
	Output          []byte                   `json:"output,omitempty"`
	FailedAt        *ast.Expr                `json:"failed_at,omitempty"`
	BenchmarkResult *testing.BenchmarkResult `json:"benchmark_result,omitempty"`
	SubResults      SubResultMap             `json:"sub_results,omitempty"`
}

func newResult(loc *ast.Location, pkg, name string, duration time.Duration, trace []*topdown.Event, output []byte) *Result {
	return &Result{
		Location:   loc,
		Package:    pkg,
		Name:       name,
		Duration:   duration,
		Trace:      trace,
		Output:     output,
		SubResults: SubResultMap{},
	}
}

// resultError mirrors the JSON shape *topdown.Error marshals to, which is the
// form errors take in the output of `opa test --format json`.
type resultError struct {
	Code       string             `json:"code,omitempty"`
	Message    string             `json:"message,omitempty"`
	Location   *ast.Location      `json:"location,omitempty"`
	StackTrace topdown.StackTrace `json:"stack_trace,omitempty"`
}

func (e *resultError) error() error {
	if e == nil {
		return nil
	}

	if e.Code != "" {
		return &topdown.Error{
			Code:       e.Code,
			Message:    e.Message,
			Location:   e.Location,
			StackTrace: e.StackTrace,
		}
	}

	return errors.New(e.Message)
}

// resultAlias drops the methods of Result so that unmarshalling into it below
// doesn't recurse. The anonymous struct shadows the embedded error interface
// field, which is marshallable but not unmarshallable, with a concrete type.
type resultAlias Result

// UnmarshalJSON reads back the JSON produced for a Result. Marshalling is left
// to the default encoder so that existing output is unchanged.
func (r *Result) UnmarshalJSON(bs []byte) error {
	aux := struct {
		*resultAlias
		Error *resultError `json:"error,omitempty"`
	}{
		resultAlias: (*resultAlias)(r),
	}

	if err := json.Unmarshal(bs, &aux); err != nil {
		return err
	}

	r.Error = aux.Error.error()

	return nil
}

// Pass returns true if the test case passed.
func (r *Result) Pass() bool {
	return !r.Fail && !r.Skip && r.Error == nil
}

func (r *Result) String() string {
	return r.string(true)
}

func (r *Result) string(subResults bool) string {
	var buf bytes.Buffer

	buf.WriteString(r.Package)
	buf.WriteByte('.')
	buf.WriteString(r.Name)
	buf.WriteString(": ")
	buf.WriteString(r.outcome())

	if r.Skip {
		return buf.String()
	}

	buf.WriteString(" (")
	buf.WriteString(r.Duration.String())
	buf.WriteByte(')')

	if subResults {
		buf.WriteByte('\n')
		buf.WriteString(r.SubResults.String())
	}

	return buf.String()
}

func (r *Result) outcome() string {
	if r.Pass() {
		return "PASS"
	}
	if r.Fail {
		return "FAIL"
	}
	if r.Skip {
		return "SKIPPED"
	}
	return "ERROR"
}

func (sr *SubResult) String() string {
	return sr.Name + ": " + sr.outcome()
}

func (sr *SubResult) outcome() string {
	if sr.Fail {
		return "FAIL"
	}
	return "PASS"
}

// Iter is a depth-first iterator over all sub-results.
func (srm SubResultMap) Iter(yield func([]string, *SubResult) bool) {
	srm.iter(nil, yield)
}

func (srm SubResultMap) iter(namePrefix []string, yield func([]string, *SubResult) bool) {
	for _, k := range util.KeysSorted(srm) {
		sr := srm[k]

		fullName := make([]string, len(namePrefix)+1)
		copy(fullName, namePrefix)
		fullName[len(fullName)-1] = k

		if !yield(fullName, sr) {
			return
		}
		sr.SubResults.iter(fullName, yield)
	}
}

func (srm SubResultMap) String() string {
	return srm.string("  ")
}

func (srm SubResultMap) string(indent string) string {
	var buf bytes.Buffer
	for fullName, sr := range srm.Iter {
		buf.WriteString(strings.Repeat(indent, len(fullName)-1))
		buf.WriteString(sr.String())
		buf.WriteByte('\n')
	}
	return buf.String()
}

// BenchmarkOptions defines options specific to benchmarking tests
type BenchmarkOptions struct {
	ReportAllocations bool
}

// Runner implements simple test discovery and execution.
type Runner struct {
	compiler              *ast.Compiler
	store                 storage.Store
	cover                 topdown.QueryTracer
	coverNoIndex          *cover.Cover
	coverNoEarlyExit      *cover.Cover
	coverageRuns          []cover.Kind
	coverRunsRegistered   bool
	trace                 bool
	enablePrintStatements bool
	raiseBuiltinErrors    bool
	stackTraces           bool
	runtime               *ast.Term
	timeout               time.Duration
	modules               map[string]*ast.Module
	bundles               map[string]*bundle.Bundle
	prefixMatchers        prefixMatchers
	filter                string
	target                string // target type (wasm, rego, etc.)
	customBuiltins        []*Builtin
	defaultRegoVersion    ast.RegoVersion
	parallel              int
	// seed, when set, seeds non-deterministic builtins via rego.EvalSeed
	// across all coverage passes, making their results reproducible.
	seed io.Reader
}

// NewRunner returns a new runner.
func NewRunner() *Runner {
	return &Runner{
		enablePrintStatements: true,
		timeout:               5 * time.Second,
		defaultRegoVersion:    ast.DefaultRegoVersion,
		parallel:              runtime.NumCPU(),
		coverageRuns:          []cover.Kind{cover.KindIndexExcluded, cover.KindEarlyExit},
	}
}

// SetParallel sets the number of tests that can run in parallel
func (r *Runner) SetParallel(parallel int) *Runner {
	if parallel < 1 {
		parallel = runtime.NumCPU()
	}

	r.parallel = parallel

	return r
}

// SetDefaultRegoVersion sets the default Rego version to use when compiling modules.
// Not applicable if a custom [ast.Compiler] is set via [SetCompiler].
func (r *Runner) SetDefaultRegoVersion(v ast.RegoVersion) *Runner {
	r.defaultRegoVersion = v
	return r
}

// SetCompiler sets the compiler used by the runner.
func (r *Runner) SetCompiler(compiler *ast.Compiler) *Runner {
	r.compiler = compiler
	return r
}

// RaiseBuiltinErrors sets the runner to raise errors encountered by builtins
// such as parsing input.
func (r *Runner) RaiseBuiltinErrors(enabled bool) *Runner {
	r.raiseBuiltinErrors = enabled
	return r
}

// StackTraces sets the runner to record the stack of queries being evaluated
// when a test errors, reported as topdown.Error.StackTrace.
func (r *Runner) StackTraces(enabled bool) *Runner {
	r.stackTraces = enabled
	return r
}

type Builtin struct {
	Decl *ast.Builtin
	Func func(*rego.Rego)
}

func (r *Runner) AddCustomBuiltins(builtinsList []*Builtin) *Runner {
	r.customBuiltins = builtinsList
	return r
}

// SetStore sets the store to execute tests over.
func (r *Runner) SetStore(store storage.Store) *Runner {
	r.store = store
	return r
}

// SetCoverageTracer sets the tracer to use to compute coverage.
//
// Deprecated: Use SetCoverageQueryTracer instead.
func (r *Runner) SetCoverageTracer(tracer topdown.Tracer) *Runner {
	if tracer == nil {
		return r
	}
	if qt, ok := tracer.(topdown.QueryTracer); ok {
		return r.SetCoverageQueryTracer(qt)
	}
	return r.SetCoverageQueryTracer(topdown.WrapLegacyTracer(tracer))
}

// SetCoverageQueryTracer sets the tracer to use to compute coverage. If
// tracer is a *cover.Cover, the runner also evaluates each test with rule
// indexing and early exit disabled, tagging any extra coverage they reveal.
// Which of those supplementary passes run is controlled by SetCoverageRuns.
func (r *Runner) SetCoverageQueryTracer(tracer topdown.QueryTracer) *Runner {
	if tracer == nil {
		return r
	}
	r.cover = tracer
	r.coverRunsRegistered = false
	return r
}

// SetCoverageRuns sets which supplementary coverage passes run when the
// coverage tracer is a *cover.Cover. Each pass re-evaluates tests with an
// optimization disabled (rule indexing for KindIndexExcluded, early exit for
// KindEarlyExit) to tag the extra not-covered ranges it reveals. A subset, or
// an empty slice, skips the unwanted passes and their cost.
func (r *Runner) SetCoverageRuns(kinds []cover.Kind) *Runner {
	r.coverageRuns = kinds
	return r
}

// CapturePrintOutput captures print() call outputs during evaluation and
// includes the output in test results. If not set, defaults to true. Print
// output is always disabled in benchmarks.
func (r *Runner) CapturePrintOutput(yes bool) *Runner {
	r.enablePrintStatements = yes
	return r
}

// EnableTracing enables tracing of evaluation and includes traces in results.
// Tracing is currently mutually exclusive with coverage.
func (r *Runner) EnableTracing(yes bool) *Runner {
	r.trace = yes
	if r.trace {
		r.cover = nil
		r.coverNoIndex = nil
		r.coverNoEarlyExit = nil
		r.coverRunsRegistered = false
	}
	return r
}

// SetRuntime sets runtime information to expose to the evaluation engine.
func (r *Runner) SetRuntime(term *ast.Term) *Runner {
	r.runtime = term
	return r
}

// SetTimeout sets the timeout for the individual test cases
func (r *Runner) SetTimeout(timout time.Duration) *Runner {
	r.timeout = timout
	return r
}

// SetModules will add modules to the Runner which will be compiled then used
// for discovering and evaluating tests.
func (r *Runner) SetModules(modules map[string]*ast.Module) *Runner {
	r.modules = modules
	return r
}

// SetBundles will add bundles to the Runner which will be compiled then used
// for discovering and evaluating tests.
func (r *Runner) SetBundles(bundles map[string]*bundle.Bundle) *Runner {
	r.bundles = bundles
	return r
}

// SetPrefixMatchers will have the test runner use the provided ref prefixes as a
// filter for which test cases to run- This could either be a shorther path to e.g.
// run all tests in one or more packages, or a full refs pointing to to individual
// tests. Potential matches are evaluated in ascending order based on ref length.
//
// Experimental: This function is used to allow editor integrations more
// flexibility in selecting which tests to run, and the implementation may
// change depending on what we learn from that.
func (r *Runner) SetPrefixMatchers(prefixes ...ast.Ref) *Runner {
	r.prefixMatchers = slices.SortedFunc(slices.Values(prefixes), util.SliceLenCompare)
	return r
}

// Filter will set a test name regex filter for the test runner. Only test
// cases which match the filter will be run.
func (r *Runner) Filter(regex string) *Runner {
	r.filter = regex
	return r
}

// Target sets the output target type to use.
func (r *Runner) Target(target string) *Runner {
	r.target = target
	return r
}

// Run executes all tests contained in supplied modules.
//
// Deprecated: Use RunTests and the Runner#SetModules or Runner#SetBundles
// helpers instead. This will NOT use the modules or bundles set on the Runner.
func (r *Runner) Run(ctx context.Context, modules map[string]*ast.Module) (chan *Result, error) {
	return r.SetModules(modules).RunTests(ctx, nil)
}

// RunTests executes tests found in either modules or bundles loaded on the runner.
// Test results are sent as they complete and may arrive in any order.
func (r *Runner) RunTests(ctx context.Context, txn storage.Transaction) (chan *Result, error) {
	return r.runTests(ctx, txn, r.enablePrintStatements, r.runTest, r.parallel)
}

// RunBenchmarks executes tests similar to tester.Runner#RunTests but will repeat
// a number of times to get stable performance metrics.
// Each benchmark test is run sequentially.
func (r *Runner) RunBenchmarks(ctx context.Context, txn storage.Transaction, options BenchmarkOptions) (chan *Result, error) {
	return r.runTests(ctx, txn, false, func(ctx context.Context, txn storage.Transaction, module *ast.Module, rule *ast.Rule) (result *Result, b bool) {
		return r.runBenchmark(ctx, txn, module, rule, options)
	}, 1)
}

type run func(context.Context, storage.Transaction, *ast.Module, *ast.Rule) (*Result, bool)

func (r *Runner) setupTestRun(ctx context.Context, txn storage.Transaction, enablePrintStatements bool) (*regexp.Regexp, error) {
	var testRegex *regexp.Regexp
	var err error

	if r.filter != "" {
		testRegex, err = regexp.Compile(r.filter)
		if err != nil {
			return nil, err
		}
	}

	if r.compiler == nil {
		capabilities := ast.CapabilitiesForThisVersion()

		// Add custom builtins declarations to compiler
		for _, builtin := range r.customBuiltins {
			capabilities.Builtins = append(capabilities.Builtins, builtin.Decl)
		}

		r.compiler = ast.NewCompiler().
			WithCapabilities(capabilities).
			WithEnablePrintStatements(enablePrintStatements).
			WithDefaultRegoVersion(r.defaultRegoVersion)
	}

	// rewrite duplicate test_* rule names as we compile modules
	r.compiler.WithStageAfterID(ast.StageRewriteRuleHeadRefs, ast.CompilerStageDefinition{
		Name:       "RewriteDuplicateTestNames",
		MetricName: "rewrite_duplicate_test_names",
		Stage:      rewriteDuplicateTestNames,
	})

	r.compiler.WithStageAfterID(ast.StageRewriteLocalVars, ast.CompilerStageDefinition{
		Name:       "InjectTestCaseFunc",
		MetricName: "inject_test_case_func",
		Stage:      injectTestCaseFunc,
	})

	if r.store == nil {
		r.store = inmem.NewWithOpts(inmem.OptRoundTripOnWrite(false))
	}

	if len(r.bundles) > 0 {
		if txn == nil {
			return nil, errors.New("unable to activate bundles: storage transaction is nil")
		}

		// Activate the bundle(s) to get their info and policies into the store
		// the actual compiled policies will be overwritten later.
		err := bundle.Activate(&bundle.ActivateOpts{
			Ctx:           ctx,
			Store:         r.store,
			Txn:           txn,
			Compiler:      r.compiler,
			Metrics:       metrics.New(),
			Bundles:       r.bundles,
			ParserOptions: ast.ParserOptions{RegoVersion: r.defaultRegoVersion},
		})
		if err != nil {
			return nil, err
		}

		// Aggregate the bundle modules with other ones provided
		if r.modules == nil {
			r.modules = map[string]*ast.Module{}
		}
		for path, b := range r.bundles {
			maps.Copy(r.modules, b.ParsedModules(path))
		}
	}

	if len(r.modules) > 0 {
		if r.compiler.Compile(r.modules); r.compiler.Failed() {
			return nil, r.compiler.Errors
		}
	}

	return testRegex, nil
}

func (r *Runner) runTests(ctx context.Context, txn storage.Transaction, enablePrintStatements bool, runFunc run, parallel int) (chan *Result, error) {
	// Register the supplementary coverage passes on the baseline *cover.Cover,
	// honoring the kinds set by SetCoverageRuns. Doing it here (rather than in
	// the setters) keeps SetCoverageQueryTracer and SetCoverageRuns
	// order-independent; the guard stops repeated runs (RunBenchmarks or a
	// --count loop reusing the runner) from registering the passes twice.
	if cc, ok := r.cover.(*cover.Cover); ok && !r.coverRunsRegistered {
		if slices.Contains(r.coverageRuns, cover.KindIndexExcluded) {
			r.coverNoIndex = cover.New()
			cc.AddRun(cover.KindIndexExcluded, r.coverNoIndex)
		}
		if slices.Contains(r.coverageRuns, cover.KindEarlyExit) {
			r.coverNoEarlyExit = cover.New()
			cc.AddRun(cover.KindEarlyExit, r.coverNoEarlyExit)
		}
		r.coverRunsRegistered = true
	}

	testRegex, err := r.setupTestRun(ctx, txn, enablePrintStatements)
	if err != nil {
		return nil, err
	}

	ch := make(chan *Result)

	go func() {
		defer close(ch)

		var wg sync.WaitGroup
		semaphore := make(chan struct{}, parallel)
		stopCtx, cancelTests := context.WithCancel(ctx)
		defer cancelTests()

		for _, module := range r.compiler.Modules {
			// If:
			// 1. prefix matchers have been provided, and
			// 2. no provided prefix matches the package path, and
			// 3. the package path is not a prefix of any provided matcher
			// We can skip this the module entirely
			if !r.prefixMatchers.Match(module.Package.Path) && !r.prefixMatchers.AnyPrefixMatcher(module.Package.Path) {
				continue
			}

			for _, rule := range module.Rules {
				wg.Go(func() {
					select {
					case <-stopCtx.Done():
						return
					default:
					}

					select {
					case semaphore <- struct{}{}:
						defer func() { <-semaphore }()
					case <-stopCtx.Done():
						return
					}

					if !r.shouldRun(rule, testRegex) {
						return
					}

					tr, stop := func() (*Result, bool) {
						runCtx, cancel := context.WithTimeout(ctx, r.timeout)
						defer cancel()
						return runFunc(runCtx, txn, module, rule)
					}()
					ch <- tr
					if stop {
						cancelTests()
					}
				})
			}
		}
		wg.Wait()
	}()

	return ch, nil
}

func (r *Runner) shouldRun(rule *ast.Rule, testRegex *regexp.Regexp) bool {
	ruleRef := rule.Head.Ref().GroundPrefix()
	// len check even though the Match function does this already, as we'll
	// want to avoid the Extend allocations in the common case of no prefixes configured
	if len(r.prefixMatchers) > 0 && !r.prefixMatchers.Match(rule.Module.Package.Path.Extend(ruleRef)) {
		return false
	}

	var ref ast.Ref

	for _, term := range ruleRef {
		ref = ref.Append(term)

		var n string
		switch v := term.Value.(type) {
		case ast.Var:
			n = string(v)
		case ast.String:
			n = string(v)
		}

		if strings.HasPrefix(n, TestPrefix) || strings.HasPrefix(n, SkipTestPrefix) {
			// Even with the prefix it needs to pass the regex (if applicable)
			fullName := rule.Module.Package.Path.Extend(ref).String()
			if testRegex != nil && !testRegex.MatchString(fullName) {
				return false
			}

			return true
		}
	}

	return false
}

// rewriteDuplicateTestNames will rewrite duplicate test names to have a numbered suffix.
// This uses a global "count" of each to ensure compiling more than once as new modules
// are added can't introduce duplicates again.
func rewriteDuplicateTestNames(compiler *ast.Compiler) *ast.Error {
	count := map[string]int{}
	for _, mod := range compiler.Modules {
		for _, rule := range mod.Rules {
			name, ref := ruleName(rule.Head)
			if !strings.HasPrefix(name, TestPrefix) {
				continue
			}

			key := mod.Package.Path.Extend(ref).String()
			if k, ok := count[key]; ok {
				dynamicSuffix := rule.Head.Ref()[len(ref):]
				newName := fmt.Sprintf("%s#%02d", name, k)
				if len(ref) == 1 {
					ref[0] = ast.VarTerm(newName)
				} else {
					ref[len(ref)-1] = ast.StringTerm(newName)
				}
				rule.Head.SetRef(append(ref, dynamicSuffix...))
			}
			count[key]++
		}
	}
	return nil
}

// injectTestCaseFunc will inject a call to the 'internal.test_case' function into partial-object test rules.
// We attempt to find the earliest point in the rule body where we can inject the call, to ensure that the test-case
// function is called as early as possible so that we capture as many failed test cases as possible.
// This may require us to move generated assignment expressions up the body.
// We do not attempt to move non-generated expressions, as that could contradict author intent.
//
// Consider the test rule:
//
//	test_concat[tc.note] if {
//		some tc in [{
//			"note": "empty + empty",
//			"a": [],
//			"b": [],
//			"exp": [],
//		}]
//		act := array.concat(tc.a, tc.b)
//		act == tc.exp
//	}
//
// The compiler will rewrite this rule to (mid-stage @ 'RewriteLocalVars'):
//
//	test_concat[__local0__] := true if {
//		__local3__ = [{"a": [], "b": [], "exp": [], "note": "empty + empty"}][__local2__]
//		__local4__ = array.concat(__local3__.a, __local3__.b)
//		__local4__ == __local3__.exp
//		__local0__ = __local3__.note # generated var
//	}
//
// We move the generated var assignment as far up the body as possible, and inject the test-case function below it:
//
//	test_concat[__local0__] := true if {
//		__local3__ = [{"a": [], "b": [], "exp": [], "note": "empty + empty"}][__local2__]
//		__local0__ = __local3__.note                          # moved up
//		internal.test_case([__local0__])                      # injected
//		__local4__ = array.concat(__local3__.a, __local3__.b) # this and below expressions can now fail eval and we will still have captured the test-case
//		__local4__ == __local3__.exp
//	}
func injectTestCaseFunc(compiler *ast.Compiler) *ast.Error {
	for _, mod := range compiler.Modules {
		for _, rule := range mod.Rules {
			// Only apply to test rules
			rName, rRef := ruleName(rule.Head)
			if !strings.HasPrefix(rName, TestPrefix) {
				continue
			}

			// Only apply to rules that doesn't have manual use of the test-case function
			manualCall := false
			ast.WalkExprs(rule.Body, func(expr *ast.Expr) bool {
				if expr.IsCall() && expr.Operator().Equal(ast.Interned.Refs.InternalTestCase) {
					manualCall = true
					return true
				}
				return false
			})

			if manualCall {
				continue
			}

			// Construct test-case name
			ref := rule.Head.Ref()
			if len(ref) <= len(rRef) {
				// We only inject the test-case function if there is a rule ref "tail" behind the rule name
				continue
			}
			argsRef := ref[len(rRef):]
			args := ast.NewArray(argsRef...)

			//
			// Pass 1: Move generated assignment expressions up the body
			//

			for _, term := range argsRef {
				// We expect to find generated expressions - if any - at the tail of the body, so we start from the end
				for i := len(rule.Body) - 1; i >= 0; {
					expr := rule.Body[i]
					moved := false

					// If the expression is a generated assignment of a var in the head ref, we attempt to move it as far
					// up the body as possible.
					// This is a shallow move, we don't attempt to detect multiple levels of indirection and don't move such expressions; in such case, we move the assigning expression up to the first reference.
					// Once done for all vars in the head ref, we can inject the test case function below the last (possibly moved) such expr.
					// Note: We don't move non-generated expressions, as that could contradict author intent.
					if expr.Generated && (expr.IsEquality() || expr.IsAssignment()) && expr.Operand(0).Equal(term) {
						// Based on the vars in the rhs of the expr, see if we can move it up the rule body
						// FIXME: Can we get away with just placing it under the lowes first occurrence of any referenced var?
						vars := ast.NewVarSet()
						ast.WalkVars(expr.Operand(1), func(v ast.Var) bool {
							// We only care about local vars
							if isLocalVar(v) {
								vars.Add(v)
							}
							return false
						})

						if len(vars) == 0 {
							// No local vars referenced, can be moved to top of body
							rule.Body, moved = moveExpr(rule.Body, i, 0)
						} else {
							// Find the lowest (highest up the body) individual index of each var referenced in the rhs,
							// and select the highest (lowest down the body) of those

							lowest := util.NewHasherMap[ast.Var, int](cmpEqual)

							for j := i - 1; j >= 0; j-- {
								expr := rule.Body[j]
								ast.WalkVars(expr, func(v ast.Var) bool {
									if vars.Contains(v) {
										// We override the value for each var, so we get the lowest index
										// (line highest up the body) for each
										lowest.Put(v, j)
										return true
									}
									return false
								})
							}

							highest := 0
							lowest.Iter(func(_ ast.Var, v int) bool {
								if v > highest {
									highest = v
								}

								return false
							})

							if highest < i {
								// The expression is lower in the body than the lowest line of any expression that might
								// contribute to its assignment. Move the expression to just after the lowest line.
								rule.Body, moved = moveExpr(rule.Body, i, highest+1)
							}
						}
					}

					// If the expression was moved, we need to re-evaluate the current index, as it contains a new expression
					if !moved {
						i--
					}
				}
			}

			//
			// Pass 2: Inject the test-case function below the lowest first occurrence of any referenced var
			//

			injectBelowMap := ast.NewValueMap()
			for _, term := range argsRef {
				for i, expr := range slices.Backward(rule.Body) {
					ast.WalkVars(expr, func(v ast.Var) bool {
						if v.Equal(term.Value) {
							injectBelowMap.Put(v, ast.Number(strconv.Itoa(i)))
						}
						return false
					})
				}
			}

			// Find the earliest point where the test case function can be injected
			injectBelow := -1
			injectBelowMap.Iter(func(_, v ast.Value) bool {
				if n, err := strconv.Atoi(string(v.(ast.Number))); err == nil {
					if n > injectBelow {
						injectBelow = n
					}
				}
				return false
			})

			testCaseFuncExpr := ast.NewExpr([]*ast.Term{
				// Copied, as later compiler stages rewrite this body in place.
				ast.NewTerm(ast.Interned.Refs.InternalTestCase.Copy()),
				ast.NewTerm(args),
			})

			rule.Body = insertExpr(rule.Body, testCaseFuncExpr, injectBelow+1)
		}
	}
	return nil
}

func cmpEqual[T comparable](a, b T) bool {
	return a == b
}

func isLocalVar(v ast.Value) bool {
	if v, ok := v.(ast.Var); ok {
		if strings.HasPrefix(string(v), ast.LocalVarPrefix) {
			return true
		}
	}
	return false
}

func insertExpr(body ast.Body, expr *ast.Expr, index int) ast.Body {
	if index <= 0 {
		return append(ast.Body{expr}, body...)
	}

	if index >= len(body) {
		return append(body, expr)
	}

	return append(body[:index], append(ast.Body{expr}, body[index:]...)...)
}

func moveExpr(body ast.Body, from int, to int) (ast.Body, bool) {
	if from == to {
		return body, false
	}

	expr := body[from]                                                // Save the expression to move
	body = slices.Delete(body, from, from+1)                          // Remove the expression from the body
	body = append(body[:to], append(ast.Body{expr}, body[to:]...)...) // Insert the expression at the new position
	return body, true
}

// ruleName is a helper to be used when checking if a function
// (a) is a test, or
// (b) needs to be skipped
// -- it'll resolve `p.q.r` to `r`. For representing results, we'll
// use rule.Head.Ref()
func ruleName(h *ast.Head) (string, ast.Ref) {
	var n string

	rgp := h.Ref().GroundPrefix()
	i := 0

	for i = range rgp {
		switch v := rgp[i].Value.(type) {
		case ast.Var:
			n = string(v)
		case ast.String:
			n = string(v)
		default:
			n = ""
		}
		if strings.HasPrefix(n, TestPrefix) || strings.HasPrefix(n, SkipTestPrefix) {
			break
		}
	}

	return n, rgp[:i+1]
}

func (r *Runner) runTest(ctx context.Context, txn storage.Transaction, mod *ast.Module, rule *ast.Rule) (*Result, bool) {
	ruleName, ruleRef := ruleName(rule.Head)
	if strings.HasPrefix(ruleName, SkipTestPrefix) { // TODO(sr): add test
		tr := newResult(rule.Loc(), mod.Package.Path.String(), ruleRef.String(), 0*time.Second, nil, nil)
		tr.Skip = true
		return tr, false
	}

	var bufferTracer *topdown.BufferTracer
	var tracers []topdown.QueryTracer

	if r.trace {
		bufferTracer = topdown.NewBufferTracer()
		tracers = append(tracers, bufferTracer)
	} else {
		t := NewTestQueryTracer()
		tracers = append(tracers, t)
		bufferTracer = &t.BufferTracer
	}

	printbuf := bytes.NewBuffer(nil)
	var builtinErrors []topdown.Error
	queryPath := rule.Module.Package.Path.Extend(ruleRef)

	opts := []func(*rego.Rego){
		rego.Store(r.store),
		rego.Transaction(txn),
		rego.Compiler(r.compiler),
		rego.Query(queryPath.String()),
		rego.Runtime(r.runtime),
		rego.Target(r.target),
		rego.PrintHook(topdown.NewPrintHook(printbuf)),
		rego.StackTraces(r.stackTraces),
	}

	// Only collect built-in errors when they will be reported below. Supplying
	// the list unconditionally makes the evaluator hold on to every error a test
	// raises, and annotate each with a stack trace nothing reads.
	if r.raiseBuiltinErrors {
		opts = append(opts, rego.BuiltinErrorList(&builtinErrors))
	}

	rg := rego.New(opts...)

	// Register custom builtins on rego instance
	for _, v := range r.customBuiltins {
		v.Func(rg)
	}

	ctx = ast.WithCompiler(ctx, r.compiler)

	t0 := time.Now()

	// Tracers are passed as EvalOptions below, so only the baseline pass
	// feeds bufferTracer/TestQueryTracer/r.cover; the supplementary passes
	// use their own tracer and options.
	var rs rego.ResultSet
	pq, err := rg.PrepareForEval(ctx)
	if err == nil {
		evalOpts := make([]rego.EvalOption, 0, len(tracers)+4)
		evalOpts = append(evalOpts, rego.EvalTransaction(txn))
		if r.seed != nil {
			evalOpts = append(evalOpts, rego.EvalSeed(r.seed))
		}
		for _, t := range tracers {
			evalOpts = append(evalOpts, rego.EvalQueryTracer(t))
		}
		var ndbc builtins.NDBCache
		if r.cover != nil {
			evalOpts = append(evalOpts, rego.EvalQueryTracer(r.cover))
			// Share one non-deterministic builtin cache across all three passes so they take the same path.
			ndbc = builtins.NDBCache{}
			evalOpts = append(evalOpts, rego.EvalNDBuiltinCache(ndbc))
		}
		rs, err = pq.Eval(ctx, evalOpts...)
		if err == nil {
			// Results and errors here are discarded. A supplementary
			// run is only used for coverage data. We might consider tracking
			// errors in future if needed.
			supplementaryBase := []rego.EvalOption{rego.EvalTransaction(txn)}
			if r.seed != nil {
				supplementaryBase = append(supplementaryBase, rego.EvalSeed(r.seed))
			}
			if r.coverNoIndex != nil {
				_, _ = pq.Eval(
					ctx,
					append(supplementaryBase, cover.NoIndexingEvalOptions(r.coverNoIndex, ndbc)...)...,
				)
			}
			if r.coverNoEarlyExit != nil {
				_, _ = pq.Eval(
					ctx,
					append(supplementaryBase, cover.NoEarlyExitEvalOptions(r.coverNoEarlyExit, ndbc)...)...,
				)
			}
		}
	}
	dt := time.Since(t0)

	var trace []*topdown.Event
	if bufferTracer != nil {
		trace = *bufferTracer
	}

	tr := newResult(rule.Loc(), mod.Package.Path.String(), ruleRef.String(), dt, trace, printbuf.Bytes())

	// If there was an error other than errors from builtins, prefer that error.
	if err != nil {
		tr.Error = err
	} else if r.raiseBuiltinErrors && len(builtinErrors) > 0 {
		if len(builtinErrors) == 1 {
			tr.Error = &builtinErrors[0]
		} else {
			tr.Error = fmt.Errorf("%v", builtinErrors)
		}
	}

	var stop bool
	if err != nil {
		if topdown.IsCancel(err) || wasm_errors.IsCancel(err) {
			stop = ctx.Err() != context.DeadlineExceeded
		}
	} else if len(rs) == 0 {
		tr.Fail = true
	} else if rule.Head.DocKind() == ast.PartialObjectDoc {
		tr.Fail, tr.SubResults = subResults(rs[0].Expressions[0].Value, trace)
	} else if b, ok := rs[0].Expressions[0].Value.(bool); !ok || !b {
		tr.Fail = true
	}

	return tr, stop
}

func subResults(v any, trace []*topdown.Event) (bool, map[string]*SubResult) {
	if v == nil {
		return true, map[string]*SubResult{}
	}

	var fail bool
	result := SubResultMap{}

	if x, ok := v.(map[string]any); ok {
		for k, v := range x {
			result[k] = subResult(k, v)
			if result[k].Fail {
				fail = true
			}
		}
	}

	// Create failed sub-results and apply per-test-case traces.
	// For each test-case event, we capture the trace from first event up until the next test-case event.
	var testEvent *topdown.Event
	for i, e := range trace {
		if e.Op == topdown.TestCaseOp {
			if testEvent != nil {
				if p, ok := testCaseTerms(testEvent); ok {
					if f := result.Update(*p, trace[:i]); f {
						fail = true
					}
				}
			}

			testEvent = e
		}
	}
	if testEvent != nil {
		if p, ok := testCaseTerms(testEvent); ok {
			if f := result.Update(*p, trace); f {
				fail = true
			}
		}
	}

	return fail, result
}

func testCaseTerms(e *topdown.Event) (*ast.Array, bool) {
	if e == nil {
		return nil, false
	}

	if expr, ok := e.Node.(*ast.Expr); ok {
		if arr, ok := expr.Operand(0).Value.(*ast.Array); ok {
			return arr, true
		}
	}

	return nil, false
}

func subResult(n string, v any) *SubResult {
	if v == nil {
		return &SubResult{}
	}

	switch x := v.(type) {
	case map[string]any:
		fail, srs := subResults(x, nil)
		return &SubResult{
			Name:       n,
			Fail:       fail,
			SubResults: srs,
		}
	case bool:
		return &SubResult{
			Name: n,
			Fail: !x,
		}
	default:
		return &SubResult{
			Name: n,
			Fail: true,
		}
	}
}

func (r *Runner) runBenchmark(ctx context.Context, txn storage.Transaction, mod *ast.Module, rule *ast.Rule, options BenchmarkOptions) (*Result, bool) {
	ruleName, rf := ruleName(rule.Head)

	tr := &Result{
		Location: rule.Loc(),
		Package:  mod.Package.Path.String(),
		Name:     rf.String(), // TODO(sr): test
		Skip:     strings.HasPrefix(ruleName, SkipTestPrefix),
	}

	var stop bool

	t0 := time.Now()

	br := testing.Benchmark(func(b *testing.B) {
		pq, err := rego.New(
			rego.Store(r.store),
			rego.Transaction(txn),
			rego.Compiler(r.compiler),
			rego.Query(rule.Module.Package.Path.Extend(rule.Head.Ref().GroundPrefix()).String()),
			rego.Runtime(r.runtime),
			rego.Target(r.target),
		).PrepareForEval(ctx)

		if err != nil {
			tr.Fail = true
			b.Fatalf("Unexpected error: %s", err)
		}

		m := metrics.New()

		// Track memory allocations
		if options.ReportAllocations {
			b.ReportAllocs()
		}

		for b.Loop() {
			opts := []rego.EvalOption{rego.EvalTransaction(txn), rego.EvalMetrics(m)}

			var tracer *TestQueryTracer
			if rule.Head.DocKind() == ast.PartialObjectDoc {
				tracer = NewTestQueryTracer()
				opts = append(opts, rego.EvalQueryTracer(tracer))
			}

			rs, err := pq.Eval(ctx, opts...)

			if err != nil {
				tr.Error = err
				if topdown.IsCancel(err) && !(ctx.Err() == context.DeadlineExceeded) {
					stop = true
				}
				b.Fatalf("Unexpected error: %s", err)
			} else if len(rs) == 0 {
				tr.Fail = true
				b.Fatal("Expected boolean result, got `undefined`")
			} else if rule.Head.DocKind() == ast.PartialObjectDoc {
				tr.Fail, tr.SubResults = subResults(rs[0].Expressions[0].Value, tracer.Events())
			} else if pass, ok := rs[0].Expressions[0].Value.(bool); !ok || !pass {
				tr.Fail = true
				b.Fatal("Expected test to evaluate as true, got false")
			}
		}

		for k, v := range m.All() {
			var val float64
			switch v := v.(type) {
			case int64:
				val = float64(v)
			case uint64:
				val = float64(v)
			case float64:
				val = v
			default:
				continue // skip this metric
			}
			fv := val / float64(b.N)
			b.ReportMetric(fv, k+"/op")
		}
	})

	tr.Duration = time.Since(t0)
	tr.BenchmarkResult = &br

	return tr, stop
}

// Load returns modules and an in-memory store for running tests.
func Load(args []string, filter loader.Filter) (map[string]*ast.Module, storage.Store, error) {
	return LoadWithRegoVersion(args, filter, ast.DefaultRegoVersion)
}

// LoadWithRegoVersion returns modules and an in-memory store for running tests.
// Modules are parsed in accordance with the given RegoVersion.
func LoadWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*ast.Module, storage.Store, error) {
	if regoVersion == ast.RegoUndefined {
		regoVersion = ast.DefaultRegoVersion
	}

	loaded, err := loader.NewFileLoader().
		WithRegoVersion(regoVersion).
		WithBundleLazyLoadingMode(bundle.HasExtension()).
		WithProcessAnnotation(true).
		Filtered(args, filter)
	if err != nil {
		return nil, nil, err
	}

	var store storage.Store
	ctx := context.Background()

	if bundle.BundleExtStore != nil {
		store = bundle.BundleExtStore()
		// inline'd NewFromObject
		if err := storage.WriteOne(ctx, store, storage.AddOp, storage.RootPath, loaded.Documents); err != nil {
			return nil, nil, err
		}
	} else {
		store = inmem.NewFromObject(loaded.Documents)
	}

	modules := make(map[string]*ast.Module, len(loaded.Modules))

	err = storage.Txn(ctx, store, storage.WriteParams, func(txn storage.Transaction) error {
		for _, loadedModule := range loaded.Modules {
			modules[loadedModule.Name] = loadedModule.Parsed

			// Add the policies to the store to ensure that any future bundle
			// activations will preserve them and re-compile the module with
			// the bundle modules.
			err := store.UpsertPolicy(ctx, txn, loadedModule.Name, loadedModule.Raw)
			if err != nil {
				return err
			}
		}
		return nil
	})
	return modules, store, err
}

// LoadWithParserOptions returns modules and an in-memory store for running tests.
// Modules are parsed in accordance with the given [ast.ParserOptions].
func LoadWithParserOptions(args []string, filter loader.Filter, popts ast.ParserOptions) (map[string]*ast.Module, storage.Store, error) {
	loaded, err := loader.NewFileLoader().
		WithRegoVersion(popts.RegoVersion).
		WithCapabilities(popts.Capabilities).
		WithProcessAnnotation(popts.ProcessAnnotation).
		WithBundleLazyLoadingMode(bundle.HasExtension()).
		Filtered(args, filter)
	if err != nil {
		return nil, nil, err
	}

	var store storage.Store
	ctx := context.Background()

	// Plumb in storage for external bundle activation plugin, if registered with bundle.RegisterStore.
	if bundle.BundleExtStore != nil {
		store = bundle.BundleExtStore()
		// inline'd NewFromObject
		if err := storage.WriteOne(ctx, store, storage.AddOp, storage.RootPath, loaded.Documents); err != nil {
			return nil, nil, err
		}
	} else {
		store = inmem.NewFromObject(loaded.Documents)
	}

	modules := make(map[string]*ast.Module, len(loaded.Modules))
	err = storage.Txn(ctx, store, storage.WriteParams, func(txn storage.Transaction) error {
		for _, loadedModule := range loaded.Modules {
			modules[loadedModule.Name] = loadedModule.Parsed

			// Add the policies to the store to ensure that any future bundle
			// activations will preserve them and re-compile the module with
			// the bundle modules.
			if err := store.UpsertPolicy(ctx, txn, loadedModule.Name, loadedModule.Raw); err != nil {
				return err
			}
		}
		return nil
	})
	return modules, store, err
}

// LoadBundles will load the given args as bundles, either tarball or directory is OK.
func LoadBundles(args []string, filter loader.Filter) (map[string]*bundle.Bundle, error) {
	return LoadBundlesWithRegoVersion(args, filter, ast.RegoV0)
}

// LoadBundlesWithRegoVersion will load the given args as bundles, either tarball or directory is OK.
// Bundles are parsed in accordance with the given RegoVersion.
func LoadBundlesWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*bundle.Bundle, error) {
	if regoVersion == ast.RegoUndefined {
		regoVersion = ast.DefaultRegoVersion
	}

	bundles := make(map[string]*bundle.Bundle, len(args))
	for _, bundleDir := range args {
		b, err := loader.NewFileLoader().
			WithRegoVersion(regoVersion).
			WithProcessAnnotation(true).
			WithSkipBundleVerification(true).
			WithBundleLazyLoadingMode(bundle.HasExtension()).
			WithFilter(filter).
			AsBundle(bundleDir)
		if err != nil {
			return nil, fmt.Errorf("unable to load bundle %s: %s", bundleDir, err)
		}
		bundles[bundleDir] = b
	}

	return bundles, nil
}

// LoadBundlesWithParserOptions will load the given args as bundles, either tarball or directory is OK.
// Bundles are parsed in accordance with the given [ast.ParserOptions].
func LoadBundlesWithParserOptions(args []string, filter loader.Filter, popts ast.ParserOptions) (map[string]*bundle.Bundle, storage.Store, error) {
	if popts.RegoVersion == ast.RegoUndefined {
		popts.RegoVersion = ast.DefaultRegoVersion
	}

	bundles := make(map[string]*bundle.Bundle, len(args))
	for _, bundleDir := range args {
		b, err := loader.NewFileLoader().
			WithRegoVersion(popts.RegoVersion).
			WithCapabilities(popts.Capabilities).
			WithProcessAnnotation(popts.ProcessAnnotation).
			WithSkipBundleVerification(true).
			WithBundleLazyLoadingMode(bundle.HasExtension()).
			WithFilter(filter).
			AsBundle(bundleDir)
		if err != nil {
			return nil, nil, fmt.Errorf("unable to load bundle %s: %s", bundleDir, err)
		}
		bundles[bundleDir] = b
	}
	// Plumb in storage for external bundle activation plugin, if registered with bundle.RegisterStore.
	if bundle.BundleExtStore != nil {
		return bundles, bundle.BundleExtStore(), nil
	}

	return bundles, inmem.NewWithOpts(inmem.OptRoundTripOnWrite(false)), nil
}
