package cmd

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

	"github.com/open-policy-agent/opa/ast"
	"github.com/open-policy-agent/opa/bundle"
	"github.com/open-policy-agent/opa/internal/file/archive"
	"github.com/open-policy-agent/opa/rego"
	"github.com/open-policy-agent/opa/topdown"
	"github.com/open-policy-agent/opa/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 {
		x  # Always true
		trace("test test")
		q["foo"]
	}
	
	x {
		y
	}
	
	y {
		true
	}
	
	q[x] {
		some x
		trace("got this far")
		r[x]
		trace("got this far1")
	}
	
	r[x] {
		trace("got this far2")
		x := data.x
	}
	
	test_p {
		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(context.Background())

	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
import rego.v1

test_p if {
	x := 1
	y := 2
	z := 3
	x == z + y
} 
`,
			},
			expected: `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:4       | Enter data.test.test_p  
  %.*%/test.rego:5       | | Eval x = 1  
  %.*%/test.rego:6       | | Eval y = 2  
  %.*%/test.rego:7       | | Eval z = 3  
  %.*%/test.rego:8       | | Eval plus(z, y, __local3__)  
  %.*%/test.rego:8       | | Eval x = __local3__  
  %.*%/test.rego:8       | | Fail x = __local3__  
  %.*%/test.rego:8       | | Redo plus(z, y, __local3__)  
  %.*%/test.rego:7       | | Redo z = 3  
  %.*%/test.rego:6       | | Redo y = 2  
  %.*%/test.rego:5       | | Redo x = 1  
  query:1 %.*%           | Fail data.test.test_p = _  

SUMMARY
--------------------------------------------------------------------------------
%.*%/test.rego:
data.test.test_p: FAIL (%.*%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note:        "with vars",
			includeVars: true,
			files: map[string]string{
				"test.rego": `package test
import rego.v1

test_p if {
	x := 1
	y := 2
	z := 3
	x == z + y
} 
`,
			},
			expected: `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:4       | Enter data.test.test_p                                    {}  
  %.*%/test.rego:5       | | Eval x = 1                                              {}  
  %.*%/test.rego:6       | | Eval y = 2                                              {}  
  %.*%/test.rego:7       | | Eval z = 3                                              {}  
  %.*%/test.rego:8       | | Eval plus(z, y, __local3__)                             {y: 2, z: 3}  
  %.*%/test.rego:8       | | Eval x = __local3__                                     {__local3__: 5, x: 1}  
  %.*%/test.rego:8       | | Fail x = __local3__                                     {__local3__: 5, x: 1}  
  %.*%/test.rego:8       | | Redo plus(z, y, __local3__)                             {__local3__: 5, y: 2, z: 3}  
  %.*%/test.rego:7       | | Redo z = 3                                              {z: 3}  
  %.*%/test.rego:6       | | Redo y = 2                                              {y: 2}  
  %.*%/test.rego:5       | | Redo x = 1                                              {x: 1}  
  query:1   %.*%         | Fail data.test.test_p = _                                 {}  

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

SUMMARY
--------------------------------------------------------------------------------
%.*%/test.rego:
data.test.test_p: FAIL (%.*%)
--------------------------------------------------------------------------------
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)

				_, err := opaTest([]string{root}, testParams)
				if err != nil {
					t.Fatalf("Unexpected error: %s", err)
				}

				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
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "simple (not)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array, var key",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array containing vars",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array containing refs",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

a := 1

b := 2

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "array containing refs, undefined",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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: `FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "nested collections containing vars",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "single line expression containing tabs",
			files: map[string]string{
				"/test.rego": `package test
	import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "single line expression containing tabs #2",
			files: map[string]string{
				"/test.rego": `package test
	import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "multi-line expression containing tabs",
			files: map[string]string{
				"/test.rego": `package test
	import rego.v1

	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: `FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rule",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rule with ref-head",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rule with ref-head, partial ref",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "composite rules with ref-head, composite value",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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: `FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "refs in different compiled sub-expressions",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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: `FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "rule not defined",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

p if {
	input.x == 1
}

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "rule defined (not)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

p if {
	input.x == 1
}

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "data ref",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "data + virtual extent ref",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

foo.x := 1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "in (array)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "in (set)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension (array)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension (set)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension (object)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "every",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "comprehension inside every",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "nested every",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "nested every with comprehension",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "ref equality",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

a := 1
b := 2

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "ref equality (data)",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "with, containing local vars",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

p := input.x

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "with, containing ref",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

p := input.x

testInput := {"x": 1}

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "negated rule ref",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

a if {true}

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

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
`,
		},
		{
			note: "negated data ref",
			files: map[string]string{
				"/test.rego": `package test
import rego.v1

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: `FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)

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

SUMMARY
--------------------------------------------------------------------------------
%ROOT%/test.rego:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
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)

				_, err := opaTest([]string{root}, testParams)
				if err != nil {
					t.Fatalf("Unexpected error: %s", err)
				}

				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\n p := input.foo == 42\ntest_p {\n p with input.foo as 42\n}",
		"/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\n p := input.foo == 42\ntest_p {\n p with input.foo as 42\n}",
		"/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, error) {

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

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

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

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

// 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 { 
	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 {
    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 { 
	input.foo == 42 # type mismatch with schema that should NOT be ignored since it is an inlined schema format
}

test_p {
    p with input.foo as 42
}`

	exitCode, err := 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")
	} else if !strings.Contains(err.Error(), "rego_type_error: match error") {
		t.Fatalf("didn't get expected %s error when inlined schema is present; got: %v", ast.TypeErr, err)
	}
}

func testSchemasAnnotationWithJSONFile(rego string, schema string) (int, error) {

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

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

		testParams := newTestCommandParams()
		testParams.count = 1
		testParams.schema.path = path
		testParams.errOutput = io.Discard

		exitCode, err = opaTest([]string{regoFilePath}, testParams)
	})
	return exitCode, err
}
func TestJSONSchemaSuccess(t *testing.T) {

	regoContents := `package test
# METADATA
# schemas:
#   - input: schema.demo_schema
p {
input.foo == 42
}

test_p {
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
   }`

	_, err := testSchemasAnnotationWithJSONFile(regoContents, schema)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
}

