// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package event

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/hashicorp/eventlogger"
	wrapping "github.com/hashicorp/go-kms-wrapping/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestHclogFormatter_Process(t *testing.T) {
	t.Parallel()
	ctx := context.Background()

	f, e := newFilter(`op == "match-filter"`)
	require.NoError(t, e)

	testPredicate := newPredicate([]*filter{f}, nil)

	tests := []struct {
		name            string
		formatter       *hclogFormatterFilter
		e               *eventlogger.Event
		wantErrContains string
		want            []string
	}{
		{
			name: "nil event",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
			},
			wantErrContains: "event is nil",
		},
		{
			name: "invalid-event-type",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
			},
			e:               &eventlogger.Event{Type: eventlogger.EventType("invalid-type")},
			wantErrContains: "unknown event type invalid-type",
		},
		{
			name: "sys-text",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(SystemType),
				Payload: &sysEvent{
					Id:      "1",
					Version: errorVersion,
					Op:      Op("text"),
					Data: map[string]any{
						"msg": "hello",
					},
				},
			},
			want: []string{
				"[INFO]  system event:",
				"data:msg=hello",
				"version=v0.1",
				"op=text",
			},
		},
		{
			name: "observation-text",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(ObservationType),
				Payload: map[string]any{
					"id":         "1",
					"version":    observationVersion,
					"latency-ms": 10,
				},
			},
			want: []string{
				"[INFO]  observation event:",
				"latency-ms=10",
				"id=1",
				"version=v0.1",
			},
		},
		{
			name: "observation-json",
			formatter: &hclogFormatterFilter{
				jsonFormat: true,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(ObservationType),
				Payload: map[string]any{
					"id":         "1",
					"version":    observationVersion,
					"latency-ms": 10,
				},
			},
			want: []string{
				"{\"@level\":\"info\",\"@message\":\"observation event\"",
				"\"latency-ms\":10",
				"\"id\":\"1\"",
				"\"version\":\"v0.1\"}\n",
			},
		},
		{
			name: "err-text",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(ErrorType),
				Payload: &err{
					Id:      "1",
					Version: errorVersion,
					Error:   ErrInvalidParameter.Error(),
					Op:      Op("text"),
				},
			},
			want: []string{
				"[ERROR] error event:",
				"error=\"invalid parameter\"",
				"id=1",
				"version=v0.1",
				"op=text",
			},
		},
		{
			name: "err-json",
			formatter: &hclogFormatterFilter{
				jsonFormat: true,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(ErrorType),
				Payload: &err{
					Id:      "1",
					Version: errorVersion,
					Error:   ErrInvalidParameter.Error(),
					Op:      Op("text"),
				},
			},
			want: []string{
				"{\"@level\":\"error\",\"@message\":\"error event\"",
				"\"error\":\"invalid parameter\"",
				"\"id\":\"1\"",
				"\"version\":\"v0.1\"",
				"\"op\":\"text\"",
			},
		},
		{
			name: "err-text-with-optional",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(ErrorType),
				Payload: &err{
					Id:      "1",
					Version: errorVersion,
					Error:   ErrInvalidParameter.Error(),
					Op:      Op("text"),
					Info:    map[string]any{"name": "alice"},
				},
			},
			want: []string{
				"[ERROR] error event:",
				"error=\"invalid parameter\"",
				"id=1",
				"version=v0.1",
				"info:name=alice",
				"op=text",
			},
		},
		{
			name: "filter-match",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
				predicate:  testPredicate,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(SystemType),
				Payload: &sysEvent{
					Id:      "1",
					Version: errorVersion,
					Op:      Op("match-filter"),
					Data: map[string]any{
						"msg": "hello",
					},
				},
			},
			want: []string{
				"[INFO]  system event:",
				"data:msg=hello",
				"version=v0.1",
				"op=match-filter",
			},
		},
		{
			name: "filter-no-match",
			formatter: &hclogFormatterFilter{
				jsonFormat: false,
				predicate:  testPredicate,
			},
			e: &eventlogger.Event{
				Type: eventlogger.EventType(SystemType),
				Payload: &sysEvent{
					Id:      "1",
					Version: errorVersion,
					Op:      Op("doesn't match"),
					Data: map[string]any{
						"msg": "hello",
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			e, err := tt.formatter.Process(ctx, tt.e)
			if tt.wantErrContains != "" {
				require.Error(err)
				assert.Contains(err.Error(), tt.wantErrContains)
				return
			}
			require.NoError(err)
			if len(tt.want) == 0 {
				assert.Nil(e)
				return
			}
			assert.NotNil(e)
			var b []byte
			var ok bool
			switch tt.formatter.jsonFormat {
			case true:
				b, ok = e.Format(string(JSONHclogSinkFormat))
			case false:
				b, ok = e.Format(string(TextHclogSinkFormat))
			}
			t.Log(string(b))
			require.True(ok)
			for _, txt := range tt.want {
				assert.Contains(string(b), txt)
			}
		})
	}
	t.Run("with-signing", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		wrapper := testWrapper(t)
		f, err := newHclogFormatterFilter(true) // produce json formatted events
		require.NoError(err)
		require.NoError(f.Rotate(wrapper))
		require.NotNil(f.signer)

		e := &eventlogger.Event{
			Type: eventlogger.EventType(AuditType),
			Payload: &audit{
				Id:      "1",
				Version: auditVersion,
				Auth:    &Auth{UserName: "alice"},
			},
		}

		gotEvent, err := f.Process(ctx, e)
		require.NoError(err)
		b, ok := gotEvent.Format(string(JSONHclogSinkFormat))
		require.True(ok)
		var rep map[string]any
		require.NoError(json.Unmarshal(b, &rep))
		assert.NotEmpty(rep["serialized"])
		assert.NotEmpty(rep["serialized_hmac"])
	})
}

