package alertcontext

import (
	"fmt"
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/crowdsecurity/go-cs-lib/ptr"

	"github.com/crowdsecurity/crowdsec/pkg/models"
	"github.com/crowdsecurity/crowdsec/pkg/pipeline"
)

func TestNewAlertContext(t *testing.T) {
	tests := []struct {
		name          string
		contextToSend map[string][]string
		valueLength   int
		expectedErr   error
	}{
		{
			name: "basic config test",
			contextToSend: map[string][]string{
				"test": {"evt.Parsed.source_ip"},
			},
			valueLength: 100,
			expectedErr: nil,
		},
	}

	for _, test := range tests {
		fmt.Printf("Running test '%s'\n", test.name)
		err := NewAlertContext(test.contextToSend, test.valueLength)
		require.ErrorIs(t, err, test.expectedErr)
	}
}

func TestEventToContext(t *testing.T) {
	tests := []struct {
		name           string
		contextToSend  map[string][]string
		valueLength    int
		events         []pipeline.Event
		expectedResult models.Meta
	}{
		{
			name: "basic test",
			contextToSend: map[string][]string{
				"source_ip":         {"evt.Parsed.source_ip"},
				"nonexistent_field": {"evt.Parsed.nonexist"},
			},
			valueLength: 100,
			events: []pipeline.Event{
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
					},
				},
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "source_ip",
					Value: "[\"1.2.3.4\"]",
				},
			},
		},
		{
			name: "test many events",
			contextToSend: map[string][]string{
				"source_ip":      {"evt.Parsed.source_ip"},
				"source_machine": {"evt.Parsed.source_machine"},
				"cve":            {"evt.Parsed.cve"},
			},
			valueLength: 100,
			events: []pipeline.Event{
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
						"cve":            "CVE-2022-1234",
					},
				},
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
						"cve":            "CVE-2022-1235",
					},
				},
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
						"cve":            "CVE-2022-125",
					},
				},
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "source_ip",
					Value: "[\"1.2.3.4\"]",
				},
				{
					Key:   "source_machine",
					Value: "[\"mymachine\"]",
				},
				{
					Key:   "cve",
					Value: "[\"CVE-2022-1234\",\"CVE-2022-1235\",\"CVE-2022-125\"]",
				},
			},
		},
		{
			name: "test many events with result above max length (need truncate, keep only 2 on 3 elements)",
			contextToSend: map[string][]string{
				"source_ip":      {"evt.Parsed.source_ip"},
				"source_machine": {"evt.Parsed.source_machine"},
				"uri":            {"evt.Parsed.uri"},
			},
			valueLength: 100,
			events: []pipeline.Event{
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
						"uri":            "/test/test/test/../../../../../../../../",
					},
				},
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
						"uri":            "/admin/admin/admin/../../../../../../../../",
					},
				},
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
						"uri":            "/login/login/login/../../../../../../../../../../../",
					},
				},
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "source_ip",
					Value: "[\"1.2.3.4\"]",
				},
				{
					Key:   "source_machine",
					Value: "[\"mymachine\"]",
				},
				{
					Key:   "uri",
					Value: "[\"/test/test/test/../../../../../../../../\",\"/admin/admin/admin/../../../../../../../../\"]",
				},
			},
		},
		{
			name: "test one events with result above max length (need truncate on one element)",
			contextToSend: map[string][]string{
				"source_ip":      {"evt.Parsed.source_ip"},
				"source_machine": {"evt.Parsed.source_machine"},
				"uri":            {"evt.Parsed.uri"},
			},
			valueLength: 100,
			events: []pipeline.Event{
				{
					Parsed: map[string]string{
						"source_ip":      "1.2.3.4",
						"source_machine": "mymachine",
						"uri":            "/test/test/test/../../../../.should_truncate_just_after_this/../../../..../../../../../../../../../../../../../../../end",
					},
				},
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "source_machine",
					Value: "[\"mymachine\"]",
				},
				{
					Key:   "uri",
					Value: "[\"/test/test/test/../../../../.should_truncate_just_after_this...\"]",
				},
				{
					Key:   "source_ip",
					Value: "[\"1.2.3.4\"]",
				},
			},
		},
	}

	for _, test := range tests {
		fmt.Printf("Running test '%s'\n", test.name)
		err := NewAlertContext(test.contextToSend, test.valueLength)
		require.NoError(t, err)

		metas, _ := EventToContext(test.events)
		assert.ElementsMatch(t, test.expectedResult, metas)
	}
}

func TestValidateContextExpr(t *testing.T) {
	tests := []struct {
		name        string
		key         string
		exprs       []string
		expectedErr *string
	}{
		{
			name: "basic config",
			key:  "source_ip",
			exprs: []string{
				"evt.Parsed.source_ip",
			},
			expectedErr: nil,
		},
		{
			name: "basic config with non existent field",
			key:  "source_ip",
			exprs: []string{
				"evt.invalid.source_ip",
			},
			expectedErr: ptr.Of("compilation of 'evt.invalid.source_ip' failed: type pipeline.Event has no field invalid"),
		},
	}
	for _, test := range tests {
		fmt.Printf("Running test '%s'\n", test.name)

		err := ValidateContextExpr(test.key, test.exprs)
		if test.expectedErr == nil {
			require.NoError(t, err)
		} else {
			require.ErrorContains(t, err, *test.expectedErr)
		}
	}
}

