package exprhelpers

import (
	"net/http"
	"net/url"
	"reflect"
	"testing"

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

func TestParseQuery(t *testing.T) {
	tests := []struct {
		name     string
		query    string
		expected url.Values
	}{
		{
			name:  "Full URI",
			query: "/foobar/toto?ab=cd&ef=gh",
			expected: url.Values{
				"/foobar/toto?ab": []string{"cd"},
				"ef":              []string{"gh"},
			},
		},
		{
			name:  "Simple query",
			query: "foo=bar",
			expected: url.Values{
				"foo": []string{"bar"},
			},
		},
		{
			name:  "Multiple values",
			query: "foo=bar&foo=baz",
			expected: url.Values{
				"foo": []string{"bar", "baz"},
			},
		},
		{
			name:  "Empty value",
			query: "foo=",
			expected: url.Values{
				"foo": []string{""},
			},
		},
		{
			name:  "Empty key",
			query: "=bar",
			expected: url.Values{
				"": []string{"bar"},
			},
		},
		{
			name:     "Empty query",
			query:    "",
			expected: url.Values{},
		},
		{
			name:  "Multiple keys",
			query: "foo=bar&baz=qux",
			expected: url.Values{
				"foo": []string{"bar"},
				"baz": []string{"qux"},
			},
		},
		{
			name:  "Multiple keys with empty value",
			query: "foo=bar&baz=qux&quux=",
			expected: url.Values{
				"foo":  []string{"bar"},
				"baz":  []string{"qux"},
				"quux": []string{""},
			},
		},
		{
			name:  "Multiple keys with empty value and empty key",
			query: "foo=bar&baz=qux&quux=&=quuz",
			expected: url.Values{
				"foo":  []string{"bar"},
				"baz":  []string{"qux"},
				"quux": []string{""},
				"":     []string{"quuz"},
			},
		},
		{
			name:  "Multiple keys with empty value and empty key and multiple values",
			query: "foo=bar&baz=qux&quux=&=quuz&foo=baz",
			expected: url.Values{
				"foo":  []string{"bar", "baz"},
				"baz":  []string{"qux"},
				"quux": []string{""},
				"":     []string{"quuz"},
			},
		},
		{
			name:  "Multiple keys with empty value and empty key and multiple values and escaped characters",
			query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz",
			expected: url.Values{
				"foo":  []string{"bar", "baz", "bar baz"},
				"baz":  []string{"qux"},
				"quux": []string{""},
				"":     []string{"quuz"},
			},
		},
		{
			name:  "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon",
			query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz",
			expected: url.Values{
				"foo":  []string{"bar", "baz", "bar baz", "bar;baz"},
				"baz":  []string{"qux"},
				"quux": []string{""},
				"":     []string{"quuz"},
			},
		},
		{
			name:  "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand",
			query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz",
			expected: url.Values{
				"foo":  []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz"},
				"baz":  []string{"qux"},
				"quux": []string{""},
				"":     []string{"quuz"},
			},
		},
		{
			name:  "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand and equals",
			query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz&foo=bar%3Dbaz",
			expected: url.Values{
				"foo":  []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz", "bar=baz"},
				"baz":  []string{"qux"},
				"quux": []string{""},
				"":     []string{"quuz"},
			},
		},
		{
			name:  "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand and equals and question mark",
			query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz&foo=bar%3Dbaz&foo=bar%3Fbaz",
			expected: url.Values{
				"foo":  []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz", "bar=baz", "bar?baz"},
				"baz":  []string{"qux"},
				"quux": []string{""},
				"":     []string{"quuz"},
			},
		},
		{
			name:  "keys with escaped characters",
			query: "foo=ba;r&baz=qu;;x&quux=x\\&ww&xx=qu?uz&",
			expected: url.Values{
				"foo":  []string{"ba;r"},
				"baz":  []string{"qu;;x"},
				"quux": []string{"x\\"},
				"ww":   []string{""},
				"xx":   []string{"qu?uz"},
			},
		},
		{
			name:  "hexadecimal characters",
			query: "foo=bar%20baz",
			expected: url.Values{
				"foo": []string{"bar baz"},
			},
		},
		{
			name:  "hexadecimal characters upper and lower case",
			query: "foo=Ba%42%42&bar=w%2f%2F",
			expected: url.Values{
				"foo": []string{"BaBB"},
				"bar": []string{"w//"},
			},
		},
		{
			name:  "hexadecimal characters with invalid characters",
			query: "foo=bar%20baz%2",
			expected: url.Values{
				"foo": []string{"bar baz%2"},
			},
		},
		{
			name:  "hexadecimal characters with invalid hex characters",
			query: "foo=bar%xx",
			expected: url.Values{
				"foo": []string{"bar%xx"},
			},
		},
		{
			name:  "hexadecimal characters with invalid 2nd hex character",
			query: "foo=bar%2x",
			expected: url.Values{
				"foo": []string{"bar%2x"},
			},
		},
		{
			name:  "url +",
			query: "foo=bar+x",
			expected: url.Values{
				"foo": []string{"bar x"},
			},
		},
		{
			name:  "url &&",
			query: "foo=bar&&lol=bur",
			expected: url.Values{
				"foo": []string{"bar"},
				"lol": []string{"bur"},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			res := ParseQuery(test.query)
			if !reflect.DeepEqual(res, test.expected) {
				t.Fatalf("unexpected result: %v", res)
			}
		})
	}
}

func TestExtractQueryParam(t *testing.T) {
	tests := []struct {
		name     string
		query    string
		param    string
		expected []string
	}{
		{
			name:     "Simple uri",
			query:    "/foobar/toto?ab=cd&ef=gh",
			param:    "ab",
			expected: []string{"cd"},
		},
		{
			name:     "Simple uri, repeating param",
			query:    "/foobar?foo=bar&foo=baz",
			param:    "foo",
			expected: []string{"bar", "baz"},
		},
		{
			name:     "Simple uri with semicolon",
			query:    "/foobar/toto?ab=cd;ef=gh",
			param:    "ab",
			expected: []string{"cd;ef=gh"},
		},
		{
			name:     "Simple query no uri",
			query:    "foo=bar",
			param:    "foo",
			expected: []string{},
		},
		{
			name:     "No QS",
			query:    "/foobar",
			param:    "foo",
			expected: []string{},
		},
		{
			name:     "missing param",
			query:    "/foobar/toto?ab=cd&ef=gh",
			param:    "baz",
			expected: []string{},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			res := ExtractQueryParam(test.query, test.param)
			if !reflect.DeepEqual(res, test.expected) {
				t.Fatalf("unexpected result: %v", res)
			}
		})
	}
}

