// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package audit

import (
	"context"
	"testing"

	"github.com/hashicorp/eventlogger"
	"github.com/hashicorp/go-uuid"
	nshelper "github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/internal/observability/event"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/stretchr/testify/require"
)

// TestProcessManual_NilData tests processManual when nil data is supplied.
func TestProcessManual_NilData(t *testing.T) {
	t.Parallel()

	var ids []eventlogger.NodeID
	nodes := make(map[eventlogger.NodeID]eventlogger.Node)

	// Formatter node
	formatterId, formatterNode := newFormatterNode(t)
	ids = append(ids, formatterId)
	nodes[formatterId] = formatterNode

	// Sink node
	sinkId, sinkNode := newSinkNode(t)
	ids = append(ids, sinkId)
	nodes[sinkId] = sinkNode

	err := processManual(nshelper.RootContext(context.Background()), nil, ids, nodes)
	require.Error(t, err)
	require.EqualError(t, err, "data cannot be nil")
}

// TestProcessManual_BadIDs tests processManual when different bad values are
// supplied for the ID parameter.
func TestProcessManual_BadIDs(t *testing.T) {
	t.Parallel()

	tests := map[string]struct {
		IDs                  []eventlogger.NodeID
		ExpectedErrorMessage string
	}{
		"nil": {
			IDs:                  nil,
			ExpectedErrorMessage: "minimum of 2 ids are required",
		},
		"one": {
			IDs:                  []eventlogger.NodeID{"1"},
			ExpectedErrorMessage: "minimum of 2 ids are required",
		},
	}

	for name, tc := range tests {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			nodes := make(map[eventlogger.NodeID]eventlogger.Node)

			// Formatter node
			formatterId, formatterNode := newFormatterNode(t)
			nodes[formatterId] = formatterNode

			// Sink node
			sinkId, sinkNode := newSinkNode(t)
			nodes[sinkId] = sinkNode

			// Data
			requestId, err := uuid.GenerateUUID()
			require.NoError(t, err)
			data := newData(requestId)

			err = processManual(nshelper.RootContext(context.Background()), data, tc.IDs, nodes)
			require.Error(t, err)
			require.EqualError(t, err, tc.ExpectedErrorMessage)
		})
	}
}

// TestProcessManual_NoNodes tests processManual when no nodes are supplied.
func TestProcessManual_NoNodes(t *testing.T) {
	t.Parallel()

	var ids []eventlogger.NodeID
	nodes := make(map[eventlogger.NodeID]eventlogger.Node)

	// Formatter node
	formatterId, _ := newFormatterNode(t)
	ids = append(ids, formatterId)

	// Sink node
	sinkId, _ := newSinkNode(t)
	ids = append(ids, sinkId)

	// Data
	requestId, err := uuid.GenerateUUID()
	require.NoError(t, err)
	data := newData(requestId)

	err = processManual(nshelper.RootContext(context.Background()), data, ids, nodes)
	require.Error(t, err)
	require.EqualError(t, err, "nodes are required")
}

// TestProcessManual_IdNodeMismatch tests processManual when IDs don't match with
// the nodes in the supplied map.
func TestProcessManual_IdNodeMismatch(t *testing.T) {
	t.Parallel()

	var ids []eventlogger.NodeID
	nodes := make(map[eventlogger.NodeID]eventlogger.Node)

	// Formatter node
	formatterId, formatterNode := newFormatterNode(t)
	ids = append(ids, formatterId)
	nodes[formatterId] = formatterNode

	// Sink node
	sinkId, _ := newSinkNode(t)
	ids = append(ids, sinkId)

	// Data
	requestId, err := uuid.GenerateUUID()
	require.NoError(t, err)
	data := newData(requestId)

	err = processManual(nshelper.RootContext(context.Background()), data, ids, nodes)
	require.Error(t, err)
	require.ErrorContains(t, err, "node not found: ")
}

// TestProcessManual_NotEnoughNodes tests processManual when there is only one
// node provided.
func TestProcessManual_NotEnoughNodes(t *testing.T) {
	t.Parallel()

	var ids []eventlogger.NodeID
	nodes := make(map[eventlogger.NodeID]eventlogger.Node)

	// Formatter node
	formatterId, formatterNode := newFormatterNode(t)
	ids = append(ids, formatterId)
	nodes[formatterId] = formatterNode

	// Data
	requestId, err := uuid.GenerateUUID()
	require.NoError(t, err)
	data := newData(requestId)

	err = processManual(nshelper.RootContext(context.Background()), data, ids, nodes)
	require.Error(t, err)
	require.EqualError(t, err, "minimum of 2 ids are required")
}

