package http

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
)

func TestClientIPMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid ClientIP matcher",
			rule:          "ClientIP(`1`)",
			expectedError: true,
		},
		{
			desc:          "invalid ClientIP matcher (no parameter)",
			rule:          "ClientIP()",
			expectedError: true,
		},
		{
			desc:          "invalid ClientIP matcher (empty parameter)",
			rule:          "ClientIP(``)",
			expectedError: true,
		},
		{
			desc:          "invalid ClientIP matcher (too many parameters)",
			rule:          "ClientIP(`127.0.0.1`, `192.168.1.0/24`)",
			expectedError: true,
		},
		{
			desc: "valid ClientIP matcher",
			rule: "ClientIP(`127.0.0.1`)",
			expected: map[string]int{
				"127.0.0.1":   http.StatusOK,
				"192.168.1.1": http.StatusNotFound,
			},
		},
		{
			desc: "valid ClientIP matcher but invalid remote address",
			rule: "ClientIP(`127.0.0.1`)",
			expected: map[string]int{
				"1": http.StatusNotFound,
			},
		},
		{
			desc: "valid ClientIP matcher using CIDR",
			rule: "ClientIP(`192.168.1.0/24`)",
			expected: map[string]int{
				"192.168.1.1":   http.StatusOK,
				"192.168.1.100": http.StatusOK,
				"192.168.2.1":   http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			results := make(map[string]int)
			for remoteAddr := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
				req.RemoteAddr = remoteAddr

				muxer.ServeHTTP(w, req)
				results[remoteAddr] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestMethodMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid Method matcher (no parameter)",
			rule:          "Method()",
			expectedError: true,
		},
		{
			desc:          "invalid Method matcher (empty parameter)",
			rule:          "Method(``)",
			expectedError: true,
		},
		{
			desc:          "invalid Method matcher (too many parameters)",
			rule:          "Method(`GET`, `POST`)",
			expectedError: true,
		},
		{
			desc: "valid Method matcher",
			rule: "Method(`GET`)",
			expected: map[string]int{
				http.MethodGet:                  http.StatusOK,
				http.MethodPost:                 http.StatusNotFound,
				strings.ToLower(http.MethodGet): http.StatusNotFound,
			},
		},
		{
			desc: "valid Method matcher (lower case)",
			rule: "Method(`get`)",
			expected: map[string]int{
				http.MethodGet:                  http.StatusOK,
				http.MethodPost:                 http.StatusNotFound,
				strings.ToLower(http.MethodGet): http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			results := make(map[string]int)
			for method := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(method, "https://example.com", http.NoBody)

				muxer.ServeHTTP(w, req)
				results[method] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestHostMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid Host matcher (no parameter)",
			rule:          "Host()",
			expectedError: true,
		},
		{
			desc:          "invalid Host matcher (empty parameter)",
			rule:          "Host(``)",
			expectedError: true,
		},
		{
			desc:          "invalid Host matcher (non-ASCII)",
			rule:          "Host(`🦭.com`)",
			expectedError: true,
		},
		{
			desc:          "invalid Host matcher (too many parameters)",
			rule:          "Host(`example.com`, `example.org`)",
			expectedError: true,
		},
		{
			desc: "valid Host matcher",
			rule: "Host(`example.com`)",
			expected: map[string]int{
				"https://example.com":      http.StatusOK,
				"https://example.com:8080": http.StatusOK,
				"https://example.com/path": http.StatusOK,
				"https://EXAMPLE.COM/path": http.StatusOK,
				"https://example.org":      http.StatusNotFound,
				"https://example.org/path": http.StatusNotFound,
			},
		},
		{
			desc: "valid Host matcher - matcher ending with a dot",
			rule: "Host(`example.com.`)",
			expected: map[string]int{
				"https://example.com":       http.StatusOK,
				"https://example.com/path":  http.StatusOK,
				"https://example.org":       http.StatusNotFound,
				"https://example.org/path":  http.StatusNotFound,
				"https://example.com.":      http.StatusOK,
				"https://example.com./path": http.StatusOK,
				"https://example.org.":      http.StatusNotFound,
				"https://example.org./path": http.StatusNotFound,
			},
		},
		{
			desc: "valid Host matcher - URL ending with a dot",
			rule: "Host(`example.com`)",
			expected: map[string]int{
				"https://example.com.":      http.StatusOK,
				"https://example.com./path": http.StatusOK,
				"https://example.org.":      http.StatusNotFound,
				"https://example.org./path": http.StatusNotFound,
			},
		},
		{
			desc: "valid Host matcher - matcher with UPPER case",
			rule: "Host(`EXAMPLE.COM`)",
			expected: map[string]int{
				"https://example.com":      http.StatusOK,
				"https://example.com/path": http.StatusOK,
				"https://example.org":      http.StatusNotFound,
				"https://example.org/path": http.StatusNotFound,
			},
		},
		{
			desc: "valid Host matcher - puny-coded emoji",
			rule: "Host(`xn--9t9h.com`)",
			expected: map[string]int{
				"https://xn--9t9h.com":      http.StatusOK,
				"https://xn--9t9h.com/path": http.StatusOK,
				"https://example.com":       http.StatusNotFound,
				"https://example.com/path":  http.StatusNotFound,
				// The request's sender must use puny-code.
				"https://🦭.com": http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			// RequestDecorator is necessary for the Host matcher
			reqHost := requestdecorator.New(nil)

			results := make(map[string]int)
			for calledURL := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)

				reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
				results[calledURL] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestHostRegexpMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid HostRegexp matcher (no parameter)",
			rule:          "HostRegexp()",
			expectedError: true,
		},
		{
			desc:          "invalid HostRegexp matcher (empty parameter)",
			rule:          "HostRegexp(``)",
			expectedError: true,
		},
		{
			desc:          "invalid HostRegexp matcher (non-ASCII)",
			rule:          "HostRegexp(`🦭.com`)",
			expectedError: true,
		},
		{
			desc:          "invalid HostRegexp matcher (invalid regexp)",
			rule:          "HostRegexp(`(example.com`)",
			expectedError: true,
		},
		{
			desc:          "invalid HostRegexp matcher (too many parameters)",
			rule:          "HostRegexp(`example.com`, `example.org`)",
			expectedError: true,
		},
		{
			desc: "valid HostRegexp matcher",
			rule: "HostRegexp(`^[a-zA-Z-]+\\.com$`)",
			expected: map[string]int{
				"https://example.com":      http.StatusOK,
				"https://example.com:8080": http.StatusOK,
				"https://example.com/path": http.StatusOK,
				"https://example.org":      http.StatusNotFound,
				"https://example.org/path": http.StatusNotFound,
			},
		},
		{
			desc: "valid HostRegexp matcher with case sensitive regexp",
			rule: "HostRegexp(`^[A-Z]+\\.com$`)",
			expected: map[string]int{
				"https://example.com":      http.StatusNotFound,
				"https://EXAMPLE.com":      http.StatusNotFound,
				"https://example.com/path": http.StatusNotFound,
				"https://example.org":      http.StatusNotFound,
				"https://example.org/path": http.StatusNotFound,
			},
		},
		{
			desc: "valid HostRegexp matcher with Traefik v2 syntax",
			rule: "HostRegexp(`{domain:[a-zA-Z-]+\\.com}`)",
			expected: map[string]int{
				"https://example.com":      http.StatusNotFound,
				"https://example.com/path": http.StatusNotFound,
				"https://example.org":      http.StatusNotFound,
				"https://example.org/path": http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)

			// RequestDecorator is necessary for the HostRegexp matcher
			reqHost := requestdecorator.New(nil)

			results := make(map[string]int)
			for calledURL := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)

				reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
				results[calledURL] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestPathMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid Path matcher (no parameter)",
			rule:          "Path()",
			expectedError: true,
		},
		{
			desc:          "invalid Path matcher (empty parameter)",
			rule:          "Path(``)",
			expectedError: true,
		},
		{
			desc:          "invalid Path matcher (no leading /)",
			rule:          "Path(`css`)",
			expectedError: true,
		},
		{
			desc:          "invalid Path matcher (too many parameters)",
			rule:          "Path(`/css`, `/js`)",
			expectedError: true,
		},
		{
			desc: "valid Path matcher",
			rule: "Path(`/css`)",
			expected: map[string]int{
				"https://example.com":              http.StatusNotFound,
				"https://example.com/html":         http.StatusNotFound,
				"https://example.org/css":          http.StatusOK,
				"https://example.com/css":          http.StatusOK,
				"https://example.com/css/":         http.StatusNotFound,
				"https://example.com/css/main.css": http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			results := make(map[string]int)
			for calledURL := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)

				muxer.ServeHTTP(w, req)
				results[calledURL] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestPathRegexpMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid PathRegexp matcher (no parameter)",
			rule:          "PathRegexp()",
			expectedError: true,
		},
		{
			desc:          "invalid PathRegexp matcher (empty parameter)",
			rule:          "PathRegexp(``)",
			expectedError: true,
		},
		{
			desc:          "invalid PathRegexp matcher (invalid regexp)",
			rule:          "PathRegexp(`/(css`)",
			expectedError: true,
		},
		{
			desc:          "invalid PathRegexp matcher (too many parameters)",
			rule:          "PathRegexp(`/css`, `/js`)",
			expectedError: true,
		},
		{
			desc: "valid PathRegexp matcher",
			rule: "PathRegexp(`^/(css|js)`)",
			expected: map[string]int{
				"https://example.com":              http.StatusNotFound,
				"https://example.com/html":         http.StatusNotFound,
				"https://example.org/css":          http.StatusOK,
				"https://example.com/CSS":          http.StatusNotFound,
				"https://example.com/css":          http.StatusOK,
				"https://example.com/css/":         http.StatusOK,
				"https://example.com/css/main.css": http.StatusOK,
				"https://example.com/js":           http.StatusOK,
				"https://example.com/js/":          http.StatusOK,
				"https://example.com/js/main.js":   http.StatusOK,
			},
		},
		{
			desc: "valid PathRegexp matcher with Traefik v2 syntax",
			rule: `PathRegexp("/{path:(css|js)}")`,
			expected: map[string]int{
				"https://example.com":                 http.StatusNotFound,
				"https://example.com/html":            http.StatusNotFound,
				"https://example.org/css":             http.StatusNotFound,
				"https://example.com/{path:css}":      http.StatusOK,
				"https://example.com/{path:css}/":     http.StatusOK,
				"https://example.com/%7Bpath:css%7D":  http.StatusOK,
				"https://example.com/%7Bpath:css%7D/": http.StatusOK,
				"https://example.com/{path:js}":       http.StatusOK,
				"https://example.com/{path:js}/":      http.StatusOK,
				"https://example.com/%7Bpath:js%7D":   http.StatusOK,
				"https://example.com/%7Bpath:js%7D/":  http.StatusOK,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			results := make(map[string]int)
			for calledURL := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)

				muxer.ServeHTTP(w, req)
				results[calledURL] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestPathPrefixMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid PathPrefix matcher (no parameter)",
			rule:          "PathPrefix()",
			expectedError: true,
		},
		{
			desc:          "invalid PathPrefix matcher (empty parameter)",
			rule:          "PathPrefix(``)",
			expectedError: true,
		},
		{
			desc:          "invalid PathPrefix matcher (no leading /)",
			rule:          "PathPrefix(`css`)",
			expectedError: true,
		},
		{
			desc:          "invalid PathPrefix matcher (too many parameters)",
			rule:          "PathPrefix(`/css`, `/js`)",
			expectedError: true,
		},
		{
			desc: "valid PathPrefix matcher",
			rule: `PathPrefix("/css")`,
			expected: map[string]int{
				"https://example.com":              http.StatusNotFound,
				"https://example.com/html":         http.StatusNotFound,
				"https://example.org/css":          http.StatusOK,
				"https://example.com/css":          http.StatusOK,
				"https://example.com/css/":         http.StatusOK,
				"https://example.com/css/main.css": http.StatusOK,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			results := make(map[string]int)
			for calledURL := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)

				muxer.ServeHTTP(w, req)
				results[calledURL] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestHeaderMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[*http.Header]int
		expectedError bool
	}{
		{
			desc:          "invalid Header matcher (no parameter)",
			rule:          "Header()",
			expectedError: true,
		},
		{
			desc:          "invalid Header matcher (missing value parameter)",
			rule:          "Header(`X-Forwarded-Host`)",
			expectedError: true,
		},
		{
			desc:          "invalid Header matcher (missing value parameter)",
			rule:          "Header(`X-Forwarded-Host`, ``)",
			expectedError: true,
		},
		{
			desc:          "invalid Header matcher (missing key parameter)",
			rule:          "Header(``, `example.com`)",
			expectedError: true,
		},
		{
			desc:          "invalid Header matcher (too many parameters)",
			rule:          "Header(`X-Forwarded-Host`, `example.com`, `example.org`)",
			expectedError: true,
		},
		{
			desc: "valid Header matcher",
			rule: "Header(`X-Forwarded-Proto`, `https`)",
			expected: map[*http.Header]int{
				{"X-Forwarded-Proto": []string{"https"}}:         http.StatusOK,
				{"x-forwarded-proto": []string{"https"}}:         http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"http", "https"}}: http.StatusOK,
				{"X-Forwarded-Proto": []string{"https", "http"}}: http.StatusOK,
				{"X-Forwarded-Host": []string{"example.com"}}:    http.StatusNotFound,
			},
		},
		{
			desc: "valid Header matcher (non-canonical form)",
			rule: "Header(`x-forwarded-proto`, `https`)",
			expected: map[*http.Header]int{
				{"X-Forwarded-Proto": []string{"https"}}:         http.StatusOK,
				{"x-forwarded-proto": []string{"https"}}:         http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"http", "https"}}: http.StatusOK,
				{"X-Forwarded-Proto": []string{"https", "http"}}: http.StatusOK,
				{"X-Forwarded-Host": []string{"example.com"}}:    http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test

		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			for headers := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
				req.Header = *headers

				muxer.ServeHTTP(w, req)
				assert.Equal(t, test.expected[headers], w.Code, headers)
			}
		})
	}
}

func TestHeaderRegexpMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[*http.Header]int
		expectedError bool
	}{
		{
			desc:          "invalid HeaderRegexp matcher (no parameter)",
			rule:          "HeaderRegexp()",
			expectedError: true,
		},
		{
			desc:          "invalid HeaderRegexp matcher (missing value parameter)",
			rule:          "HeaderRegexp(`X-Forwarded-Host`)",
			expectedError: true,
		},
		{
			desc:          "invalid HeaderRegexp matcher (missing value parameter)",
			rule:          "HeaderRegexp(`X-Forwarded-Host`, ``)",
			expectedError: true,
		},
		{
			desc:          "invalid HeaderRegexp matcher (missing key parameter)",
			rule:          "HeaderRegexp(``, `example.com`)",
			expectedError: true,
		},
		{
			desc:          "invalid HeaderRegexp matcher (invalid regexp)",
			rule:          "HeaderRegexp(`X-Forwarded-Host`,`(example.com`)",
			expectedError: true,
		},
		{
			desc:          "invalid HeaderRegexp matcher (too many parameters)",
			rule:          "HeaderRegexp(`X-Forwarded-Host`, `example.com`, `example.org`)",
			expectedError: true,
		},
		{
			desc: "valid HeaderRegexp matcher",
			rule: "HeaderRegexp(`X-Forwarded-Proto`, `^https?$`)",
			expected: map[*http.Header]int{
				{"X-Forwarded-Proto": []string{"http"}}:        http.StatusOK,
				{"x-forwarded-proto": []string{"http"}}:        http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"https"}}:       http.StatusOK,
				{"X-Forwarded-Proto": []string{"HTTPS"}}:       http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"ws", "https"}}: http.StatusOK,
				{"X-Forwarded-Host": []string{"example.com"}}:  http.StatusNotFound,
			},
		},
		{
			desc: "valid HeaderRegexp matcher (non-canonical form)",
			rule: "HeaderRegexp(`x-forwarded-proto`, `^https?$`)",
			expected: map[*http.Header]int{
				{"X-Forwarded-Proto": []string{"http"}}:        http.StatusOK,
				{"x-forwarded-proto": []string{"http"}}:        http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"https"}}:       http.StatusOK,
				{"X-Forwarded-Proto": []string{"HTTPS"}}:       http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"ws", "https"}}: http.StatusOK,
				{"X-Forwarded-Host": []string{"example.com"}}:  http.StatusNotFound,
			},
		},
		{
			desc: "valid HeaderRegexp matcher with Traefik v2 syntax",
			rule: "HeaderRegexp(`X-Forwarded-Proto`, `http{secure:s?}`)",
			expected: map[*http.Header]int{
				{"X-Forwarded-Proto": []string{"http"}}:                 http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"https"}}:                http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"http{secure:}"}}:        http.StatusOK,
				{"X-Forwarded-Proto": []string{"HTTP{secure:}"}}:        http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"http{secure:s}"}}:       http.StatusOK,
				{"X-Forwarded-Proto": []string{"http{secure:S}"}}:       http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"HTTPS"}}:                http.StatusNotFound,
				{"X-Forwarded-Proto": []string{"ws", "http{secure:s}"}}: http.StatusOK,
				{"X-Forwarded-Host": []string{"example.com"}}:           http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			for headers := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
				req.Header = *headers

				muxer.ServeHTTP(w, req)
				assert.Equal(t, test.expected[headers], w.Code, *headers)
			}
		})
	}
}

func TestQueryMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid Query matcher (no parameter)",
			rule:          "Query()",
			expectedError: true,
		},
		{
			desc:          "invalid Query matcher (empty key, one parameter)",
			rule:          "Query(``)",
			expectedError: true,
		},
		{
			desc:          "invalid Query matcher (empty key)",
			rule:          "Query(``, `traefik`)",
			expectedError: true,
		},
		{
			desc:          "invalid Query matcher (empty value)",
			rule:          "Query(`q`, ``)",
			expectedError: true,
		},
		{
			desc:          "invalid Query matcher (too many parameters)",
			rule:          "Query(`q`, `traefik`, `proxy`)",
			expectedError: true,
		},
		{
			desc: "valid Query matcher",
			rule: "Query(`q`, `traefik`)",
			expected: map[string]int{
				"https://example.com":                     http.StatusNotFound,
				"https://example.com?q=traefik":           http.StatusOK,
				"https://example.com?rel=ddg&q=traefik":   http.StatusOK,
				"https://example.com?q=traefik&q=proxy":   http.StatusOK,
				"https://example.com?q=awesome&q=traefik": http.StatusOK,
				"https://example.com?q=nginx":             http.StatusNotFound,
				"https://example.com?rel=ddg":             http.StatusNotFound,
				"https://example.com?q=TRAEFIK":           http.StatusNotFound,
				"https://example.com?Q=traefik":           http.StatusNotFound,
				"https://example.com?rel=traefik":         http.StatusNotFound,
			},
		},
		{
			desc: "valid Query matcher with empty value",
			rule: "Query(`mobile`)",
			expected: map[string]int{
				"https://example.com":             http.StatusNotFound,
				"https://example.com?mobile":      http.StatusOK,
				"https://example.com?mobile=true": http.StatusNotFound,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			results := make(map[string]int)
			for calledURL := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)

				muxer.ServeHTTP(w, req)
				results[calledURL] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}

