package cmd

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"maps"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"syscall"
	"testing"
	"time"

	"github.com/open-policy-agent/opa/internal/file/archive"
	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/bundle"
	"github.com/open-policy-agent/opa/v1/ir"
	"github.com/open-policy-agent/opa/v1/rego"
	"github.com/open-policy-agent/opa/v1/repl"
	"github.com/open-policy-agent/opa/v1/storage/inmem"
	"github.com/open-policy-agent/opa/v1/topdown"
	"github.com/open-policy-agent/opa/v1/util/test"
)

func TestFilterTraceDefault(t *testing.T) {
	p := newTestCommandParams()
	p.verbose = false
	expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Enter data.testing.q
| | | | Enter data.testing.r
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
	verifyFilteredTrace(t, &p, expected)
}

func TestFilterTraceVerbose(t *testing.T) {
	p := newTestCommandParams()
	p.verbose = true
	expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Note "test test"
| | | Enter data.testing.q
| | | | Note "got this far"
| | | | Enter data.testing.r
| | | | | Note "got this far2"
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
	verifyFilteredTrace(t, &p, expected)
}

func TestFilterTraceExplainFails(t *testing.T) {
	p := newTestCommandParams()
	err := p.explain.Set(explainModeFails)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Enter data.testing.q
| | | | Enter data.testing.r
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
	verifyFilteredTrace(t, &p, expected)
}

func TestFilterTraceExplainNotes(t *testing.T) {
	p := newTestCommandParams()
	err := p.explain.Set(explainModeNotes)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Note "test test"
| | | Enter data.testing.q
| | | | Note "got this far"
| | | | Enter data.testing.r
| | | | | Note "got this far2"
`
	verifyFilteredTrace(t, &p, expected)
}

func TestFilterTraceExplainFull(t *testing.T) {
	p := newTestCommandParams()
	err := p.explain.Set(explainModeFull)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	expected := `Enter data.testing.test_p = _
| Eval data.testing.test_p = _
| Index data.testing.test_p (matched 1 rule, early exit)
| Enter data.testing.test_p
| | Eval data.testing.p with data.x as "bar"
| | Index data.testing.p (matched 1 rule, early exit)
| | Enter data.testing.p
| | | Eval data.testing.x
| | | Index data.testing.x (matched 1 rule, early exit)
| | | Enter data.testing.x
| | | | Eval data.testing.y
| | | | Index data.testing.y (matched 1 rule, early exit)
| | | | Enter data.testing.y
| | | | | Eval true
| | | | | Exit data.testing.y early
| | | | Exit data.testing.x early
| | | Eval trace("test test")
| | | Note "test test"
| | | Eval data.testing.q.foo
| | | Index data.testing.q (matched 1 rule)
| | | Enter data.testing.q
| | | | Eval trace("got this far")
| | | | Note "got this far"
| | | | Eval data.testing.r[x]
| | | | Index data.testing.r (matched 1 rule)
| | | | Enter data.testing.r
| | | | | Eval trace("got this far2")
| | | | | Note "got this far2"
| | | | | Eval x = data.x
| | | | | Fail x = data.x
| | | | | Redo trace("got this far2")
| | | | Fail data.testing.r[x]
| | | | Redo trace("got this far")
| | | Fail data.testing.q.foo
| | | Redo trace("test test")
| | | Redo data.testing.x
| | | Redo data.testing.x
| | | | Redo data.testing.y
| | | | | Redo true
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
	verifyFilteredTrace(t, &p, expected)
}

func TestThresholdRange(t *testing.T) {
	thresholds := []float64{-1, 101}
	for _, threshold := range thresholds {
		if isThresholdValid(threshold) {
			t.Fatalf("invalid threshold %2f shoul be reported", threshold)
		}
	}
}

func verifyFilteredTrace(t *testing.T, params *testCommandParams, expected string) {
	filtered := filterTrace(params, failTrace(t))

	var buff bytes.Buffer
	topdown.PrettyTrace(&buff, filtered)
	actual := buff.String()

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

func failTrace(t *testing.T) []*topdown.Event {
	t.Helper()
	mod := `
	package testing
	
	p if {
		x  # Always true
		trace("test test")
		q["foo"]
	}
	
	x if {
		y
	}
	
	y if {
		true
	}
	
	q contains x if {
		some x
		trace("got this far")
		r[x]
		trace("got this far1")
	}
	
	r contains x if {
		trace("got this far2")
		x := data.x
	}
	
	test_p if {
		p with data.x as "bar"
	}
	`

	tracer := topdown.NewBufferTracer()

	_, err := rego.New(
		rego.Module("test.rego", mod),
		rego.Trace(true),
		rego.QueryTracer(tracer),
		rego.Query("data.testing.test_p"),
	).Eval(t.Context())

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

	return *tracer
}

func TestPrettyTraceWithLocalVars(t *testing.T) {
	tests := []struct {
		note        string
		includeVars bool
		files       map[string]string
		expected    string
	}{
		{
			note:        "without vars",
			includeVars: false,
			files: map[string]string{
				"test.rego": `package test

test_p if {
	x := 1
	y := 2
	z := 3
	x == z + y
} 
`,
			},
			expected: `%.*%/test.rego:3:
data.test.test_p: FAIL (%.*%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%.*%)

  query:1 %.*%           Enter data.test.test_p = _  
  query:1 %.*%           | Eval data.test.test_p = _  
  query:1 %.*%           | Index data.test.test_p (matched 1 rule, early exit)  
  %.*%/test.rego:3       | Enter data.test.test_p  
  %.*%/test.rego:4       | | Eval x = 1  
  %.*%/test.rego:5       | | Eval y = 2  
  %.*%/test.rego:6       | | Eval z = 3  
  %.*%/test.rego:7       | | Eval plus(z, y, __local3__)  
  %.*%/test.rego:7       | | Eval x = __local3__  
  %.*%/test.rego:7       | | Fail x = __local3__  
  %.*%/test.rego:7       | | Redo plus(z, y, __local3__)  
  %.*%/test.rego:6       | | Redo z = 3  
  %.*%/test.rego:5       | | Redo y = 2  
  %.*%/test.rego:4       | | Redo x = 1  
  query:1 %.*%           | Fail data.test.test_p = _  

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note:        "with vars",
			includeVars: true,
			files: map[string]string{
				"test.rego": `package test

test_p if {
	x := 1
	y := 2
	z := 3
	x == z + y
}
`,
			},
			expected: `%.*%/test.rego:3:
data.test.test_p: FAIL (%.*%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%.*%)

  query:1 %.*%           Enter data.test.test_p = _                                  {}  
  query:1 %.*%           | Eval data.test.test_p = _                                 {}  
  query:1 %.*%           | Index data.test.test_p (matched 1 rule, early exit)       {}  
  %.*%/test.rego:3       | Enter data.test.test_p                                    {}  
  %.*%/test.rego:4       | | Eval x = 1                                              {}  
  %.*%/test.rego:5       | | Eval y = 2                                              {}  
  %.*%/test.rego:6       | | Eval z = 3                                              {}  
  %.*%/test.rego:7       | | Eval plus(z, y, __local3__)                             {y: 2, z: 3}  
  %.*%/test.rego:7       | | Eval x = __local3__                                     {__local3__: 5, x: 1}  
  %.*%/test.rego:7       | | Fail x = __local3__                                     {__local3__: 5, x: 1}  
  %.*%/test.rego:7       | | Redo plus(z, y, __local3__)                             {__local3__: 5, y: 2, z: 3}  
  %.*%/test.rego:6       | | Redo z = 3                                              {z: 3}  
  %.*%/test.rego:5       | | Redo y = 2                                              {y: 2}  
  %.*%/test.rego:4       | | Redo x = 1                                              {x: 1}  
  query:1   %.*%         | Fail data.test.test_p = _                                 {}  

  %.*%/test.rego:7:
    	x == z + y
    	|    |   |
    	|    |   2
    	|    z + y: 5
    	|    z: 3
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			test.WithTempFS(tc.files, func(root string) {
				buf := new(bytes.Buffer)
				testParams := newTestCommandParams()
				testParams.count = 1
				testParams.output = buf
				testParams.errOutput = io.Discard
				testParams.bundleMode = true
				testParams.verbose = true
				testParams.varValues = tc.includeVars
				_ = testParams.explain.Set(explainModeFull)

				errorCode := opaTest([]string{root}, testParams)
				if errorCode != 2 {
					t.Fatalf("Unexpected error code: %d", errorCode)
				}

				actual := buf.String()
				if !stringsMatch(t, tc.expected, actual) {
					t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", tc.expected, actual)
				}
			})
		})
	}
}