// TestProcessManual_LastNodeNotSink tests processManual when the last node is
// not a Sink node.
func TestProcessManual_LastNodeNotSink(t *testing.T) {
	t.Parallel()

	var ids []eventlogger.NodeID
	nodes := make(map[eventlogger.NodeID]eventlogger.Node)

	// Formatter node
	formatterId, formatterNode := newFormatterNode(t)
	ids = append(ids, formatterId)
	nodes[formatterId] = formatterNode

	// Another Formatter node
	formatterId, formatterNode = newFormatterNode(t)
	ids = append(ids, formatterId)
	nodes[formatterId] = formatterNode

	// Data
	requestId, err := uuid.GenerateUUID()
	require.NoError(t, err)
	data := newData(requestId)

	err = processManual(nshelper.RootContext(context.Background()), data, ids, nodes)
	require.Error(t, err)
	require.EqualError(t, err, "last node must be a filter or sink")
}

// TestProcessManualEndWithSink ensures that the manual processing of a test
// message works as expected with proper inputs, which mean processing ends with
// sink node.
func TestProcessManualEndWithSink(t *testing.T) {
	t.Parallel()

	var ids []eventlogger.NodeID
	nodes := make(map[eventlogger.NodeID]eventlogger.Node)

	// Formatter node
	formatterId, formatterNode := newFormatterNode(t)
	ids = append(ids, formatterId)
	nodes[formatterId] = formatterNode

	// Sink node
	sinkId, sinkNode := newSinkNode(t)
	ids = append(ids, sinkId)
	nodes[sinkId] = sinkNode

	// Data
	requestId, err := uuid.GenerateUUID()
	require.NoError(t, err)
	data := newData(requestId)

	err = processManual(nshelper.RootContext(context.Background()), data, ids, nodes)
	require.NoError(t, err)
}

// TestProcessManual_EndWithFilter ensures that the manual processing of a test
// message works as expected with proper inputs, which mean processing ends with
// sink node.
func TestProcessManual_EndWithFilter(t *testing.T) {
	t.Parallel()

	var ids []eventlogger.NodeID
	nodes := make(map[eventlogger.NodeID]eventlogger.Node)

	// Filter node
	filterId, filterNode := newFilterNode(t)
	ids = append(ids, filterId)
	nodes[filterId] = filterNode

	// Formatter node
	formatterId, formatterNode := newFormatterNode(t)
	ids = append(ids, formatterId)
	nodes[formatterId] = formatterNode

	// Sink node
	sinkId, sinkNode := newSinkNode(t)
	ids = append(ids, sinkId)
	nodes[sinkId] = sinkNode

	// Data
	requestId, err := uuid.GenerateUUID()
	require.NoError(t, err)
	data := newData(requestId)

	err = processManual(nshelper.RootContext(context.Background()), data, ids, nodes)
	require.NoError(t, err)
}

// newSinkNode creates a new UUID and NoopSink (sink node).
func newSinkNode(t *testing.T) (eventlogger.NodeID, *event.NoopSink) {
	t.Helper()

	sinkId, err := event.GenerateNodeID()
	require.NoError(t, err)
	sinkNode := event.NewNoopSink()

	return sinkId, sinkNode
}

// TestFilter is a trivial implementation of eventlogger.Node used as a placeholder
// for Filter nodes in tests.
type TestFilter struct{}

// Process trivially filters the event preventing it from being processed by subsequent nodes.
func (f *TestFilter) Process(_ context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
	return nil, nil
}

// Reopen does nothing.
func (f *TestFilter) Reopen() error {
	return nil
}

// Type returns the eventlogger.NodeTypeFormatter type.
func (f *TestFilter) Type() eventlogger.NodeType {
	return eventlogger.NodeTypeFilter
}

// TestFormatter is a trivial implementation of the eventlogger.Node interface
// used as a place-holder for Formatter nodes in tests.
type TestFormatter struct{}

// Process trivially formats the event by storing "test" as a byte slice under
// the test format type.
func (f *TestFormatter) Process(_ context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
	e.FormattedAs("test", []byte("test"))

	return e, nil
}

// Reopen does nothing.
func (f *TestFormatter) Reopen() error {
	return nil
}

// Type returns the eventlogger.NodeTypeFormatter type.
func (f *TestFormatter) Type() eventlogger.NodeType {
	return eventlogger.NodeTypeFormatter
}

// newFilterNode creates a new TestFormatter (filter node).
func newFilterNode(t *testing.T) (eventlogger.NodeID, *TestFilter) {
	nodeId, err := event.GenerateNodeID()
	require.NoError(t, err)
	node := &TestFilter{}

	return nodeId, node
}

// newFormatterNode creates a new TestFormatter (formatter node).
func newFormatterNode(t *testing.T) (eventlogger.NodeID, *TestFormatter) {
	nodeId, err := event.GenerateNodeID()
	require.NoError(t, err)
	node := &TestFormatter{}

	return nodeId, node
}

// newData creates a sample logical.LogInput to be used as data for tests.
func newData(id string) *logical.LogInput {
	return &logical.LogInput{
		Type: "request",
		Auth: nil,
		Request: &logical.Request{
			ID:        id,
			Operation: "update",
			Path:      "sys/audit/test",
		},
		Response: nil,
		OuterErr: nil,
	}
}