func TestAppsecEventToContext(t *testing.T) {
	tests := []struct {
		name           string
		contextToSend  map[string][]string
		match          pipeline.AppsecEvent
		req            *http.Request
		expectedResult models.Meta
		expectedErrLen int
	}{
		{
			name: "basic test on match",
			contextToSend: map[string][]string{
				"id": {"match.id"},
			},
			match: pipeline.AppsecEvent{
				MatchedRules: pipeline.MatchedRules{
					{
						"id": "test",
					},
				},
			},
			req: &http.Request{},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "id",
					Value: "[\"test\"]",
				},
			},
			expectedErrLen: 0,
		},
		{
			name: "basic test on req",
			contextToSend: map[string][]string{
				"ua": {"req.UserAgent()"},
			},
			match: pipeline.AppsecEvent{
				MatchedRules: pipeline.MatchedRules{
					{
						"id": "test",
					},
				},
			},
			req: &http.Request{
				Header: map[string][]string{
					"User-Agent": {"test"},
				},
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "ua",
					Value: "[\"test\"]",
				},
			},
			expectedErrLen: 0,
		},
		{
			name: "test on req -> []string",
			contextToSend: map[string][]string{
				"foobarxx": {"req.Header.Values('Foobar')"},
			},
			match: pipeline.AppsecEvent{
				MatchedRules: pipeline.MatchedRules{
					{
						"id": "test",
					},
				},
			},
			req: &http.Request{
				Header: map[string][]string{
					"User-Agent": {"test"},
					"Foobar":     {"test1", "test2"},
				},
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "foobarxx",
					Value: "[\"test1\",\"test2\"]",
				},
			},
			expectedErrLen: 0,
		},
		{
			name: "test on type int",
			contextToSend: map[string][]string{
				"foobarxx": {"len(req.Header.Values('Foobar'))"},
			},
			match: pipeline.AppsecEvent{
				MatchedRules: pipeline.MatchedRules{
					{
						"id": "test",
					},
				},
			},
			req: &http.Request{
				Header: map[string][]string{
					"User-Agent": {"test"},
					"Foobar":     {"test1", "test2"},
				},
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "foobarxx",
					Value: "[\"2\"]",
				},
			},
			expectedErrLen: 0,
		},
		{
			name: "test JA4H - appsec event",
			contextToSend: map[string][]string{
				"ja4h": {"JA4H(req)"},
			},
			match: pipeline.AppsecEvent{
				MatchedRules: pipeline.MatchedRules{
					{
						"id": "test",
					},
				},
			},
			req: &http.Request{
				Header: map[string][]string{
					"User-Agent": {"test"},
					"Foobar":     {"test1", "test2"},
				},
				ProtoMajor: 1,
				ProtoMinor: 1,
				Method:     http.MethodGet,
			},
			expectedResult: []*models.MetaItems0{
				{
					Key:   "ja4h",
					Value: "[\"ge11nn020000_3a31a0f8fbf9_000000000000_000000000000\"]",
				},
			},
		},
		{
			name: "test JA4H - no appsec event",
			contextToSend: map[string][]string{
				"ja4h": {"JA4H(req)"},
			},
			req:            nil,
			expectedResult: []*models.MetaItems0{},
		},
	}

	for _, test := range tests {
		// reset cache
		alertContext = Context{}
		// compile
		if err := NewAlertContext(test.contextToSend, 100); err != nil {
			t.Fatalf("failed to compile %s: %s", test.name, err)
		}
		// run

		metas, errors := AppsecEventToContext(test.match, test.req)
		assert.Len(t, errors, test.expectedErrLen)
		assert.ElementsMatch(t, test.expectedResult, metas)
	}
}

func TestEvalAlertContextRules(t *testing.T) {
	tests := []struct {
		name           string
		contextToSend  map[string][]string
		event          pipeline.Event
		match          pipeline.MatchedRule
		req            *http.Request
		expectedResult map[string][]string
		expectedErrLen int
	}{
		{
			name: "no appsec match",
			contextToSend: map[string][]string{
				"source_ip": {"evt.Parsed.source_ip"},
				"id":        {"match.id"},
			},
			event: pipeline.Event{
				Parsed: map[string]string{
					"source_ip":      "1.2.3.4",
					"source_machine": "mymachine",
					"uri":            "/test/test/test/../../../../../../../../",
				},
			},
			expectedResult: map[string][]string{
				"source_ip": {"1.2.3.4"},
				"id":        {},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			contextDict := make(map[string][]string)

			alertContext = Context{}
			if err := NewAlertContext(test.contextToSend, 100); err != nil {
				t.Fatalf("failed to compile %s: %s", test.name, err)
			}

			errs := EvalAlertContextRules(test.event, &test.match, test.req, contextDict)
			assert.Len(t, errs, test.expectedErrLen)
			assert.Equal(t, test.expectedResult, contextDict)
		})
	}
}