func TestJSONSchemaFail(t *testing.T) {

	regoContents := `package test
# METADATA
# schemas:
#   - input: schema.demo_schema
p {
input.foo == 42
}

test_p {
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, err := 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.")
	} else if !strings.Contains(err.Error(), "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, err)
	}
}

func TestWatchMode(t *testing.T) {

	files := map[string]string{
		"/policy.rego":      "package foo\n p := 1",
		"/policy_test.rego": "package foo\n test_p { 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 { p == 2 }")
		if err != nil {
			t.Fatal(err)
		}
		f.Close()

		r := regexp.MustCompile(`FAIL \(.*s\)`)
		expected = `%ROOT%/policy_test.rego:
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)
		}

		f.Close()

		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\n test_p { 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)
		}
		f.Close()

		r := regexp.MustCompile(`FAIL \(.*s\)`)
		expected = `%ROOT%/policy.rego:
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 { data.y == 2 }")
		if err != nil {
			t.Fatal(err)
		}

		f.Close()

		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\n test_p { 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)
		}
		f.Close()

		r := regexp.MustCompile(`FAIL \(.*s\)`)
		expected = `%ROOT%/policy.rego:
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)
		}
		f.Close()

		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 {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\n p := 1",
		"/policy_test.rego": "package foo\n test_p { 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)
					}
				}
				f.Close()

				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)
				}
				f.Close()

				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, error) {
	files := map[string]string{
		"test.rego": rego,
	}

	var exitCode int
	var err error
	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, err = opaTest([]string{regoFilePath}, testParams)
	})
	return exitCode, err
}

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 { true }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  0,
		},
		"fail when failed tests": {
			Test: `package foo
			test_pass { true }
			test_fail { false }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  2,
		},
		"fail when skipped tests": {
			Test: `package foo
			test_pass { true }
			todo_test_skip { true }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  2,
		},
		"fail when failed tests and skipped tests": {
			Test: `package foo
			test_pass { true }
			test_fail { false }
			todo_test_skip { true }
			`,
			ExitZeroOnSkipped: false,
			ExpectedExitCode:  2,
		},
		"pass when skipped tests and exit zero on skipped": {
			Test: `package foo
			test_pass { true }
			todo_test_skip { true }
			`,
			ExitZeroOnSkipped: true,
			ExpectedExitCode:  0,
		},
		"fail when failed tests and exit zero on skipped": {
			Test: `package foo
			test_pass { true }
			test_fail { false }
			`,
			ExitZeroOnSkipped: true,
			ExpectedExitCode:  2,
		},
		"fail when failed tests, skipped tests and exit zero on skipped": {
			Test: `package foo
			test_pass { true }
			test_fail { false }
			todo_test_skip { 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 { p == 1 }`,
			},
			expectedExitCode: 0,
		},
		{
			note: "coverage threshold not met",
			modules: map[string]string{
				"test.rego": `package test
					p := 1 {
						1 == 1
					}
					q := 2
					r := 3
					test_q { 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 {
						1 == 1
					}
					q := 2
					r := 3
					test_q { 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:2-3
	%ROOT%/test.rego:6
`,
		},
		{
			note: "coverage threshold not met (verbose, multiple files)",
			modules: map[string]string{
				"policy1.rego": `package test
					p := 1 {
						1 == 1
					}
					q := 2
					r := 3`,
				"policy2.rego": `package test
					s := 4 {
						1 == 1
						2 == 2
					}
					t := 5
					u := 6
					v := 7`,
				"test.rego": `package test
					test_q { q == 2 }
					test_t { 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:2-3
	%ROOT%/policy1.rego:6
	%ROOT%/policy2.rego:2-4
	%ROOT%/policy2.rego:7-8
`,
		},
	}

	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())
				}
			})
		})
	}
}

type loadType int

const (
	loadFile loadType = iota
	loadBundle
	loadTarball
)

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

func TestWithV1CompatibleFlag(t *testing.T) {
	tests := []struct {
		note         string
		v1Compatible bool
		files        map[string]string
		expErr       string
	}{
		{
			note: "0.x module, no imports",
			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: "0.x module, rego.v1 imported",
			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: "0.x module, future.keywords imported",
			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:         "1.0 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:         "1.0 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:         "1.0 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
}`,
			},
		},
	}

	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]interface{}{},
						}
						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.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 {
						for k, v := range tc.files {
							files[k] = v
						}
					}

					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)
							}
						}
					})
				})
			}
		}
	}
}
