// Copyright 2020 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package topdown

import (
	"context"
	"reflect"
	"strings"
	"testing"

	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/storage"
	inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test"
)

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

	cases := []struct {
		note         string
		tracerConfs  []TraceConfig
		expectLocals bool
	}{
		{
			note: "plug locals single tracer",
			tracerConfs: []TraceConfig{
				{PlugLocalVars: true},
			},
			expectLocals: true,
		},
		{
			note: "dont plug locals single tracer",
			tracerConfs: []TraceConfig{
				{PlugLocalVars: false},
			},
			expectLocals: false,
		},
		{
			note: "plug locals multiple tracers",
			tracerConfs: []TraceConfig{
				{PlugLocalVars: true},
				{PlugLocalVars: true},
				{PlugLocalVars: true},
			},
			expectLocals: true,
		},
		{
			note: "dont plug locals multiple tracers",
			tracerConfs: []TraceConfig{
				{PlugLocalVars: false},
				{PlugLocalVars: false},
				{PlugLocalVars: false},
			},
			expectLocals: false,
		},
		{
			note: "plug locals multiple plugins mixed",
			tracerConfs: []TraceConfig{
				{PlugLocalVars: false},
				{PlugLocalVars: true},
				{PlugLocalVars: false},
			},
			expectLocals: true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			t.Parallel()

			query := initTracerTestQuery()

			var tracers []*testQueryTracer
			for _, conf := range tc.tracerConfs {
				tt := &testQueryTracer{
					events:  []*Event{},
					conf:    conf,
					enabled: true,
					t:       t,
				}
				tracers = append(tracers, tt)

				query = query.WithQueryTracer(tt)
			}

			_, err := query.Run(t.Context())
			if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}

			// Even if the individual tracer didn't specify for local metadata
			// they will _all_ either have it or not.
			for _, tt := range tracers {
				for _, e := range tt.events {
					if !tc.expectLocals && e.LocalMetadata != nil {
						t.Fatal("Expected event LocalMetadata to nil")
					}
					if tc.expectLocals && e.LocalMetadata == nil {
						t.Fatal("Expected event LocalMetadata to be non-nil")
					}
				}
			}
		})
	}
}

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

	query := initTracerTestQuery()

	tracer := &testQueryTracer{
		events:  []*Event{},
		conf:    TraceConfig{PlugLocalVars: false},
		enabled: true,
		t:       t,
	}

	// Call with older API, expect to be "upgraded" to QueryTracer
	// If the deprecated Trace() API is called the test will fail.
	query.WithTracer(tracer)

	_, err := query.Run(t.Context())
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
}

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

	query := initTracerTestQuery()

	// Using a tracer that does _not_ implement the newer
	// QueryTracer interface, only the deprecated Tracer one.
	tracer := &testLegacyTracer{
		events: []*Event{},
	}

	query.WithTracer(tracer)

	// For comparison use a buffer tracer and the new interface
	bt := NewBufferTracer()
	query.WithQueryTracer(bt)

	_, err := query.Run(t.Context())
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if len(*bt) != len(tracer.events) {
		t.Fatalf("Expected %d events on the test tracer, got %d", len(*bt), len(tracer.events))
	}

	if !reflect.DeepEqual([]*Event(*bt), tracer.events) {
		t.Fatal("Expected same events on test tracer and BufferTracer")
	}
}

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

	query := initTracerTestQuery()

	tracer := &testQueryTracer{
		events:  []*Event{},
		conf:    TraceConfig{PlugLocalVars: false},
		enabled: false,
		t:       t,
	}

	// Both API's should ignore the disabled tracer
	query.WithTracer(tracer)
	query.WithQueryTracer(tracer)

	_, err := query.Run(t.Context())
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if len(tracer.events) > 0 {
		t.Fatalf("Expected no events on test tracer, got %d", len(tracer.events))
	}
}

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

	tests := []struct {
		note          string
		query         string
		expectedError string
	}{
		{
			note:          "rego.metadata.chain() call",
			query:         "rego.metadata.chain()",
			expectedError: "rego.metadata.chain(): eval_builtin_error: rego.metadata.chain: the rego.metadata.chain function must only be called within the scope of a rule",
		},
		{
			note:          "rego.metadata.rule() call",
			query:         "rego.metadata.rule()",
			expectedError: "rego.metadata.rule(): eval_builtin_error: rego.metadata.rule: the rego.metadata.rule function must only be called within the scope of a rule",
		},
	}

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

			c := ast.NewCompiler()
			q := NewQuery(ast.MustParseBody(tc.query)).WithCompiler(c).
				WithStrictBuiltinErrors(true)
			_, err := q.Run(t.Context())

			if err == nil {
				t.Fatal("expected error")
			}

			if tc.expectedError != err.Error() {
				t.Fatalf("expected error:\n\n%s\n\ngot:\n\n%s", tc.expectedError, err.Error())
			}
		})
	}
}

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

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

	// Policy with reference to non-existing function
	modules := map[string]*ast.Module{
		"tst": ast.MustParseModule(`package test
p := data.q(42)`),
	}

	c := ast.NewCompiler()
	c.Compile(modules)
	q := NewQuery(ast.MustParseBody("data.a = 1")).
		WithCompiler(c).
		WithStore(store).
		WithTransaction(txn)

	_, err := q.Run(t.Context())
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	expected := "eval_internal_error: compiler has errors"
	if !strings.Contains(err.Error(), expected) {
		t.Fatalf("expected error to contain '%s', got: %v", expected, err)
	}
}

func initTracerTestQuery() *Query {
	ctx := context.Background()
	store := inmem.New()
	inputTerm := &ast.Term{}
	txn := storage.NewTransactionOrDie(ctx, store)
	defer store.Abort(ctx, txn)

	compiler := compileModules([]string{
		`package x

	p if {
		a := [1, 2, 3]
		f(a[_])
	}

	f(x) if {
		x == 3
	}

	`})

	return NewQuery(ast.MustParseBody("data.x.p")).
		WithCompiler(compiler).
		WithStore(store).
		WithTransaction(txn).
		WithInput(inputTerm)
}

type testQueryTracer struct {
	events  []*Event
	conf    TraceConfig
	enabled bool
	t       *testing.T
}

func (n *testQueryTracer) Enabled() bool {
	return n.enabled
}

func (n *testQueryTracer) Trace(e *Event) {
	n.t.Errorf("Unexpected call to Trace() with event %v", e)
}

func (n *testQueryTracer) TraceEvent(e Event) {
	n.events = append(n.events, &e)
}

func (n *testQueryTracer) Config() TraceConfig {
	return n.conf
}

type testLegacyTracer struct {
	events []*Event
}

func (*testLegacyTracer) Enabled() bool {
	return true
}

func (n *testLegacyTracer) Trace(e *Event) {
	n.events = append(n.events, e)
}
