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

package audit

import (
	"testing"

	"github.com/hashicorp/eventlogger"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/vault/helper/constants"
	"github.com/stretchr/testify/require"
)

// TestBackend_newFormatterConfig ensures that all the configuration values are
// parsed correctly when trying to create a new formatterConfig via newFormatterConfig.
func TestBackend_newFormatterConfig(t *testing.T) {
	t.Parallel()

	tests := map[string]struct {
		config          map[string]string
		want            formatterConfig
		wantErr         bool
		expectedMessage string
	}{
		"happy-path-json": {
			config: map[string]string{
				"format":               jsonFormat.String(),
				"hmac_accessor":        "true",
				"log_raw":              "true",
				"elide_list_responses": "true",
			},
			want: formatterConfig{
				raw:                true,
				hmacAccessor:       true,
				elideListResponses: true,
				requiredFormat:     "json",
			}, wantErr: false,
		},
		"happy-path-jsonx": {
			config: map[string]string{
				"format":               jsonxFormat.String(),
				"hmac_accessor":        "true",
				"log_raw":              "true",
				"elide_list_responses": "true",
			},
			want: formatterConfig{
				raw:                true,
				hmacAccessor:       true,
				elideListResponses: true,
				requiredFormat:     "jsonx",
			},
			wantErr: false,
		},
		"invalid-format": {
			config: map[string]string{
				"format":               " squiggly ",
				"hmac_accessor":        "true",
				"log_raw":              "true",
				"elide_list_responses": "true",
			},
			want:            formatterConfig{},
			wantErr:         true,
			expectedMessage: "unsupported \"format\": invalid configuration",
		},
		"invalid-hmac-accessor": {
			config: map[string]string{
				"format":        jsonFormat.String(),
				"hmac_accessor": "maybe",
			},
			want:            formatterConfig{},
			wantErr:         true,
			expectedMessage: "unable to parse \"hmac_accessor\": invalid configuration",
		},
		"invalid-log-raw": {
			config: map[string]string{
				"format":        jsonFormat.String(),
				"hmac_accessor": "true",
				"log_raw":       "maybe",
			},
			want:            formatterConfig{},
			wantErr:         true,
			expectedMessage: "unable to parse \"log_raw\": invalid configuration",
		},
		"invalid-elide-bool": {
			config: map[string]string{
				"format":               jsonFormat.String(),
				"hmac_accessor":        "true",
				"log_raw":              "true",
				"elide_list_responses": "maybe",
			},
			want:            formatterConfig{},
			wantErr:         true,
			expectedMessage: "unable to parse \"elide_list_responses\": invalid configuration",
		},
		"prefix": {
			config: map[string]string{
				"format": jsonFormat.String(),
				"prefix": "foo",
			},
			want: formatterConfig{
				requiredFormat: jsonFormat,
				prefix:         "foo",
				hmacAccessor:   true,
			},
		},
	}
	for name, tc := range tests {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			got, err := newFormatterConfig(&noopHeaderFormatter{}, tc.config)
			if tc.wantErr {
				require.Error(t, err)
				require.EqualError(t, err, tc.expectedMessage)
			} else {
				require.NoError(t, err)
			}
			require.Equal(t, tc.want.requiredFormat, got.requiredFormat)
			require.Equal(t, tc.want.raw, got.raw)
			require.Equal(t, tc.want.elideListResponses, got.elideListResponses)
			require.Equal(t, tc.want.hmacAccessor, got.hmacAccessor)
			require.Equal(t, tc.want.omitTime, got.omitTime)
			require.Equal(t, tc.want.prefix, got.prefix)
		})
	}
}

// TestBackend_configureFormatterNode ensures that configureFormatterNode
// populates the nodeIDList and nodeMap on backend when given valid config.
func TestBackend_configureFormatterNode(t *testing.T) {
	t.Parallel()

	b, err := newBackend(&noopHeaderFormatter{}, &BackendConfig{
		MountPath: "foo",
		Logger:    hclog.NewNullLogger(),
	})
	require.NoError(t, err)

	require.Len(t, b.nodeIDList, 1)
	require.Len(t, b.nodeMap, 1)
	id := b.nodeIDList[0]
	node := b.nodeMap[id]
	require.Equal(t, eventlogger.NodeTypeFormatter, node.Type())
}

// TestBackend_hasEnterpriseAuditOptions checks that the existence of any Enterprise
// only options in the options which can be supplied to enable an audit device can
// be flagged.
func TestBackend_hasEnterpriseAuditOptions(t *testing.T) {
	t.Parallel()

	tests := map[string]struct {
		input    map[string]string
		expected bool
	}{
		"nil": {
			expected: false,
		},
		"empty": {
			input:    make(map[string]string),
			expected: false,
		},
		"non-ent-opts": {
			input: map[string]string{
				"log_raw": "true",
			},
			expected: false,
		},
		"ent-opt-filter": {
			input: map[string]string{
				"filter": "mount_type == kv",
			},
			expected: true,
		},
		"ent-opt-fallback": {
			input: map[string]string{
				"fallback": "true",
			},
			expected: true,
		},
		"ent-opt-filter-and-fallback": {
			input: map[string]string{
				"filter":   "mount_type == kv",
				"fallback": "true",
			},
			expected: true,
		},
		"ent-opt-exclude": {
			input: map[string]string{
				"exclude": `{
					"condition": "\"/request/mount_type\" == transit",
					"fields": [ "/request/data", "/response/data" ]
				}`,
			},
			expected: true,
		},
	}

	for name, tc := range tests {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tc.expected, hasEnterpriseAuditOptions(tc.input))
		})
	}
}

// TestBackend_hasInvalidAuditOptions tests that depending on whether we are running
// an Enterprise or non-Enterprise version of Vault, the options supplied to enable
// an audit device may or may not be valid.
// NOTE: In the non-Enterprise version of Vault supplying audit options such as
// 'filter' or 'fallback' is not allowed.
func TestBackend_hasInvalidAuditOptions(t *testing.T) {
	tests := map[string]struct {
		input    map[string]string
		expected bool
	}{
		"non-ent-opts": {
			input: map[string]string{
				"log_raw": "true",
			},
			expected: false,
		},
		"ent-opt": {
			input: map[string]string{
				"filter": "mount_type == kv",
			},
			expected: !constants.IsEnterprise,
		},
		"ent-opt-filter": {
			input: map[string]string{
				"filter": "mount_type == kv",
			},
			expected: !constants.IsEnterprise,
		},
		"ent-opt-fallback": {
			input: map[string]string{
				"fallback": "true",
			},
			expected: !constants.IsEnterprise,
		},
		"ent-opt-filter-and-fallback": {
			input: map[string]string{
				"filter":   "mount_type == kv",
				"fallback": "true",
			},
			expected: !constants.IsEnterprise,
		},
		"ent-opt-exclude": {
			input: map[string]string{
				"exclude": `{
					"condition": "\"/request/mount_type\" == transit",
					"fields": [ "/request/data", "/response/data" ]
				}`,
			},
			expected: !constants.IsEnterprise,
		},
	}

	for name, tc := range tests {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tc.expected, HasInvalidOptions(tc.input))
		})
	}
}