func TestFailVarValues(t *testing.T) {
	tests := []struct {
		note     string
		files    map[string]string
		expected string
	}{
		{
			note: "simple",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	x := 1
	y := 2
	z := 3
	x == y + z
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	x == y + z
    	|    |   |
    	|    |   3
    	|    y + z: 5
    	|    y: 2
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "simple (not)",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	x := 5
	y := 2
	z := 3
	not x == y + z
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	not x == y + z
    	    |    |   |
    	    |    |   3
    	    |    y + z: 5
    	    |    y: 2
    	    5

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	x := 1
	y := [1, 2, 3]
	z := 3
	x == y[2] + z
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	x == y[2] + z
    	|    |      |
    	|    |      3
    	|    y[2] + z: 6
    	|    y[2]: 3
    	|    y: [1, 2, 3]
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array, var key",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	x := 1
	y := [1, 2, 3]
	z := 3
	i := 2
	x == y[i] + z
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	x == y[i] + z
    	|    | |    |
    	|    | |    3
    	|    | 2
    	|    y[i] + z: 6
    	|    y[i]: 3
    	|    y: [1, 2, 3]
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array containing vars",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	x := 1
	y := 2
	z := 3
	[x, y, z] == [4, 5, 6]
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	[x, y, z] == [4, 5, 6]
    	 |  |  |
    	 |  |  3
    	 |  2
    	 1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array containing refs",
			files: map[string]string{
				"/test.rego": `package test

a := 1

b := 2

test_foo if {
	[a, data.test.b, data.c] == [4, 5, 6]
}
`,
				"data.json": `{"c": 3}`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	[a, data.test.b, data.c] == [4, 5, 6]
    	 |  |            |
    	 |  |            3
    	 |  2
    	 1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array containing refs, undefined",
			files: map[string]string{
				"/test.rego": `package test

a := 1

b := data.b

test_foo if {
	[a, b, data.c] == [4, 5, 6]
}
`,
				"data.json": `{"c": 3}`,
			},
			// Note: each dynamic array element is broken out into a separate "co-expression" by the compiler.
			// Since we failed on the 2nd element (b), we don't have value for the 3rd element (data.c).
			expected: `%ROOT%/test.rego:7:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	[a, b, data.c] == [4, 5, 6]
    	 |  |
    	 |  undefined
    	 1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "nested collections containing vars",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	x := 1
	y := 2
	z := 3
	[x, {y, {"a": z}}] == [4, {5, {"a": 6}}]
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	[x, {y, {"a": z}}] == [4, {5, {"a": 6}}]
    	 |   |        |
    	 |   |        3
    	 |   2
    	 1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "single line expression containing tabs",
			files: map[string]string{
				"/test.rego": `package test

	test_foo if {
		x := 1
		y := 2
		z := 3
		x == y +	z
	}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    		x == y +	z
    		|    |  	|
    		|    |  	3
    		|    y +	z: 5
    		|    y: 2
    		1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "single line expression containing tabs #2",
			files: map[string]string{
				"/test.rego": `package test

	test_foo if {
		x := 1
		y := 2
		z := 3
		x	== y +	z
	}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    		x	== y +	z
    		|	   |  	|
    		|	   |  	3
    		|	   y +	z: 5
    		|	   y: 2
    		1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "multi-line expression containing tabs",
			files: map[string]string{
				"/test.rego": `package test

	test_foo if {
		x := 1
		y := 2
		z := 3
		obj := {
			"foo_": 1,
			"bar__": 42,
			"baz": 3,
		}
		obj == {
			"foo_":		x,
			"bar__":	y,
			"baz":		z,
		}
	}
`,
			},
			// We can't deal with tabs in a consistent manner when they occur on multiple lines
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:12:
    		obj == {
    			"foo_":		x,
    			"bar__":	y,
    			"baz":		z,
    		}
    
    Where:
    
    obj: {"bar__": 42, "baz": 3, "foo_": 1}
    x: 1
    y: 2
    z: 3

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rule",
			files: map[string]string{
				"/test.rego": `package test

p contains v if {
	some v in numbers.range(1, 3)
}

test_p if {
	p == {4, 5, 6}
}`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	p == {4, 5, 6}
    	|
    	{1, 2, 3}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rule with ref-head",
			files: map[string]string{
				"/test.rego": `package test

p.q contains v if {
	some v in numbers.range(1, 3)
}

test_p if {
	p.q == {4, 5, 6}
}`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	p.q == {4, 5, 6}
    	|
    	{1, 2, 3}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rule with ref-head, partial ref",
			files: map[string]string{
				"/test.rego": `package test

p.q contains v if {
	some v in numbers.range(1, 3)
}

test_p if {
	p == {
		"q": {4, 5, 6}
	}
}`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	p == {
    		"q": {4, 5, 6}
    	}
    	|
    	{"q": {1, 2, 3}}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rules with ref-head, composite value",
			files: map[string]string{
				"/test.rego": `package test

p.q contains v if {
	some v in numbers.range(1, 3)
}

p.r := "foo"

test_p if {
	p == {
		"q": {4, 5, 6},
		"r": "bar"
	}
}`,
			},
			expected: `%ROOT%/test.rego:9:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:10:
    	p == {
    		"q": {4, 5, 6},
    		"r": "bar"
    	}
    	|
    	{"q": {1, 2, 3}, "r": "foo"}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "refs in different compiled sub-expressions",
			files: map[string]string{
				"/test.rego": `package test

a := 1
b := 2
c := 3

test_p if {
	# This expression is split into multiple final expressions by the compiler, each containing a rule ref
	a == b + c
}
`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:9:
    	a == b + c
    	|    |   |
    	|    |   3
    	|    b + c: 5
    	|    b: 2
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "rule not defined",
			files: map[string]string{
				"/test.rego": `package test

p if {
	input.x == 1
}

test_p if {
	p with input.x as 2
}`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	p with input.x as 2
    	|
    	undefined

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "rule defined (not)",
			files: map[string]string{
				"/test.rego": `package test

p if {
	input.x == 1
}

test_p if {
	not p with input.x as 1
}`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	not p with input.x as 1
    	    |
    	    true

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "data ref",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	y := 1
	data.x == y
}
`,
				"data.json": `{"x": 2}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:5:
    	data.x == y
    	|         |
    	|         1
    	2

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "data + virtual extent ref",
			files: map[string]string{
				"/test.rego": `package test

foo.x := 1

test_foo if {
	y := {"x": 1, "y": 42}
	foo == y
}
`,
				"data.json": `{"test": {"foo": {"y": 2}}}`,
			},
			expected: `%ROOT%/test.rego:5:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	foo == y
    	|      |
    	|      {"x": 1, "y": 42}
    	{"x": 1, "y": 2}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "in (array)",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := ["a", "b", "c"]
	x := "q"
	x in l
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:6:
    	x in l
    	|    |
    	|    ["a", "b", "c"]
    	"q"

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "in (set)",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := {"a", "b", "c"}
	x := "q"
	x in l
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:6:
    	x in l
    	|    |
    	|    {"a", "b", "c"}
    	"q"

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension (array)",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := ["a", "b", "c"]
	[x | x := l[_]] == ["d", "e", "f"]
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:5:
    	[x | x := l[_]] == ["d", "e", "f"]
    	|
    	["a", "b", "c"]

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension (set)",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := ["a"]
	{x | x := l[_]} == {"b"}
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:5:
    	{x | x := l[_]} == {"b"}
    	|
    	{"a"}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension (object)",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := ["a", "b", "c"]
	{k: x | x := l[k]} == {3: "d", 4: "e", 5: "f"}
}
`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:5:
    	{k: x | x := l[k]} == {3: "d", 4: "e", 5: "f"}
    	|
    	{0: "a", 1: "b", 2: "c"}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "every",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := [1, 2, 3]
	every x in l {
		x == 1 
	}
}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:6:
    		x == 1
    		|
    		2

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension inside every",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := [1, 2, 3]
	every x in l {
		[v | v := x] == [42]
	}
}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:6:
    		[v | v := x] == [42]
    		|
    		[1]

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "nested every",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := [[1, 2], [3, 4], [5, 6]]
	every x in l {
		every y in x {
			y < 4
		}
	}
}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    			y < 4
    			|
    			4

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "nested every with comprehension",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	l := [[1, 2], [3, 4], [5, 6]]
	every x in l {
		every y in x {
			[v | v := y] == [42]
		}
	}
}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    			[v | v := y] == [42]
    			|
    			[1]

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "ref equality",
			files: map[string]string{
				"/test.rego": `package test

a := 1
b := 2

test_foo if {
	a == b
}`,
			},
			expected: `%ROOT%/test.rego:6:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	a == b
    	|    |
    	|    2
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "ref equality (data)",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	data.a == data.b
}`,
				"data.json": `{"a": 1, "b": 2}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:4:
    	data.a == data.b
    	|         |
    	|         2
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "object ref selection from local var",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	tc := {"data": false, "expected": {"data": true}}
	tc.data == tc.expected.data
}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:5:
    	tc.data == tc.expected.data
    	|          |
    	|          true
    	tc.data: false
    	tc: {"data": false, "expected": {"data": true}}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "object ref selection alongside function call",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	tc := {"content": "big string value", "inp": {"foo": false}, "outp": {"foo": true}}
	foo(tc.inp) == tc.outp
}

foo(inp) := {"foo": inp.foo}`,
			},
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:5:
    	foo(tc.inp) == tc.outp
    	|   |          |
    	|   |          {"foo": true}
    	|   tc.inp: {"foo": false}
    	|   tc: {"content": "big string value", "inp": {"foo": false}, "outp"...
    	{"foo": false}

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "with, containing local vars",
			files: map[string]string{
				"/test.rego": `package test

p := input.x

test_p if {
	a := 1
	p == 2 with input.x as a
}`,
			},
			expected: `%ROOT%/test.rego:5:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:7:
    	p == 2 with input.x as a
    	|                      |
    	|                      1
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "with, containing ref",
			files: map[string]string{
				"/test.rego": `package test

p := input.x

testInput := {"x": 1}

test_p if {
	p == 2 with input as testInput
}`,
			},
			expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

  %ROOT%/test.rego:8:
    	p == 2 with input as testInput
    	|                    |
    	|                    {"x": 1}
    	1

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "negated rule ref",
			files: map[string]string{
				"/test.rego": `package test

a if {true}

test_foo if {
	not a
}`,
				"data.json": `{"a": true}`,
			},
			expected: `%ROOT%/test.rego:5:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:6:
    	not a
    	    |
    	    true

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "negated data ref",
			files: map[string]string{
				"/test.rego": `package test

test_foo if {
	not data.a
}`,
				"data.json": `{"a": true}`,
			},
			// Because of the negated expr, the compiler will have opted out of rewriting the expression to
			// capture the value of data.a in a local variable, and since data.a isn't in the local bindings
			// or in the virtual cache, we don't know if it's undefined or unknown, and therefore can't report
			// on a value.
			expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

  %ROOT%/test.rego:4:
    	not data.a

--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
	}

	r := regexp.MustCompile(`FAIL \(.*s\)`)
	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			test.WithTempFS(tc.files, func(root string) {
				buf := new(bytes.Buffer)
				testParams := newTestCommandParams()
				testParams.count = 1
				testParams.output = buf
				testParams.errOutput = io.Discard
				testParams.bundleMode = true
				testParams.varValues = true
				_ = testParams.explain.Set(explainModeFull)

				exitCode := opaTest([]string{root}, testParams)
				if exitCode != 2 {
					t.Fatalf("Unexpected error code: %d", exitCode)
				}

				actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
				expected := strings.ReplaceAll(tc.expected, "%ROOT%", root)

				if !stringsMatch(t, expected, actual) {
					t.Fatalf("Expected output to be:\n\n%s\n\ngot:\n\n%s", expected, actual)
				}
			})
		})
	}
}

