package httpz

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"net/http"
	"net/textproto"
	"net/url"
	"slices"
	"strings"
	"testing"

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

const (
	// definedHeadersFilePath defines the filename where the `*Header` constants are defined.
	definedHeadersFilePath = "tools.go"
	// expectedNumberOfDefinedHeaders defines the number of all expected `*Header` constants in tools.go
	// This is used in the TestHeaderConstantsCanonicalKeys unit test to make sure we are checking the
	// right amount of header names.
	expectedNumberOfDefinedHeaders = 58
)

func TestMergeURLPathAndQuery(t *testing.T) {
	tests := []struct {
		baseURL   string
		extraPath string
		query     url.Values
		expected  string
	}{
		{
			baseURL:  "/ok",
			expected: "/ok",
		},
		{
			baseURL:   "/ok",
			extraPath: "/abc",
			expected:  "/ok/abc",
		},
		{
			baseURL:   "/ok",
			extraPath: "/abc/",
			expected:  "/ok/abc/",
		},
		{
			baseURL:   "/ok/",
			extraPath: "/abc/",
			expected:  "/ok/abc/",
		},
		{
			baseURL:   "/ok",
			extraPath: "abc",
			expected:  "/ok/abc",
		},
		{
			baseURL:   "/ok/",
			extraPath: "abc",
			expected:  "/ok/abc",
		},
		{
			baseURL:   "/ok?a=b",
			extraPath: "/abc",
			expected:  "/ok/abc?a=b",
		},
		{
			baseURL:   "/ok",
			extraPath: "/abc",
			query: url.Values{
				"c": {"d", "e"},
			},
			expected: "/ok/abc?c=d&c=e",
		},
		{
			baseURL:   "/ok?a=b",
			extraPath: "/abc",
			query: url.Values{
				"c": {"d", "e"},
			},
			expected: "/ok/abc?a=b&c=d&c=e",
		},
		{
			baseURL:   "/ok?a=b",
			extraPath: "/abc",
			query: url.Values{
				"a": {"d", "e"},
			},
			expected: "/ok/abc?a=d&a=e", // override values, don't merge
		},
	}

	for i, tc := range tests {
		t.Run(fmt.Sprintf("%d - %s", i, tc.expected), func(t *testing.T) {
			base, err := url.Parse(tc.baseURL)
			require.NoError(t, err)
			actual := MergeURLPathAndQuery(base, tc.extraPath, tc.query)
			assert.Equal(t, tc.expected, actual)
		})
		t.Run(fmt.Sprintf("%d - %s", i, "example.com"+tc.expected), func(t *testing.T) {
			base, err := url.Parse("example.com" + tc.baseURL)
			require.NoError(t, err)
			actual := MergeURLPathAndQuery(base, tc.extraPath, tc.query)
			assert.Equal(t, "example.com"+tc.expected, actual)
		})
	}
}

func TestHeaderConstantsCanonicalKeys(t *testing.T) {
	// GIVEN
	definedHeaderConstants := retrieveDefinedHeaderConstants(t)

	// THEN
	assert.Lenf(
		t, definedHeaderConstants, expectedNumberOfDefinedHeaders,
		"Expected different amount of defined header constants: want %d, got %d", expectedNumberOfDefinedHeaders, len(definedHeaderConstants),
	)

	for name, actualHeaderValue := range definedHeaderConstants {
		expectedHeaderValue := textproto.CanonicalMIMEHeaderKey(actualHeaderValue)
		assert.Equalf(
			t,
			expectedHeaderValue,
			actualHeaderValue,
			"Constant %s has non-canonical header key: want %q, got %q", name, expectedHeaderValue, actualHeaderValue,
		)
	}
}

func TestHeaderValues(t *testing.T) {
	// GIVEN
	tests := []struct {
		name   string
		input  http.Header
		output []string
	}{
		{
			name:   "empty",
			input:  http.Header{},
			output: nil,
		},
		{
			name:   "no values",
			input:  http.Header{"Test": []string{}},
			output: nil,
		},
		{
			name:   "two elements, one field each",
			input:  http.Header{"Test": []string{"foo", "bar"}},
			output: []string{"foo", "bar"},
		},
		{
			name:   "one element, two values",
			input:  http.Header{"Test": []string{"foo, bar"}},
			output: []string{"foo", "bar"},
		},
		{
			name:   "one element with two values, one element with one value",
			input:  http.Header{"Test": []string{"foo, bar", "baz"}},
			output: []string{"foo", "bar", "baz"},
		},
		{
			name:   "mixed",
			input:  http.Header{"Test": []string{"foo, bar", "baz", "foobar, barbaz"}},
			output: []string{"foo", "bar", "baz", "foobar", "barbaz"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			actualOutput := slices.Collect(HeaderValues(tc.input, "Test"))

			assert.Equal(t, tc.output, actualOutput)
		})
	}
}