func TestJA4H(t *testing.T) {
	ctx := t.Context()

	tests := []struct {
		name         string
		method       string
		url          string
		cookies      map[string]string
		headers      map[string]string
		expectedHash string
	}{
		{
			name:         "Basic GET - No cookies",
			method:       "GET",
			url:          "http://example.com",
			cookies:      map[string]string{},
			headers:      map[string]string{},
			expectedHash: "ge11nn000000_e3b0c44298fc_000000000000_000000000000",
		},
		{
			name:         "Basic POST - No cookies",
			method:       "POST",
			url:          "http://example.com",
			cookies:      map[string]string{},
			headers:      map[string]string{},
			expectedHash: "po11nn000000_e3b0c44298fc_000000000000_000000000000",
		},
		{
			name:   "GET - With cookies",
			method: "GET",
			url:    "http://example.com/foobar",
			cookies: map[string]string{
				"foo": "bar",
				"baz": "qux",
			},
			headers: map[string]string{
				"User-Agent": "Mozilla/5.0",
			},
			expectedHash: "ge11cn010000_b8bcd45ac095_bd87575d11f6_d401f362552e",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			req, err := http.NewRequestWithContext(ctx, test.method, test.url, http.NoBody)
			if err != nil {
				t.Fatalf("Failed to create request: %s", err)
			}

			for key, value := range test.cookies {
				req.AddCookie(&http.Cookie{
					Name:  key,
					Value: value,
				})
			}

			for key, value := range test.headers {
				req.Header.Add(key, value)
			}

			hash, err := JA4H(req)
			require.NoError(t, err)

			if hash != test.expectedHash {
				t.Fatalf("JA4H returned unexpected hash: %s", hash)
			}
		})
	}

}