// Assert that ignore flag is correctly used when the bundle flag is activated
func TestIgnoreFlag(t *testing.T) {
	files := map[string]string{
		"/test.rego": `package test

p := input.foo == 42
test_p if {
	p with input.foo as 42
}`,
		"/broken.rego": "package foo\n bar {",
	}

	var exitCode int
	test.WithTempFS(files, func(root string) {
		testParams := newTestCommandParams()
		testParams.count = 1
		testParams.errOutput = io.Discard
		testParams.bundleMode = false
		testParams.ignore = []string{"broken.rego"}

		exitCode = opaTest([]string{root}, testParams)
	})

	if exitCode > 0 {
		t.Fatalf("unexpected exit code: %d", exitCode)
	}
}

// Assert that ignore flag is correctly used when the bundle flag is activated
func TestIgnoreFlagWithBundleFlag(t *testing.T) {
	files := map[string]string{
		"/test.rego": `package test

p := input.foo == 42
test_p if {
	p with input.foo as 42
}`,
		"/broken.rego": "package foo\n bar {",
	}

	var exitCode int
	test.WithTempFS(files, func(root string) {
		testParams := newTestCommandParams()
		testParams.count = 1
		testParams.errOutput = io.Discard
		testParams.bundleMode = true
		testParams.ignore = []string{"broken.rego"}
		exitCode = opaTest([]string{root}, testParams)
	})

	if exitCode > 0 {
		t.Fatalf("unexpected exit code: %d", exitCode)
	}
}

func testSchemasAnnotation(rego string) (int, []byte) {

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

	var exitCode int
	var buf bytes.Buffer
	test.WithTempFS(files, func(path string) {
		regoFilePath := filepath.Join(path, "test.rego")

		testParams := newTestCommandParams()
		testParams.count = 1
		testParams.errOutput = &buf

		exitCode = opaTest([]string{regoFilePath}, testParams)
	})
	return exitCode, buf.Bytes()
}

// Assert that 'schemas' annotations with schema ref are ignored, but not inlined schemas
func TestSchemasAnnotation(t *testing.T) {
	policyWithSchemaRef := `
package test

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

test_p if {
    p with input.foo as 42
}`

	exitCode, _ := testSchemasAnnotation(policyWithSchemaRef)
	if exitCode > 0 {
		t.Fatalf("unexpected error when schema ref is present")
	}
}
func TestSchemasAnnotationInline(t *testing.T) {
	policyWithInlinedSchema := `
package test

# METADATA
# schemas:
#   - input.foo: {"type": "boolean"}
p if { 
	input.foo == 42 # type mismatch with schema that should NOT be ignored since it is an inlined schema format
}

test_p if {
    p with input.foo as 42
}`

	exitCode, errOutput := testSchemasAnnotation(policyWithInlinedSchema)
	// We expect an error here, as inlined schemas are always used for type checking

	if exitCode == 0 {
		t.Fatalf("didn't get expected error when inlined schema is present")
	}

	if !strings.Contains(string(errOutput), "rego_type_error: match error") {
		t.Fatalf("didn't get expected %s error when inlined schema is present; got: %v", ast.TypeErr, string(errOutput))
	}
}