func TestHasHeaderValue(t *testing.T) {
	// GIVEN
	tests := []struct {
		name      string
		input     http.Header
		expectHas bool
	}{
		{
			name:      "empty",
			input:     http.Header{},
			expectHas: false,
		},
		{
			name:      "no values",
			input:     http.Header{"Haystack": []string{}},
			expectHas: false,
		},
		{
			name:      "first item",
			input:     http.Header{"Haystack": []string{"needle", "bar"}},
			expectHas: true,
		},
		{
			name:      "second item",
			input:     http.Header{"Haystack": []string{"bar", "needle"}},
			expectHas: true,
		},
		{
			name:      "first item with comma",
			input:     http.Header{"Haystack": []string{"needle, bar"}},
			expectHas: true,
		},
		{
			name:      "second item with comma",
			input:     http.Header{"Haystack": []string{"bar, needle"}},
			expectHas: true,
		},
		{
			name:      "second item in second comma list",
			input:     http.Header{"Haystack": []string{"foo, bar", "baz, needle"}},
			expectHas: true,
		},
		{
			name:      "not found",
			input:     http.Header{"Haystack": []string{"foo, bar", "baz", "foobar, barbaz"}},
			expectHas: false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			actualHas := HasHeaderValue(tc.input, "Haystack", "needle")

			assert.Equal(t, tc.expectHas, actualHas)
		})
	}
}

func TestRemoveHeaderValue(t *testing.T) {
	// GIVEN
	tests := []struct {
		name   string
		input  http.Header
		output http.Header
	}{
		{
			name:   "empty",
			input:  http.Header{},
			output: http.Header{},
		},
		{
			name:   "no values",
			input:  http.Header{"Haystack": []string{}},
			output: http.Header{},
		},
		{
			name:   "first item",
			input:  http.Header{"Haystack": []string{"needle", "bar"}},
			output: http.Header{"Haystack": []string{"bar"}},
		},
		{
			name:   "second item",
			input:  http.Header{"Haystack": []string{"bar", "needle"}},
			output: http.Header{"Haystack": []string{"bar"}},
		},
		{
			name:   "first item with comma",
			input:  http.Header{"Haystack": []string{"needle, bar"}},
			output: http.Header{"Haystack": []string{"bar"}},
		},
		{
			name:   "second item with comma",
			input:  http.Header{"Haystack": []string{"bar, needle"}},
			output: http.Header{"Haystack": []string{"bar"}},
		},
		{
			name:   "second item in second comma list",
			input:  http.Header{"Haystack": []string{"foo, bar", "baz, needle"}},
			output: http.Header{"Haystack": []string{"foo", "bar", "baz"}},
		},
		{
			name:   "not found",
			input:  http.Header{"Haystack": []string{"foo, bar", "baz", "foobar, barbaz"}},
			output: http.Header{"Haystack": []string{"foo", "bar", "baz", "foobar", "barbaz"}},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			RemoveHeaderValue(tc.input, "Haystack", func(s string) bool { return s == "needle" })

			assert.Equal(t, tc.output, tc.input)
		})
	}
}

func TestHasContentType(t *testing.T) {
	// GIVEN
	tests := []struct {
		name     string
		h        http.Header
		expected bool
	}{
		{
			name:     "nil headers",
			h:        nil,
			expected: false,
		},
		{
			name:     "no headers",
			h:        make(http.Header),
			expected: false,
		},
		{
			name:     "missing content-type header",
			h:        http.Header{"Another-Header": []string{"value"}},
			expected: false,
		},
		{
			name:     "no content-type header values",
			h:        http.Header{ContentTypeHeader: []string{}},
			expected: false,
		},
		{
			name:     "empty content-type header",
			h:        http.Header{ContentTypeHeader: []string{""}},
			expected: true,
		},
		{
			name:     "has content-type header",
			h:        http.Header{ContentTypeHeader: []string{"text/plain"}},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// WHEN
			actual := HasContentType(tt.h)

			// THEN
			assert.Equal(t, tt.expected, actual)
		})
	}
}

// retrieveDefinedHeaderConstants retrieves all the `*Header` constants from the definedHeadersFilePath file
// and returns a map[<header constant name>]<header constant value>.
func retrieveDefinedHeaderConstants(t *testing.T) map[string]string {
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, definedHeadersFilePath, nil, parser.AllErrors)
	require.NoError(t, err)

	headers := make(map[string]string, expectedNumberOfDefinedHeaders)

	ast.Inspect(file, func(n ast.Node) bool {
		genDecl, ok := n.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.CONST {
			return true
		}

		for _, spec := range genDecl.Specs {
			valueSpec, ok := spec.(*ast.ValueSpec)
			if !ok {
				continue
			}

			for i, name := range valueSpec.Names {
				if !name.IsExported() || !strings.HasSuffix(name.Name, "Header") {
					continue
				}

				basicLit, ok := valueSpec.Values[i].(*ast.BasicLit)
				require.True(t, ok && basicLit.Kind == token.STRING, "Constant %q is not of the right kind: want %q, got %q", token.STRING, basicLit.Kind)

				// trim the quotes
				actualHeaderValue := basicLit.Value[1 : len(basicLit.Value)-1]
				headers[name.Name] = actualHeaderValue
			}
		}
		return false
	})

	return headers
}