func TestQueryRegexpMatcher(t *testing.T) {
	testCases := []struct {
		desc          string
		rule          string
		expected      map[string]int
		expectedError bool
	}{
		{
			desc:          "invalid QueryRegexp matcher (no parameter)",
			rule:          "QueryRegexp()",
			expectedError: true,
		},
		{
			desc:          "invalid QueryRegexp matcher (empty parameter)",
			rule:          "QueryRegexp(``)",
			expectedError: true,
		},
		{
			desc:          "invalid QueryRegexp matcher (invalid regexp)",
			rule:          "QueryRegexp(`q`, `(traefik`)",
			expectedError: true,
		},
		{
			desc:          "invalid QueryRegexp matcher (too many parameters)",
			rule:          "QueryRegexp(`q`, `traefik`, `proxy`)",
			expectedError: true,
		},
		{
			desc: "valid QueryRegexp matcher",
			rule: "QueryRegexp(`q`, `^(traefik|nginx)$`)",
			expected: map[string]int{
				"https://example.com":                     http.StatusNotFound,
				"https://example.com?q=traefik":           http.StatusOK,
				"https://example.com?rel=ddg&q=traefik":   http.StatusOK,
				"https://example.com?q=traefik&q=proxy":   http.StatusOK,
				"https://example.com?q=awesome&q=traefik": http.StatusOK,
				"https://example.com?q=TRAEFIK":           http.StatusNotFound,
				"https://example.com?Q=traefik":           http.StatusNotFound,
				"https://example.com?rel=traefik":         http.StatusNotFound,
				"https://example.com?q=nginx":             http.StatusOK,
				"https://example.com?rel=ddg&q=nginx":     http.StatusOK,
				"https://example.com?q=nginx&q=proxy":     http.StatusOK,
				"https://example.com?q=awesome&q=nginx":   http.StatusOK,
				"https://example.com?q=NGINX":             http.StatusNotFound,
				"https://example.com?Q=nginx":             http.StatusNotFound,
				"https://example.com?rel=nginx":           http.StatusNotFound,
				"https://example.com?q=haproxy":           http.StatusNotFound,
				"https://example.com?rel=ddg":             http.StatusNotFound,
			},
		},
		{
			desc: "valid QueryRegexp matcher",
			rule: "QueryRegexp(`q`, `^.*$`)",
			expected: map[string]int{
				"https://example.com":                     http.StatusNotFound,
				"https://example.com?q=traefik":           http.StatusOK,
				"https://example.com?rel=ddg&q=traefik":   http.StatusOK,
				"https://example.com?q=traefik&q=proxy":   http.StatusOK,
				"https://example.com?q=awesome&q=traefik": http.StatusOK,
				"https://example.com?q=TRAEFIK":           http.StatusOK,
				"https://example.com?Q=traefik":           http.StatusNotFound,
				"https://example.com?rel=traefik":         http.StatusNotFound,
				"https://example.com?q=nginx":             http.StatusOK,
				"https://example.com?rel=ddg&q=nginx":     http.StatusOK,
				"https://example.com?q=nginx&q=proxy":     http.StatusOK,
				"https://example.com?q=awesome&q=nginx":   http.StatusOK,
				"https://example.com?q=NGINX":             http.StatusOK,
				"https://example.com?Q=nginx":             http.StatusNotFound,
				"https://example.com?rel=nginx":           http.StatusNotFound,
				"https://example.com?q=haproxy":           http.StatusOK,
				"https://example.com?rel=ddg":             http.StatusNotFound,
			},
		},
		{
			desc: "valid QueryRegexp matcher with Traefik v2 syntax",
			rule: "QueryRegexp(`q`, `{value:(traefik|nginx)}`)",
			expected: map[string]int{
				"https://example.com?q=traefik":         http.StatusNotFound,
				"https://example.com?q={value:traefik}": http.StatusOK,
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			muxer, err := NewMuxer()
			require.NoError(t, err)

			err = muxer.AddRoute(test.rule, "", 0, handler)
			if test.expectedError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			results := make(map[string]int)
			for calledURL := range test.expected {
				w := httptest.NewRecorder()

				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)

				muxer.ServeHTTP(w, req)
				results[calledURL] = w.Code
			}
			assert.Equal(t, test.expected, results)
		})
	}
}