func testSchemasAnnotationWithJSONFile(rego string, schema string) (int, []byte) {

	files := map[string]string{
		"test.rego":        rego,
		"demo_schema.json": schema,
	}

	var exitCode int
	var buf bytes.Buffer
	test.WithTempFS(files, func(path string) {
		regoFilePath := filepath.Join(path, "test.rego")

		testParams := newTestCommandParams()
		testParams.count = 1
		testParams.schema.path = path
		testParams.errOutput = &buf

		exitCode = opaTest([]string{regoFilePath}, testParams)
	})

	return exitCode, buf.Bytes()
}
func TestJSONSchemaSuccess(t *testing.T) {

	regoContents := `package test

# METADATA
# schemas:
#   - input: schema.demo_schema
p if {
	input.foo == 42
}

test_p if {
	p with input.foo as 42
}`

	schema := `{
		"$schema": "http://json-schema.org/draft-07/schema",
		"$id": "schema",
		"type": "object",
		"description": "The root schema comprises the entire JSON document.",
		"required": [
			"foo"
		],
		"properties": {
			"foo": {
				"$id": "#/properties/foo",
				"type": "number",
				"description": "foo"
			}         
		},
		"additionalProperties": false
   }`

	errorCode, _ := testSchemasAnnotationWithJSONFile(regoContents, schema)
	if errorCode != 0 {
		t.Fatalf("unexpected error code: %d", errorCode)
	}
}

func TestJSONSchemaFail(t *testing.T) {

	regoContents := `package test

# METADATA
# schemas:
#   - input: schema.demo_schema
p if {
	input.foo == 42
}

test_p if {
	p with input.foo as 42
}`

	schema := `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "schema",
"type": "object",
"description": "The root schema comprises the entire JSON document.",
"required": [
	"foo"
],
"properties": {
	"foo": {
		"$id": "#/properties/foo",
		"type": "boolean",
		"description": "foo"
	}         
},
"additionalProperties": false
}`

	exitCode, errOutput := testSchemasAnnotationWithJSONFile(regoContents, schema)
	if exitCode == 0 {
		t.Fatalf("didn't get expected error when schema is present and is defining a different type than being used.")
	}

	if !strings.Contains(string(errOutput), "rego_type_error: match error") {
		t.Fatalf("didn't get expected %s error when schema is defining a different type than being used; got: %v", ast.TypeErr, string(errOutput))
	}
}