func Test_hclogFormatterFilter_Name(t *testing.T) {
	t.Parallel()
	t.Run("simple", func(t *testing.T) {
		ff := &hclogFormatterFilter{}
		assert.Equal(t, hclogNodeName, ff.Name())
	})
}

func Test_hclogFormatterFilter_Reopen(t *testing.T) {
	t.Parallel()
	t.Run("simple", func(t *testing.T) {
		ff := &hclogFormatterFilter{}
		assert.Equal(t, nil, ff.Reopen())
	})
}

func Test_hclogFormatterFilter_Type(t *testing.T) {
	t.Parallel()
	t.Run("simple", func(t *testing.T) {
		ff := &hclogFormatterFilter{}
		assert.Equal(t, eventlogger.NodeTypeFormatterFilter, ff.Type())
	})
}

func Test_newHclogFormatterFilter(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name            string
		jsonFormat      bool
		opt             []Option
		wantErr         bool
		wantIsError     error
		wantErrContains string
		wantAllow       []string
		wantDeny        []string
	}{
		{
			name: "no-opts",
		},
		{
			name:       "bad-allow-filter",
			jsonFormat: true,
			opt: []Option{
				WithAllow("foo=;22", "foo==bar"),
			},
			wantErr:         true,
			wantErrContains: "invalid allow filter 'foo=;22'",
		},
		{
			name:       "bad-deny-filter",
			jsonFormat: true,
			opt: []Option{
				WithDeny("foo=;22", "foo==bar"),
			},
			wantErr:         true,
			wantErrContains: "invalid deny filter 'foo=;22'",
		},
		{
			name:       "empty-allow-filter",
			jsonFormat: true,
			opt: []Option{
				WithAllow(""),
			},
			wantErr:         true,
			wantErrContains: "missing filter",
		},
		{
			name:       "empty-deny-filter",
			jsonFormat: true,
			opt: []Option{
				WithDeny(""),
			},
			wantErr:         true,
			wantErrContains: "missing filter",
		},
		{
			name:       "valid-filters",
			jsonFormat: true,
			opt: []Option{
				WithAllow("alice==friend", "bob==friend"),
				WithDeny("eve==acquaintance", "fido!=dog"),
			},
			wantAllow: []string{"alice==friend", "bob==friend"},
			wantDeny:  []string{"eve==acquaintance", "fido!=dog"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			got, err := newHclogFormatterFilter(tt.jsonFormat, tt.opt...)
			if tt.wantErr {
				require.Error(err)
				assert.Nil(got)
				if tt.wantIsError != nil {
					assert.ErrorIs(err, tt.wantIsError)
				}
				if tt.wantErrContains != "" {
					assert.Contains(err.Error(), tt.wantErrContains)
				}
				return
			}
			require.NoError(err)
			assert.NotNil(got)

			assert.Equal(tt.jsonFormat, got.jsonFormat)

			assert.Len(got.allow, len(tt.wantAllow))
			for _, f := range got.allow {
				assert.Contains(tt.wantAllow, f.raw)
			}
			assert.Len(got.deny, len(tt.wantDeny)+4) // +4 since there's always a default deny
			defs, err := defaultHclogEventsDenyFilters()
			require.NoError(err)
			for _, f := range defs {
				tt.wantDeny = append(tt.wantDeny, f.raw)
			}
			for _, f := range got.deny {
				assert.Contains(tt.wantDeny, f.raw)
			}
		})
	}
}

func Test_hclogFormatterFilter_Rotate(t *testing.T) {
	tests := []struct {
		name            string
		f               *hclogFormatterFilter
		w               wrapping.Wrapper
		wantIsError     error
		wantErrContains string
	}{
		{
			name:            "missing-wrapper",
			f:               &hclogFormatterFilter{},
			wantIsError:     ErrInvalidParameter,
			wantErrContains: "missing wrapper",
		},
		{
			name: "valid",
			f:    &hclogFormatterFilter{},
			w:    testWrapper(t),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			err := tt.f.Rotate(tt.w)
			if tt.wantIsError != nil {
				require.Error(err)
				assert.ErrorIs(err, tt.wantIsError)
				if tt.wantErrContains != "" {
					assert.Contains(err.Error(), tt.wantErrContains)
				}
				return
			}
			assert.NotNil(tt.f.signer)
		})
	}
}