func TestWatchMode(t *testing.T) {

	files := map[string]string{
		"/policy.rego": `package foo
p := 1`,
		"/policy_test.rego": `package foo

test_p if { 
	p == 1
}`,
	}

	test.WithTempFS(files, func(root string) {
		buf := test.BlockingWriter{}

		testParams := newTestCommandParams()
		testParams.output = &buf
		testParams.watch = true
		testParams.count = 1

		done := make(chan struct{})
		go func() {
			_ = opaTest([]string{root}, testParams)
			<-done
		}()

		expected := "Watching for changes ..."
		if !test.Eventually(t, 2*time.Second, func() bool {
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update the test
		f, _ := os.OpenFile(path.Join(root, "policy_test.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err := f.WriteString("package foo\n test_p if { p == 2 }")
		if err != nil {
			t.Fatal(err)
		}
		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		r := regexp.MustCompile(`FAIL \(.*s\)`)
		expected = `%ROOT%/policy_test.rego:2:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(actual, expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update policy so test passes
		f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err = f.WriteString("package foo\n p := 2")
		if err != nil {
			t.Fatal(err)
		}

		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// add new policy and test
		if err := os.WriteFile(path.Join(root, "policy2.rego"), []byte("package bar\n q := \"hello\""), 0644); err != nil {
			t.Fatal(err)
		}

		if err := os.WriteFile(path.Join(root, "policy2_test.rego"), []byte("package bar\n test_q if { q == \"hello\" }"), 0644); err != nil {
			t.Fatal(err)
		}

		expected = `PASS: 2/2
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		testParams.stopChan <- syscall.SIGINT
		done <- struct{}{}
	})
}

func TestWatchMode_v0(t *testing.T) {

	files := map[string]string{
		"/policy.rego": `package foo
p := 1`,
		"/policy_test.rego": `package foo

test_p { 
	p == 1
}`,
	}

	test.WithTempFS(files, func(root string) {
		buf := test.BlockingWriter{}

		testParams := newTestCommandParams()
		testParams.output = &buf
		testParams.watch = true
		testParams.count = 1
		testParams.v0Compatible = true

		done := make(chan struct{})
		go func() {
			_ = opaTest([]string{root}, testParams)
			<-done
		}()

		expected := "Watching for changes ..."
		if !test.Eventually(t, 2*time.Second, func() bool {
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update the test
		f, _ := os.OpenFile(path.Join(root, "policy_test.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err := f.WriteString("package foo\n test_p { p == 2 }")
		if err != nil {
			t.Fatal(err)
		}
		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		r := regexp.MustCompile(`FAIL \(.*s\)`)
		expected = `%ROOT%/policy_test.rego:2:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(actual, expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update policy so test passes
		f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err = f.WriteString("package foo\n p := 2")
		if err != nil {
			t.Fatal(err)
		}

		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// add new policy and test
		if err := os.WriteFile(path.Join(root, "policy2.rego"), []byte("package bar\n q := \"hello\""), 0644); err != nil {
			t.Fatal(err)
		}

		if err := os.WriteFile(path.Join(root, "policy2_test.rego"), []byte("package bar\n test_q { q == \"hello\" }"), 0644); err != nil {
			t.Fatal(err)
		}

		expected = `PASS: 2/2
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		testParams.stopChan <- syscall.SIGINT
		done <- struct{}{}
	})
}

func TestWatchModeWithDataFile(t *testing.T) {

	files := map[string]string{
		"/policy.rego": `package foo

test_p if { 
	data.y == 1
}`,
		"/data.json": `{"y": 1}`,
	}

	test.WithTempFS(files, func(root string) {
		buf := test.BlockingWriter{}

		testParams := newTestCommandParams()
		testParams.output = &buf
		testParams.watch = true
		testParams.count = 1

		done := make(chan struct{})
		go func() {
			_ = opaTest([]string{root}, testParams)
			<-done
		}()

		expected := "Watching for changes ..."
		if !test.Eventually(t, 2*time.Second, func() bool {
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update the data
		f, _ := os.OpenFile(path.Join(root, "data.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err := f.WriteString(`{"y": 2}`)
		if err != nil {
			t.Fatal(err)
		}
		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		r := regexp.MustCompile(`FAIL \(.*s\)`)
		expected = `%ROOT%/policy.rego:3:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(actual, expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update policy so test passes
		f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err = f.WriteString("package foo\n test_p if { data.y == 2 }")
		if err != nil {
			t.Fatal(err)
		}

		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`

		if !test.Eventually(t, 2*time.Second, func() bool {
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		testParams.stopChan <- syscall.SIGINT
		done <- struct{}{}
	})
}

func TestWatchModeWhenDataFileRemoved(t *testing.T) {
	files := map[string]string{
		"/policy.rego": `package foo

test_p if { 
	data.y == 1 
}`,
		"/data.json": `{"y": 1}`,
	}

	test.WithTempFS(files, func(root string) {
		buf := test.BlockingWriter{}

		testParams := newTestCommandParams()
		testParams.output = &buf
		testParams.watch = true
		testParams.count = 1

		done := make(chan struct{})
		go func() {
			_ = opaTest([]string{root}, testParams)
			<-done
		}()

		expected := "Watching for changes ..."
		if !test.Eventually(t, 2*time.Second, func() bool {
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update the data
		f, _ := os.OpenFile(path.Join(root, "data.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err := f.WriteString(`{"y": 2}`)
		if err != nil {
			t.Fatal(err)
		}
		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		r := regexp.MustCompile(`FAIL \(.*s\)`)
		expected = `%ROOT%/policy.rego:3:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
		if !test.Eventually(t, 2*time.Second, func() bool {
			actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(actual, expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// update the data back to the original state, so the opa test passes
		f, _ = os.OpenFile(path.Join(root, "data.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
		_, err = f.WriteString(`{"y": 1}`)
		if err != nil {
			t.Fatal(err)
		}
		err = f.Close()
		if err != nil {
			t.Fatal(err)
		}

		expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`

		if !test.Eventually(t, 2*time.Second, func() bool {
			expected := strings.ReplaceAll(expected, "%ROOT%", root)
			return strings.Contains(buf.String(), expected)
		}) {
			t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
		}
		buf.Reset()

		// remove the data file, check that test fails afterward
		err = os.Remove(path.Join(root, "data.json"))
		if err != nil {
			t.Fatal(err)
		}

		time.Sleep(500 * time.Millisecond)

		testParams.stopChan <- syscall.SIGINT
		done <- struct{}{}
	})
}

func TestWatchModeBrokenFileRecovery(t *testing.T) {

	tests := []struct {
		note           string
		fileName       string
		brokenFile     string
		fixedFile      string
		expectedOutput string
	}{
		{
			note:      "empty data file (EOF read by watcher)",
			fileName:  "data.json",
			fixedFile: `{"foo": "bar"}`,
			expectedOutput: `1 error occurred during loading: %ROOT%/data.json: EOF
********************************************************************************
Watching for changes ...`,
		},
		{
			note:       "broken policy",
			fileName:   "broken_policy.rego",
			brokenFile: "package foo\n bar {",
			fixedFile:  "package foo\n bar if {true}",
			expectedOutput: `1 error occurred during loading: %ROOT%/broken_policy.rego:2: rego_parse_error: unexpected eof token
	 bar {
	     ^
********************************************************************************
Watching for changes ...`,
		},
	}

	files := map[string]string{
		"/policy.rego": `package foo
p := 1`,
		"/policy_test.rego": `package foo

test_p if { 
	p == 1
}`,
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			test.WithTempFS(files, func(root string) {
				buf := test.BlockingWriter{}

				testParams := newTestCommandParams()
				testParams.output = &buf
				testParams.watch = true
				testParams.count = 1
				testParams.errOutput = io.Discard

				done := make(chan struct{})
				go func() {
					_ = opaTest([]string{root}, testParams)
					<-done
				}()

				expected := "Watching for changes ..."
				if !test.Eventually(t, 2*time.Second, func() bool {
					return strings.Contains(buf.String(), expected)
				}) {
					t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
				}
				buf.Reset()

				// create broken (possibly empty) file
				f, _ := os.OpenFile(path.Join(root, tc.fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
				if len(tc.brokenFile) > 0 {
					_, err := f.WriteString(tc.brokenFile)
					if err != nil {
						t.Fatal(err)
					}
				}
				err := f.Close()
				if err != nil {
					t.Fatal(err)
				}

				if !test.Eventually(t, 2*time.Second, func() bool {
					expected := strings.ReplaceAll(tc.expectedOutput, "%ROOT%", root)
					return strings.Contains(buf.String(), expected)
				}) {
					t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", tc.expectedOutput, buf.String())
				}
				buf.Reset()

				// write data to empty file
				f, _ = os.OpenFile(path.Join(root, tc.fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
				_, err = f.WriteString(tc.fixedFile)
				if err != nil {
					t.Fatal(err)
				}
				err = f.Close()
				if err != nil {
					t.Fatal(err)
				}

				expected = "Watching for changes ..."
				if !test.Eventually(t, 2*time.Second, func() bool {
					expected := strings.ReplaceAll(expected, "%ROOT%", root)
					return strings.Contains(buf.String(), expected)
				}) {
					t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
				}
				buf.Reset()

				testParams.stopChan <- syscall.SIGINT
				done <- struct{}{}
			})
		})
	}
}

func testExitCode(rego string, skipExitZero bool) int {
	files := map[string]string{
		"test.rego": rego,
	}

	var exitCode int
	test.WithTempFS(files, func(path string) {
		regoFilePath := filepath.Join(path, "test.rego")

		testParams := newTestCommandParams()
		testParams.count = 1
		testParams.skipExitZero = skipExitZero
		testParams.errOutput = io.Discard
		testParams.output = io.Discard

		exitCode = opaTest([]string{regoFilePath}, testParams)
	})
	return exitCode
}

func testExitCodeWithFailOnEmpty(rego string, failOnEmpty bool) int {
	files := map[string]string{
		"test.rego": rego,
	}

	var exitCode int
	test.WithTempFS(files, func(path string) {
		regoFilePath := filepath.Join(path, "test.rego")

		testParams := newTestCommandParams()
		testParams.count = 1
		testParams.failOnEmpty = failOnEmpty
		testParams.errOutput = io.Discard
		testParams.output = io.Discard

		exitCode = opaTest([]string{regoFilePath}, testParams)
	})
	return exitCode
}

func TestExitCode(t *testing.T) {
	testCases := map[string]struct {
		Test              string
		ExitZeroOnSkipped bool
		ExpectedExitCode  int
	}{
		"pass when no failed or skipped tests": {
			Test: `package foo
			
			test_pass if { true }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  0,
		},
		"fail when failed tests": {
			Test: `package foo
			
			test_pass if { true }
			test_fail if { false }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  2,
		},
		"fail when skipped tests": {
			Test: `package foo
			
			test_pass if { true }
			todo_test_skip if { true }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  2,
		},
		"fail when failed tests and skipped tests": {
			Test: `package foo
			
			test_pass if { true }
			test_fail if { false }
			todo_test_skip if { true }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  2,
		},
		"pass when skipped tests and exit zero on skipped": {
			Test: `package foo
			
			test_pass if { true }
			todo_test_skip if { true }
			`,
			ExitZeroOnSkipped: true,
			ExpectedExitCode:  0,
		},
		"fail when failed tests and exit zero on skipped": {
			Test: `package foo
			
			test_pass if { true }
			test_fail if { false }
			`,
			ExitZeroOnSkipped: true,
			ExpectedExitCode:  2,
		},
		"fail when failed tests, skipped tests and exit zero on skipped": {
			Test: `package foo
			
			test_pass if { true }
			test_fail if { false }
			todo_test_skip if { true }
			`,
			ExitZeroOnSkipped: true,
			ExpectedExitCode:  2,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			exitCode := testExitCode(tc.Test, tc.ExitZeroOnSkipped)

			if exitCode != tc.ExpectedExitCode {
				t.Errorf("Expected exit code to be %d but got %d", tc.ExpectedExitCode, exitCode)
			}
		})
	}
}

func TestCoverageThreshold(t *testing.T) {
	testCases := []struct {
		note              string
		modules           map[string]string
		threshold         float64
		verbose           bool
		expectedErrOutput string
		expectedExitCode  int
	}{
		{
			note: "coverage threshold met",
			modules: map[string]string{
				"test.rego": `package test

					p := 1
					test_p if { p == 1 }`,
			},
			expectedExitCode: 0,
		},
		{
			note: "coverage threshold not met",
			modules: map[string]string{
				"test.rego": `package test

					p := 1 if {
						1 == 1
					}
					q := 2
					r := 3
					test_q if { q == 2 }`,
			},
			threshold:         100,
			expectedExitCode:  2,
			expectedErrOutput: "Code coverage threshold not met: got 40.00 instead of 100.00\n",
		},
		{
			note: "coverage threshold not met (verbose)",
			modules: map[string]string{
				"test.rego": `package test

					p := 1 if {
						1 == 1
					}
					q := 2
					r := 3
					test_q if { q == 2 }`,
			},
			threshold:        100,
			expectedExitCode: 2,
			verbose:          true,
			expectedErrOutput: `Code coverage threshold not met: got 40.00 instead of 100.00
Lines not covered:
	%ROOT%/test.rego:3-4
	%ROOT%/test.rego:7
`,
		},
		{
			note: "coverage threshold not met (verbose, multiple files)",
			modules: map[string]string{
				"policy1.rego": `package test
					
					p := 1 if {
						1 == 1
					}
					q := 2
					r := 3`,
				"policy2.rego": `package test
					
					s := 4 if {
						1 == 1
						2 == 2
					}
					t := 5
					u := 6
					v := 7`,
				"test.rego": `package test
					
					test_q if { q == 2 }
					test_t if { t == 5 }`,
			},
			threshold:        100,
			expectedExitCode: 2,
			verbose:          true,
			expectedErrOutput: `Code coverage threshold not met: got 33.33 instead of 100.00
Lines not covered:
	%ROOT%/policy1.rego:3-4
	%ROOT%/policy1.rego:7
	%ROOT%/policy2.rego:3-5
	%ROOT%/policy2.rego:8-9
`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.note, func(t *testing.T) {
			test.WithTempFS(tc.modules, func(root string) {
				var buf bytes.Buffer

				testParams := newTestCommandParams()
				testParams.threshold = tc.threshold
				testParams.verbose = tc.verbose
				testParams.count = 1
				testParams.errOutput = &buf

				exitCode := opaTest([]string{root}, testParams)
				if exitCode != tc.expectedExitCode {
					t.Fatalf("unexpected exit code: %d", exitCode)
				}

				if len(tc.expectedErrOutput) == 0 && buf.Len() > 0 {
					t.Fatalf("expected no error output but got:\n\n%q", buf.String())
				}

				expectedErrOutput := strings.ReplaceAll(tc.expectedErrOutput, "%ROOT%", root)
				if buf.String() != expectedErrOutput {
					t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expectedErrOutput, buf.String())
				}
			})
		})
	}
}

func TestCoverageThresholdWithBundles(t *testing.T) {
	modules := map[string]string{
		"authz/policy.rego": `package authz

import rego.v1

allow if {
	input.path == ["users"]
	input.method == "POST"
}

allow if {
	some profile_id
	input.path = ["users", profile_id]
	input.method == "GET"
	profile_id == input.user_id
}
`,
		"authz/policy_test.rego": `package authz

import rego.v1

test_post_allowed if {
	allow with input as {"path": ["users"], "method": "POST"}
}

test_get_anonymous_denied if {
	not allow with input as {"path": ["users"], "method": "GET"}
}
test_get_another_user_denied if {
	not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"}
}
`,
	}

	for _, loadType := range []loadType{loadBundle, loadTarball} {
		t.Run(loadType.String(), func(t *testing.T) {
			var files map[string]string
			if loadType != loadTarball {
				files = modules
			}
			test.WithTempFS(files, func(root string) {
				if loadType == loadTarball {
					f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
					if err != nil {
						t.Fatal(err)
					}

					testBundle := bundle.Bundle{
						Data: map[string]any{},
					}
					for k, v := range modules {
						testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
							Path: k,
							Raw:  []byte(v),
						})
					}

					if err := bundle.Write(f, testBundle); err != nil {
						t.Fatal(err)
					}
				}

				var buf bytes.Buffer

				testParams := newTestCommandParams()
				testParams.threshold = 100
				testParams.verbose = true
				testParams.count = 1
				testParams.bundleMode = true
				testParams.errOutput = &buf
				testParams.coverage = true

				var paths []string
				if loadType == loadTarball {
					paths = []string{filepath.Join(root, "bundle.tar.gz")}
				} else {
					paths = []string{root}
				}

				exitCode := opaTest(paths, testParams)
				// Coverage should NOT be 100% since the second allow rule is never exercised
				if exitCode != 2 {
					t.Fatalf("expected exit code 2 (threshold not met), got %d\noutput: %s", exitCode, buf.String())
				}

				if !strings.Contains(buf.String(), "Code coverage threshold not met: got 92.31 instead of 100.00") {
					t.Fatalf("expected coverage of 92.31, got:\n\n%q", buf.String())
				}
			})
		})
	}
}

type loadType int

const (
	loadFile loadType = iota
	loadBundle
	loadTarball
)

func (t loadType) String() string {
	return [...]string{"file", "bundle", "bundle tarball"}[t]
}

func TestRun_DefaultRegoVersion(t *testing.T) {
	tests := []struct {
		note    string
		files   map[string]string
		expErrs []string
	}{
		{
			note: "v0 module",
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
			expErrs: []string{
				"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
				"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
			},
		},
		{
			note: "v1 module",
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
	}

	loadTypes := []loadType{loadFile, loadBundle, loadTarball}

	for _, tc := range tests {
		for _, loadType := range loadTypes {
			t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) {
				var files map[string]string
				if loadType != loadTarball {
					files = tc.files
				}
				test.WithTempFS(files, func(root string) {
					if loadType == loadTarball {
						f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
						if err != nil {
							t.Fatal(err)
						}

						testBundle := bundle.Bundle{
							Data: map[string]any{},
						}
						for k, v := range tc.files {
							testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
								Path: k,
								Raw:  []byte(v),
							})
						}

						if err := bundle.Write(f, testBundle); err != nil {
							t.Fatal(err)
						}
					}

					var buf bytes.Buffer
					var errBuf bytes.Buffer

					testParams := newTestCommandParams()
					testParams.bundleMode = loadType == loadBundle
					testParams.count = 1
					testParams.output = &buf
					testParams.errOutput = &errBuf

					var paths []string
					if loadType == loadTarball {
						paths = []string{filepath.Join(root, "bundle.tar.gz")}
					} else {
						paths = []string{root}
					}

					exitCode := opaTest(paths, testParams)
					if len(tc.expErrs) > 0 {
						if exitCode == 0 {
							t.Fatalf("expected non-zero exit code")
						}

						for _, expErr := range tc.expErrs {
							if actual := errBuf.String(); !strings.Contains(actual, expErr) {
								t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expErr, actual)
							}
						}
					} else {
						if exitCode != 0 {
							t.Fatalf("unexpected exit code: %d", exitCode)
						}

						if errBuf.Len() > 0 {
							t.Fatalf("expected no error output but got:\n\n%q", buf.String())
						}

						expected := "PASS: 1/1"
						if actual := buf.String(); !strings.Contains(actual, expected) {
							t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
						}
					}
				})
			})
		}
	}
}

func TestRunWithRegoV1Capability(t *testing.T) {
	tests := []struct {
		note         string
		v0Compatible bool
		capabilities *ast.Capabilities
		files        map[string]string
		expErrs      []string
	}{
		{
			note:         "v0 module, v0-compatible, no capabilities",
			v0Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
		},
		{
			note:         "v0 module, v0-compatible, v0 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
		},
		{
			note:         "v0 module, v0-compatible, v1 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
		},

		{
			note: "v0 module, not v0-compatible, no capabilities",
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
			expErrs: []string{
				"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
				"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
			},
		},
		{
			note:         "v0 module, not v0-compatible, v0 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
			expErrs: []string{
				"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
				"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
			},
		},
		{
			note:         "v0 module, not v0-compatible, v0 capabilities without rego_v1 feature",
			capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
			expErrs: []string{
				"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
			},
		},
		{
			note:         "v0 module, not v0-compatible, v1 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}

test_l {
	l1 == l2
}`,
			},
			expErrs: []string{
				"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
				"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
				"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
			},
		},

		{
			note:         "v1 module, v0-compatible, no capabilities",
			v0Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
			expErrs: []string{
				"test.rego:4: rego_parse_error: var cannot be used for rule name",
			},
		},
		{
			note:         "v1 module, v0-compatible, v0 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
			expErrs: []string{
				"test.rego:4: rego_parse_error: var cannot be used for rule name",
			},
		},
		{
			note:         "v1 module, v0-compatible, v1 capabilities",
			v0Compatible: true,
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
			expErrs: []string{
				"test.rego:4: rego_parse_error: var cannot be used for rule name",
			},
		},

		{
			note: "v1 module, not v0-compatible, no capabilities",
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note:         "v1 module, not v0-compatible, v0 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note:         "v1 module, not v0-compatible, v0 capabilities without rego_v1 feature",
			capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
			expErrs: []string{
				"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
			},
		},
		{
			note:         "v1 module, not v0-compatible, v1 capabilities",
			capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
	}

	loadTypes := []loadType{loadFile, loadBundle, loadTarball}

	for _, tc := range tests {
		for _, loadType := range loadTypes {
			t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) {
				var files map[string]string
				if loadType != loadTarball {
					files = tc.files
				}
				test.WithTempFS(files, func(root string) {
					if loadType == loadTarball {
						f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
						if err != nil {
							t.Fatal(err)
						}

						testBundle := bundle.Bundle{
							Data: map[string]any{},
						}
						for k, v := range tc.files {
							testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
								Path: k,
								Raw:  []byte(v),
							})
						}

						if err := bundle.Write(f, testBundle); err != nil {
							t.Fatal(err)
						}
					}

					var buf bytes.Buffer
					var errBuf bytes.Buffer

					testParams := newTestCommandParams()
					testParams.bundleMode = loadType == loadBundle
					testParams.count = 1
					testParams.output = &buf
					testParams.errOutput = &errBuf
					testParams.v0Compatible = tc.v0Compatible
					testParams.capabilities.C = tc.capabilities

					var paths []string
					if loadType == loadTarball {
						paths = []string{filepath.Join(root, "bundle.tar.gz")}
					} else {
						paths = []string{root}
					}

					exitCode := opaTest(paths, testParams)
					if len(tc.expErrs) > 0 {
						if exitCode == 0 {
							t.Fatalf("expected non-zero exit code")
						}

						for _, expErr := range tc.expErrs {
							if actual := errBuf.String(); !strings.Contains(actual, expErr) {
								t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expErr, actual)
							}
						}
					} else {
						if exitCode != 0 {
							t.Fatalf("unexpected exit code: %d", exitCode)
						}

						if errBuf.Len() > 0 {
							t.Fatalf("expected no error output but got:\n\n%q", buf.String())
						}

						expected := "PASS: 1/1"
						if actual := buf.String(); !strings.Contains(actual, expected) {
							t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
						}
					}
				})
			})
		}
	}
}

func TestRun_CompatibleFlags(t *testing.T) {
	tests := []struct {
		note         string
		v0Compatible bool
		v1Compatible bool
		files        map[string]string
		expErr       string
	}{
		{
			note:         "v0 module, no imports",
			v0Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
			expErr: "rego_parse_error",
		},
		{
			note:         "v0 module, rego.v1 imported",
			v0Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

import rego.v1

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note:         "v0 module, future.keywords imported",
			v0Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

import future.keywords

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},

		{
			note:         "v1 compatible module, no imports",
			v1Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note:         "v1 compatible module, rego.v1 imported",
			v1Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

import rego.v1

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note:         "v1 compatible module, future.keywords imported",
			v1Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

import future.keywords

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},

		// v0 takes precedence over v1
		{
			note:         "v0+v1 module, no imports",
			v0Compatible: true,
			v1Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
			expErr: "rego_parse_error",
		},
		{
			note:         "v0+v1 module, rego.v1 imported",
			v0Compatible: true,
			v1Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

import rego.v1

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note:         "v0+v1 module, future.keywords imported",
			v0Compatible: true,
			v1Compatible: true,
			files: map[string]string{
				"/test.rego": `package test

import future.keywords

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
	}

	loadTypes := []loadType{loadFile, loadBundle, loadTarball}

	for _, tc := range tests {
		for _, loadType := range loadTypes {
			t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) {
				var files map[string]string
				if loadType != loadTarball {
					files = tc.files
				}
				test.WithTempFS(files, func(root string) {
					if loadType == loadTarball {
						f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
						if err != nil {
							t.Fatal(err)
						}

						testBundle := bundle.Bundle{
							Data: map[string]any{},
						}
						for k, v := range tc.files {
							testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
								Path: k,
								Raw:  []byte(v),
							})
						}

						if err := bundle.Write(f, testBundle); err != nil {
							t.Fatal(err)
						}
					}

					var buf bytes.Buffer
					var errBuf bytes.Buffer

					testParams := newTestCommandParams()
					testParams.v0Compatible = tc.v0Compatible
					testParams.v1Compatible = tc.v1Compatible
					testParams.bundleMode = loadType == loadBundle
					testParams.count = 1
					testParams.output = &buf
					testParams.errOutput = &errBuf

					var paths []string
					if loadType == loadTarball {
						paths = []string{filepath.Join(root, "bundle.tar.gz")}
					} else {
						paths = []string{root}
					}

					exitCode := opaTest(paths, testParams)
					if tc.expErr != "" {
						if exitCode == 0 {
							t.Fatalf("expected non-zero exit code")
						}

						if actual := errBuf.String(); !strings.Contains(actual, tc.expErr) {
							t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", tc.expErr, actual)
						}
					} else {
						if exitCode != 0 {
							t.Fatalf("unexpected exit code: %d", exitCode)
						}

						if errBuf.Len() > 0 {
							t.Fatalf("expected no error output but got:\n\n%q", buf.String())
						}

						expected := "PASS: 1/1"
						if actual := buf.String(); !strings.Contains(actual, expected) {
							t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
						}
					}
				})
			})
		}
	}
}

func TestWithBundleRegoVersion(t *testing.T) {
	tests := []struct {
		note   string
		files  map[string]string
		expErr string
	}{
		{
			note: "v0.x bundle, no imports",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
			expErr: "rego_parse_error",
		},
		{
			note: "v0.x bundle, rego.v1 imported",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test

import rego.v1

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v0.x bundle, future.keywords imported",
			files: map[string]string{
				".manifest": `{"rego_version": 0}`,
				"policy.rego": `package test

import future.keywords

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v0 bundle, v1 per-file override",
			files: map[string]string{
				".manifest": `{
	"rego_version": 0,
	"file_rego_versions": {
		"/policy2.rego": 1
	}
}`,
				"policy1.rego": `package test
l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}`,
				"policy2.rego": `package test
test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v0 bundle, v1 per-file override (glob)",
			files: map[string]string{
				".manifest": `{
	"rego_version": 0,
	"file_rego_versions": {
		"*/policy2.rego": 1
	}
}`,
				"policy1.rego": `package test
l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}`,
				"policy2.rego": `package test
test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v0 bundle, v1 per-file override, incompatible",
			files: map[string]string{
				".manifest": `{
	"rego_version": 0,
	"file_rego_versions": {
		"/policy2.rego": 1
	}
}`,
				"policy1.rego": `package test
l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}`,
				"policy2.rego": `package test
test_l {
	l1 == l2
}`,
			},
			expErr: "rego_parse_error",
		},

		{
			note: "v1.0 bundle, no imports",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v1.0 bundle, rego.v1 imported",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test

import rego.v1

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v1.0 bundle, future.keywords imported",
			files: map[string]string{
				".manifest": `{"rego_version": 1}`,
				"policy.rego": `package test

import future.keywords

l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}

test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v1 bundle, v0 per-file override",
			files: map[string]string{
				".manifest": `{
	"rego_version": 1,
	"file_rego_versions": {
		"/policy1.rego": 0
	}
}`,
				"policy1.rego": `package test
l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}`,
				"policy2.rego": `package test
test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v1 bundle, v0 per-file override (glob)",
			files: map[string]string{
				".manifest": `{
	"rego_version": 1,
	"file_rego_versions": {
		"*/policy1.rego": 0
	}
}`,
				"policy1.rego": `package test
l1 := {1, 3, 5}
l2[v] {
	v := l1[_]
}`,
				"policy2.rego": `package test
test_l if {
	l1 == l2
}`,
			},
		},
		{
			note: "v1 bundle, v0 per-file override, incompatible",
			files: map[string]string{
				".manifest": `{
	"rego_version": 1,
	"file_rego_versions": {
		"/policy1.rego": 0
	}
}`,
				"policy1.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}`,
				"policy2.rego": `package test
test_l if {
	l1 == l2
}`,
			},
			expErr: "rego_parse_error",
		},
	}

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

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

	for _, bundleType := range bundleTypeCases {
		for _, v1CompatibleFlag := range v1CompatibleFlagCases {
			for _, tc := range tests {

				t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, v1CompatibleFlag.note, tc.note), func(t *testing.T) {
					files := map[string]string{}
					if bundleType.tar {
						files["bundle.tar.gz"] = ""
					} else {
						maps.Copy(files, tc.files)
					}

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

						var buf bytes.Buffer
						var errBuf bytes.Buffer

						testParams := newTestCommandParams()
						testParams.v1Compatible = v1CompatibleFlag.used
						testParams.bundleMode = true
						testParams.count = 1
						testParams.output = &buf
						testParams.errOutput = &errBuf

						exitCode := opaTest([]string{p}, testParams)
						if tc.expErr != "" {
							if exitCode == 0 {
								t.Fatalf("expected non-zero exit code")
							}

							if actual := errBuf.String(); !strings.Contains(actual, tc.expErr) {
								t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", tc.expErr, actual)
							}
						} else {
							if exitCode != 0 {
								t.Fatalf("unexpected exit code: %d", exitCode)
							}

							if errBuf.Len() > 0 {
								t.Fatalf("expected no error output but got:\n\n%q", buf.String())
							}

							expected := "PASS: 1/1"
							if actual := buf.String(); !strings.Contains(actual, expected) {
								t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
							}
						}
					})
				})
			}
		}
	}
}

// Assert that a failing test doesn't cause a panic.
// https://github.com/open-policy-agent/opa/issues/7205
func TestTestBenchFailingTest(t *testing.T) {
	files := map[string]string{
		"test.rego": `package test
			test_fail if false`,
	}

	test.WithTempFS(files, func(path string) {
		fp := filepath.Join(path, "test.rego")
		tp := newTestCommandParams()
		tp.benchmark = true
		tp.count = 1

		exitCode := opaTest([]string{fp}, tp)
		if exitCode == 0 {
			t.Fatalf("Expected exit code != 0, got %d", exitCode)
		}
	})
}

func TestTestRunParallel(t *testing.T) {
	tests := []struct {
		note     string
		parallel int
	}{
		{
			note:     "default workers",
			parallel: 0,
		},
		{
			note:     "1 workers",
			parallel: 1,
		},
		{
			note:     "2 workers",
			parallel: 2,
		},
		{
			note:     "100 workers",
			parallel: 100,
		},
	}

	for _, tc := range tests {
		testParams := newTestCommandParams()
		testParams.parallel = tc.parallel

		files := map[string]string{
			"policy1.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
	v := l1[_]
}`,
			"policy2.rego": `package test
test_l if {
	l1 == l2
}`}

		var exitCode int
		test.WithTempFS(files, func(root string) {
			exitCode = opaTest([]string{root}, testParams)
		})

		if exitCode > 0 {
			t.Fatalf("unexpected exit code: %d", exitCode)
		}
	}
}

func TestWithDefaultRegoPlugin(t *testing.T) {
	// We're injecting a default rego plugin that always returns true.
	// If it's picked as a default (as intended), the tests run here will also
	// yield true. If it wasn't picked, we'd use topdown, and would see a failing
	// test.
	tp := &testPlugin{}
	rego.RegisterPlugin(targetPlugin, tp)
	t.Cleanup(func() { tp.target = targetPlugin })

	t.Run("test", func(t *testing.T) {
		test.WithTempFS(map[string]string{"test.rego": "package test\ntest_true if false"}, func(path string) {
			fp := filepath.Join(path, "test.rego")
			tp := newTestCommandParams()
			tp.output = io.Discard
			tp.count = 1

			exitCode := opaTest([]string{fp}, tp)
			if exitCode != 0 {
				t.Fatalf("Expected exit code 0, got %d", exitCode)
			}
		})
	})
	t.Run("eval", func(t *testing.T) {
		params := newEvalCommandParams()
		params.fail = true
		query := "2+2 = 5" // unification will fail ("2+2 == 5" would be false, but defined)

		defined, err := eval([]string{query}, params, io.Discard, nil)
		if err != nil {
			t.Fatal("unexpected error", err)
		}
		if !defined {
			t.Errorf("expected defined result")
		}
	})

	t.Run("repl", func(t *testing.T) {
		ctx := t.Context()
		store := inmem.New()
		var buffer bytes.Buffer
		repl := repl.New(store, "", &buffer, "", 0, "")
		if err := repl.OneShot(ctx, "2+2==5"); err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}
		result := buffer.String()
		if result != "true\n" {
			t.Errorf("Expected result to be false but got: %v", result)
		}
	})
}

func TestFailOnEmpty(t *testing.T) {
	testCases := map[string]struct {
		Test             string
		FailOnEmpty      bool
		ExpectedExitCode int
	}{
		"pass when no tests and fail-on-empty disabled": {
			Test: `package foo

			p := 1
			`,
			FailOnEmpty:      false,
			ExpectedExitCode: 0,
		},
		"fail when no tests and fail-on-empty enabled": {
			Test: `package foo

			p := 1
			`,
			FailOnEmpty:      true,
			ExpectedExitCode: 1,
		},
		"pass when tests exist and fail-on-empty enabled": {
			Test: `package foo

			test_pass if { true }
			`,
			FailOnEmpty:      true,
			ExpectedExitCode: 0,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			exitCode := testExitCodeWithFailOnEmpty(tc.Test, tc.FailOnEmpty)

			if exitCode != tc.ExpectedExitCode {
				t.Errorf("Expected exit code to be %d but got %d", tc.ExpectedExitCode, exitCode)
			}
		})
	}
}

type testPlugin struct {
	target string
}

func (t *testPlugin) IsTarget(tgt string) bool {
	return tgt == t.target // t == "" makes it the global default
}

func (*testPlugin) PrepareForEval(context.Context, *ir.Policy, ...rego.PrepareOption) (rego.TargetPluginEval, error) {
	return &testPlugin{}, nil
}

func (*testPlugin) Eval(context.Context, *rego.EvalContext, ast.Value) (ast.Value, error) {
	return ast.NewSet(ast.NewTerm(ast.NewObject([2]*ast.Term{ast.StringTerm("^term1"), ast.BooleanTerm(true)}))), nil
}

const targetPlugin = "rego_test_default_plugin"
