package router

import (
	"context"
	"crypto/tls"
	"io"
	"math"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/containous/alice"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	ptypes "github.com/traefik/paerser/types"
	"github.com/traefik/traefik/v3/pkg/config/dynamic"
	"github.com/traefik/traefik/v3/pkg/config/runtime"
	"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
	httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
	"github.com/traefik/traefik/v3/pkg/server/middleware"
	"github.com/traefik/traefik/v3/pkg/server/service"
	"github.com/traefik/traefik/v3/pkg/testhelpers"
	traefiktls "github.com/traefik/traefik/v3/pkg/tls"
)

func TestRouterManager_Get(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))

	t.Cleanup(func() { server.Close() })

	type expectedResult struct {
		StatusCode     int
		RequestHeaders map[string]string
	}

	testCases := []struct {
		desc              string
		routersConfig     map[string]*dynamic.Router
		serviceConfig     map[string]*dynamic.Service
		middlewaresConfig map[string]*dynamic.Middleware
		entryPoints       []string
		expected          expectedResult
	}{
		{
			desc: "no middleware",
			routersConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			entryPoints: []string{"web"},
			expected:    expectedResult{StatusCode: http.StatusOK},
		},
		{
			desc: "empty host",
			routersConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(``)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			entryPoints: []string{"web"},
			expected:    expectedResult{StatusCode: http.StatusNotFound},
		},
		{
			desc: "no load balancer",
			routersConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {},
			},
			entryPoints: []string{"web"},
			expected:    expectedResult{StatusCode: http.StatusNotFound},
		},
		{
			desc: "no middleware, no matching",
			routersConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`bar.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			entryPoints: []string{"web"},
			expected:    expectedResult{StatusCode: http.StatusNotFound},
		},
		{
			desc: "middleware: headers > auth",
			routersConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Middlewares: []string{"headers-middle", "auth-middle"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			middlewaresConfig: map[string]*dynamic.Middleware{
				"auth-middle": {
					BasicAuth: &dynamic.BasicAuth{
						Users: []string{"toto:titi"},
					},
				},
				"headers-middle": {
					Headers: &dynamic.Headers{
						CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
					},
				},
			},
			entryPoints: []string{"web"},
			expected: expectedResult{
				StatusCode: http.StatusUnauthorized,
				RequestHeaders: map[string]string{
					"X-Apero": "beer",
				},
			},
		},
		{
			desc: "middleware: auth > header",
			routersConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Middlewares: []string{"auth-middle", "headers-middle"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			middlewaresConfig: map[string]*dynamic.Middleware{
				"auth-middle": {
					BasicAuth: &dynamic.BasicAuth{
						Users: []string{"toto:titi"},
					},
				},
				"headers-middle": {
					Headers: &dynamic.Headers{
						CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
					},
				},
			},
			entryPoints: []string{"web"},
			expected: expectedResult{
				StatusCode: http.StatusUnauthorized,
				RequestHeaders: map[string]string{
					"X-Apero": "",
				},
			},
		},
		{
			desc: "no middleware with provider name",
			routersConfig: map[string]*dynamic.Router{
				"foo@provider-1": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service@provider-1": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			entryPoints: []string{"web"},
			expected:    expectedResult{StatusCode: http.StatusOK},
		},
		{
			desc: "no middleware with specified provider name",
			routersConfig: map[string]*dynamic.Router{
				"foo@provider-1": {
					EntryPoints: []string{"web"},
					Service:     "foo-service@provider-2",
					Rule:        "Host(`foo.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service@provider-2": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			entryPoints: []string{"web"},
			expected:    expectedResult{StatusCode: http.StatusOK},
		},
		{
			desc: "middleware: chain with provider name",
			routersConfig: map[string]*dynamic.Router{
				"foo@provider-1": {
					EntryPoints: []string{"web"},
					Middlewares: []string{"chain-middle@provider-2", "headers-middle"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"foo-service@provider-1": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: server.URL,
							},
						},
					},
				},
			},
			middlewaresConfig: map[string]*dynamic.Middleware{
				"chain-middle@provider-2": {
					Chain: &dynamic.Chain{Middlewares: []string{"auth-middle"}},
				},
				"auth-middle@provider-2": {
					BasicAuth: &dynamic.BasicAuth{
						Users: []string{"toto:titi"},
					},
				},
				"headers-middle@provider-1": {
					Headers: &dynamic.Headers{
						CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
					},
				},
			},
			entryPoints: []string{"web"},
			expected: expectedResult{
				StatusCode: http.StatusUnauthorized,
				RequestHeaders: map[string]string{
					"X-Apero": "",
				},
			},
		},
	}

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

			rtConf := runtime.NewConfig(dynamic.Configuration{
				HTTP: &dynamic.HTTPConfiguration{
					Services:    test.serviceConfig,
					Routers:     test.routersConfig,
					Middlewares: test.middlewaresConfig,
				},
			})

			transportManager := service.NewTransportManager(nil)
			transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})

			serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
			middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
			tlsManager := traefiktls.NewManager(nil)

			parser, err := httpmuxer.NewSyntaxParser()
			require.NoError(t, err)

			routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})

			handlers := routerManager.BuildHandlers(t.Context(), test.entryPoints, false)

			w := httptest.NewRecorder()
			req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)

			reqHost := requestdecorator.New(nil)
			reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)

			assert.Equal(t, test.expected.StatusCode, w.Code)

			for key, value := range test.expected.RequestHeaders {
				assert.Equal(t, value, req.Header.Get(key))
			}
		})
	}
}

func TestRuntimeConfiguration(t *testing.T) {
	testCases := []struct {
		desc             string
		serviceConfig    map[string]*dynamic.Service
		routerConfig     map[string]*dynamic.Router
		middlewareConfig map[string]*dynamic.Middleware
		tlsOptions       map[string]traefiktls.Options
		expectedError    int
	}{
		{
			desc: "No error",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1:8085",
							},
							{
								URL: "http://127.0.0.1:8086",
							},
						},
						HealthCheck: &dynamic.ServerHealthCheck{
							Interval: ptypes.Duration(500 * time.Millisecond),
							Path:     "/health",
						},
					},
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`bar.foo`)",
				},
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			expectedError: 0,
		},
		{
			desc: "One router with wrong rule",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "WrongRule(`bar.foo`)",
				},
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			expectedError: 1,
		},
		{
			desc: "All router with wrong rule",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "WrongRule(`bar.foo`)",
				},
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "WrongRule(`foo.bar`)",
				},
			},
			expectedError: 2,
		},
		{
			desc: "Router with unknown service",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"foo": {
					EntryPoints: []string{"web"},
					Service:     "wrong-service",
					Rule:        "Host(`bar.foo`)",
				},
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			expectedError: 1,
		},
		{
			desc: "Router with broken service",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: nil,
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
				},
			},
			expectedError: 2,
		},
		{
			desc: "Router with middleware",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			middlewareConfig: map[string]*dynamic.Middleware{
				"auth": {
					BasicAuth: &dynamic.BasicAuth{
						Users: []string{"admin:admin"},
					},
				},
				"addPrefixTest": {
					AddPrefix: &dynamic.AddPrefix{
						Prefix: "/toto",
					},
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
					Middlewares: []string{"auth", "addPrefixTest"},
				},
				"test": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar.other`)",
					Middlewares: []string{"addPrefixTest", "auth"},
				},
			},
		},
		{
			desc: "Router with unknown middleware",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			middlewareConfig: map[string]*dynamic.Middleware{
				"auth": {
					BasicAuth: &dynamic.BasicAuth{
						Users: []string{"admin:admin"},
					},
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
					Middlewares: []string{"unknown"},
				},
			},
			expectedError: 1,
		},
		{
			desc: "Router with broken middleware",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			middlewareConfig: map[string]*dynamic.Middleware{
				"auth": {
					BasicAuth: &dynamic.BasicAuth{
						Users: []string{"foo"},
					},
				},
			},
			routerConfig: map[string]*dynamic.Router{
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
					Middlewares: []string{"auth"},
				},
			},
			expectedError: 2,
		},
		{
			desc: "Router priority exceeding max user-defined priority",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			middlewareConfig: map[string]*dynamic.Middleware{},
			routerConfig: map[string]*dynamic.Router{
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
					Priority:    math.MaxInt,
					TLS:         &dynamic.RouterTLSConfig{},
				},
			},
			tlsOptions:    map[string]traefiktls.Options{},
			expectedError: 1,
		},
		{
			desc: "Router with broken tlsOption",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			middlewareConfig: map[string]*dynamic.Middleware{},
			routerConfig: map[string]*dynamic.Router{
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
					TLS: &dynamic.RouterTLSConfig{
						Options: "broken-tlsOption",
					},
				},
			},
			tlsOptions: map[string]traefiktls.Options{
				"broken-tlsOption": {
					ClientAuth: traefiktls.ClientAuth{
						ClientAuthType: "foobar",
					},
				},
			},
			expectedError: 1,
		},
		{
			desc: "Router with broken default tlsOption",
			serviceConfig: map[string]*dynamic.Service{
				"foo-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers: []dynamic.Server{
							{
								URL: "http://127.0.0.1",
							},
						},
					},
				},
			},
			middlewareConfig: map[string]*dynamic.Middleware{},
			routerConfig: map[string]*dynamic.Router{
				"bar": {
					EntryPoints: []string{"web"},
					Service:     "foo-service",
					Rule:        "Host(`foo.bar`)",
					TLS:         &dynamic.RouterTLSConfig{},
				},
			},
			tlsOptions: map[string]traefiktls.Options{
				"default": {
					ClientAuth: traefiktls.ClientAuth{
						ClientAuthType: "foobar",
					},
				},
			},
			expectedError: 1,
		},
	}
	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			entryPoints := []string{"web"}

			rtConf := runtime.NewConfig(dynamic.Configuration{
				HTTP: &dynamic.HTTPConfiguration{
					Services:    test.serviceConfig,
					Routers:     test.routerConfig,
					Middlewares: test.middlewareConfig,
				},
				TLS: &dynamic.TLSConfiguration{
					Options: test.tlsOptions,
				},
			})

			transportManager := service.NewTransportManager(nil)
			transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})

			serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
			middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
			tlsManager := traefiktls.NewManager(nil)
			tlsManager.UpdateConfigs(t.Context(), nil, test.tlsOptions, nil)

			parser, err := httpmuxer.NewSyntaxParser()
			require.NoError(t, err)

			routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})

			_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
			_ = routerManager.BuildHandlers(t.Context(), entryPoints, true)

			// even though rtConf was passed by argument to the manager builders above,
			// it's ok to use it as the result we check, because everything worth checking
			// can be accessed by pointers in it.
			var allErrors int
			for _, v := range rtConf.Services {
				if v.Err != nil {
					allErrors++
				}
			}
			for _, v := range rtConf.Routers {
				if len(v.Err) > 0 {
					allErrors++
				}
			}
			for _, v := range rtConf.Middlewares {
				if v.Err != nil {
					allErrors++
				}
			}
			assert.Equal(t, test.expectedError, allErrors)
		})
	}
}

func TestProviderOnMiddlewares(t *testing.T) {
	entryPoints := []string{"web"}

	rtConf := runtime.NewConfig(dynamic.Configuration{
		HTTP: &dynamic.HTTPConfiguration{
			Services: map[string]*dynamic.Service{
				"test@file": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Strategy: dynamic.BalancerStrategyWRR,
						Servers:  []dynamic.Server{},
					},
				},
			},
			Routers: map[string]*dynamic.Router{
				"router@file": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`test`)",
					Service:     "test@file",
					Middlewares: []string{"chain@file", "m1"},
				},
				"router@docker": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`test`)",
					Service:     "test@file",
					Middlewares: []string{"chain", "m1@file"},
				},
			},
			Middlewares: map[string]*dynamic.Middleware{
				"chain@file": {
					Chain: &dynamic.Chain{Middlewares: []string{"m1", "m2", "m1@file"}},
				},
				"chain@docker": {
					Chain: &dynamic.Chain{Middlewares: []string{"m1", "m2", "m1@file"}},
				},
				"m1@file":   {AddPrefix: &dynamic.AddPrefix{Prefix: "/m1"}},
				"m2@file":   {AddPrefix: &dynamic.AddPrefix{Prefix: "/m2"}},
				"m1@docker": {AddPrefix: &dynamic.AddPrefix{Prefix: "/m1"}},
				"m2@docker": {AddPrefix: &dynamic.AddPrefix{Prefix: "/m2"}},
			},
		},
	})

	transportManager := service.NewTransportManager(nil)
	transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})

	serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, nil)
	middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
	tlsManager := traefiktls.NewManager(nil)

	parser, err := httpmuxer.NewSyntaxParser()
	require.NoError(t, err)

	routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})

	_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)

	assert.Equal(t, []string{"chain@file", "m1@file"}, rtConf.Routers["router@file"].Middlewares)
	assert.Equal(t, []string{"m1@file", "m2@file", "m1@file"}, rtConf.Middlewares["chain@file"].Chain.Middlewares)
	assert.Equal(t, []string{"chain@docker", "m1@file"}, rtConf.Routers["router@docker"].Middlewares)
	assert.Equal(t, []string{"m1@docker", "m2@docker", "m1@file"}, rtConf.Middlewares["chain@docker"].Chain.Middlewares)
}

func BenchmarkRouterServe(b *testing.B) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))

	b.Cleanup(func() { server.Close() })

	res := &http.Response{
		StatusCode: http.StatusOK,
		Body:       io.NopCloser(strings.NewReader("")),
	}

	routersConfig := map[string]*dynamic.Router{
		"foo": {
			EntryPoints: []string{"web"},
			Service:     "foo-service",
			Rule:        "Host(`foo.bar`) && Path(`/`)",
		},
	}
	serviceConfig := map[string]*dynamic.Service{
		"foo-service": {
			LoadBalancer: &dynamic.ServersLoadBalancer{
				Servers: []dynamic.Server{
					{
						URL: server.URL,
					},
				},
			},
		},
	}
	entryPoints := []string{"web"}

	rtConf := runtime.NewConfig(dynamic.Configuration{
		HTTP: &dynamic.HTTPConfiguration{
			Services:    serviceConfig,
			Routers:     routersConfig,
			Middlewares: map[string]*dynamic.Middleware{},
		},
	})

	serviceManager := service.NewManager(rtConf.Services, nil, nil, staticTransportManager{res}, nil)
	middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
	tlsManager := traefiktls.NewManager(nil)

	parser, err := httpmuxer.NewSyntaxParser()
	require.NoError(b, err)

	routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})

	handlers := routerManager.BuildHandlers(b.Context(), entryPoints, false)

	w := httptest.NewRecorder()
	req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)

	reqHost := requestdecorator.New(nil)
	b.ReportAllocs()
	for b.Loop() {
		reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)
	}
}

func BenchmarkService(b *testing.B) {
	res := &http.Response{
		StatusCode: http.StatusOK,
		Body:       io.NopCloser(strings.NewReader("")),
	}

	serviceConfig := map[string]*dynamic.Service{
		"foo-service": {
			LoadBalancer: &dynamic.ServersLoadBalancer{
				Servers: []dynamic.Server{
					{
						URL: "tchouk",
					},
				},
			},
		},
	}

	rtConf := runtime.NewConfig(dynamic.Configuration{
		HTTP: &dynamic.HTTPConfiguration{
			Services: serviceConfig,
		},
	})

	serviceManager := service.NewManager(rtConf.Services, nil, nil, staticTransportManager{res}, nil)
	w := httptest.NewRecorder()
	req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)

	handler, _ := serviceManager.BuildHTTP(b.Context(), "foo-service")
	b.ReportAllocs()
	for b.Loop() {
		handler.ServeHTTP(w, req)
	}
}

func TestProvidersPrecedence(t *testing.T) {
	// Each provider gets its own service with a fake URL whose host encodes the
	// provider label. labellingProxyBuilder writes the host back as the X-From
	// response header so the test can identify which backend was selected.
	//
	// Service names must be fully qualified ("svc@<provider>") because the
	// router manager qualifies every unqualified name with the provider embedded
	// in the router's own name ("router@<provider>").
	svcFor := func(provider string) *dynamic.Service {
		return &dynamic.Service{
			LoadBalancer: &dynamic.ServersLoadBalancer{
				Strategy: dynamic.BalancerStrategyWRR,
				Servers:  []dynamic.Server{{URL: "http://" + provider}},
			},
		}
	}

	testCases := []struct {
		desc                string
		providersPrecedence []string
		routersConfig       map[string]*dynamic.Router
		serviceConfig       map[string]*dynamic.Service
		expectedFrom        string
	}{
		{
			desc:                "kubernetescrd beats kubernetes when listed after it",
			providersPrecedence: []string{"kubernetescrd", "kubernetes"},
			routersConfig: map[string]*dynamic.Router{
				// Service names are bare; the manager qualifies them with the
				// provider extracted from the router key (@kubernetes / @kubernetescrd).
				"router@kubernetes": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Service:     "svc",
				},
				"router@kubernetescrd": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Service:     "svc",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"svc@kubernetes":    svcFor("kubernetes"),
				"svc@kubernetescrd": svcFor("kubernetescrd"),
			},
			expectedFrom: "kubernetescrd",
		},
		{
			desc:                "kubernetes beats kubernetescrd when listed after it",
			providersPrecedence: []string{"kubernetes", "kubernetescrd"},
			routersConfig: map[string]*dynamic.Router{
				"router@kubernetes": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Service:     "svc",
				},
				"router@kubernetescrd": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Service:     "svc",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"svc@kubernetes":    svcFor("kubernetes"),
				"svc@kubernetescrd": svcFor("kubernetescrd"),
			},
			expectedFrom: "kubernetes",
		},
		{
			desc:                "higher numeric priority wins regardless of providersPrecedence",
			providersPrecedence: []string{"kubernetescrd", "kubernetes"},
			routersConfig: map[string]*dynamic.Router{
				"router@kubernetes": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Priority:    100,
					Service:     "svc",
				},
				"router@kubernetescrd": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Priority:    10,
					Service:     "svc",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"svc@kubernetes":    svcFor("kubernetes"),
				"svc@kubernetescrd": svcFor("kubernetescrd"),
			},
			expectedFrom: "kubernetes",
		},
		{
			desc:                "provider not in providersPrecedence loses to any listed provider",
			providersPrecedence: []string{"kubernetes"},
			routersConfig: map[string]*dynamic.Router{
				"router@file": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Service:     "svc",
				},
				"router@kubernetes": {
					EntryPoints: []string{"web"},
					Rule:        "Host(`foo.bar`)",
					Service:     "svc",
				},
			},
			serviceConfig: map[string]*dynamic.Service{
				"svc@file":       svcFor("file"),
				"svc@kubernetes": svcFor("kubernetes"),
			},
			expectedFrom: "kubernetes",
		},
	}

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

			rtConf := runtime.NewConfig(dynamic.Configuration{
				HTTP: &dynamic.HTTPConfiguration{
					Services:    test.serviceConfig,
					Routers:     test.routersConfig,
					Middlewares: map[string]*dynamic.Middleware{},
				},
			})

			transportManager := service.NewTransportManager(nil)
			transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})

			serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, labellingProxyBuilder{})
			middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
			tlsManager := traefiktls.NewManager(nil)

			parser, err := httpmuxer.NewSyntaxParser()
			require.NoError(t, err)

			routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, test.providersPrecedence)
			handlers := routerManager.BuildHandlers(t.Context(), []string{"web"}, false)

			w := httptest.NewRecorder()
			req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
			requestdecorator.New(nil).ServeHTTP(w, req, handlers["web"].ServeHTTP)

			assert.Equal(t, http.StatusOK, w.Code)
			assert.Equal(t, test.expectedFrom, w.Header().Get("X-From"), "wrong provider won the route")
		})
	}
}

func TestManager_ComputeMultiLayerRouting(t *testing.T) {
	testCases := []struct {
		desc                string
		routers             map[string]*dynamic.Router
		expectedStatuses    map[string]string
		expectedChildRefs   map[string][]string
		expectedErrors      map[string][]string
		expectedErrorCounts map[string]int
	}{
		{
			desc: "Simple router",
			routers: map[string]*dynamic.Router{
				"A": {
					Service: "A-service",
				},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusEnabled,
			},
			expectedChildRefs: map[string][]string{
				"A": {},
			},
		},
		{
			// A->B1
			//  ->B2
			desc: "Router with two children",
			routers: map[string]*dynamic.Router{
				"A": {},
				"B1": {
					ParentRefs: []string{"A"},
					Service:    "B1-service",
				},
				"B2": {
					ParentRefs: []string{"A"},
					Service:    "B2-service",
				},
			},
			expectedStatuses: map[string]string{
				"A":  runtime.StatusEnabled,
				"B1": runtime.StatusEnabled,
				"B2": runtime.StatusEnabled,
			},
			expectedChildRefs: map[string][]string{
				"A":  {"B1", "B2"},
				"B1": nil,
				"B2": nil,
			},
		},
		{
			desc: "Non-root router with TLS config",
			routers: map[string]*dynamic.Router{
				"A": {},
				"B": {
					ParentRefs: []string{"A"},
					Service:    "B-service",
					TLS:        &dynamic.RouterTLSConfig{},
				},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusEnabled,
				"B": runtime.StatusDisabled,
			},
			expectedChildRefs: map[string][]string{
				"A": {"B"},
				"B": nil,
			},
			expectedErrors: map[string][]string{
				"B": {"non-root router cannot have TLS configuration"},
			},
		},
		{
			desc: "Non-root router with observability config",
			routers: map[string]*dynamic.Router{
				"A": {},
				"B": {
					ParentRefs:    []string{"A"},
					Service:       "B-service",
					Observability: &dynamic.RouterObservabilityConfig{},
				},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusEnabled,
				"B": runtime.StatusDisabled,
			},
			expectedChildRefs: map[string][]string{
				"A": {"B"},
				"B": nil,
			},
			expectedErrors: map[string][]string{
				"B": {"non-root router cannot have Observability configuration"},
			},
		},
		{
			desc: "Non-root router with EntryPoints config",
			routers: map[string]*dynamic.Router{
				"A": {},
				"B": {
					ParentRefs:  []string{"A"},
					Service:     "B-service",
					EntryPoints: []string{"web"},
				},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusEnabled,
				"B": runtime.StatusDisabled,
			},
			expectedChildRefs: map[string][]string{
				"A": {"B"},
				"B": nil,
			},
			expectedErrors: map[string][]string{
				"B": {"non-root router cannot have Entrypoints configuration"},
			},
		},

		{
			desc: "Router with non-existing parent",
			routers: map[string]*dynamic.Router{
				"B": {
					ParentRefs: []string{"A"},
					Service:    "B-service",
				},
			},
			expectedStatuses: map[string]string{
				"B": runtime.StatusDisabled,
			},
			expectedChildRefs: map[string][]string{
				"B": nil,
			},
			expectedErrors: map[string][]string{
				"B": {"parent router \"A\" does not exist", "router is not reachable"},
			},
		},
		{
			desc: "Dead-end router with no child and no service",
			routers: map[string]*dynamic.Router{
				"A": {},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusDisabled,
			},
			expectedChildRefs: map[string][]string{
				"A": {},
			},
			expectedErrors: map[string][]string{
				"A": {"router has no service and no child routers"},
			},
		},
		{
			// A->B->A
			desc: "Router is not reachable",
			routers: map[string]*dynamic.Router{
				"A": {
					ParentRefs: []string{"B"},
				},
				"B": {
					ParentRefs: []string{"A"},
				},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusDisabled,
				"B": runtime.StatusDisabled,
			},
			expectedChildRefs: map[string][]string{
				"A": {"B"},
				"B": {"A"},
			},
			// Cycle detection does not visit unreachable routers (it avoids computing the cycle dependency graph for unreachable routers).
			expectedErrors: map[string][]string{
				"A": {"router is not reachable"},
				"B": {"router is not reachable"},
			},
		},
		{
			// A->B->C->D->B
			desc: "Router creating a cycle is a dead-end and should be disabled",
			routers: map[string]*dynamic.Router{
				"A": {},
				"B": {
					ParentRefs: []string{"A", "D"},
				},
				"C": {
					ParentRefs: []string{"B"},
				},
				"D": {
					ParentRefs: []string{"C"},
				},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusEnabled,
				"B": runtime.StatusEnabled,
				"C": runtime.StatusEnabled,
				"D": runtime.StatusDisabled, // Dead-end router is disabled, because the cycle error broke the link with B.
			},
			expectedChildRefs: map[string][]string{
				"A": {"B"},
				"B": {"C"},
				"C": {"D"},
				"D": {},
			},
			expectedErrors: map[string][]string{
				"D": {
					"cyclic reference detected in router tree: B -> C -> D -> B",
					"router has no service and no child routers",
				},
			},
		},
		{
			// A->B->C->D->B
			//           ->E
			desc: "Router creating a cycle A->B->C->D->B but which is referenced elsewhere, must be set to warning status",
			routers: map[string]*dynamic.Router{
				"A": {},
				"B": {
					ParentRefs: []string{"A", "D"},
				},
				"C": {
					ParentRefs: []string{"B"},
				},
				"D": {
					ParentRefs: []string{"C"},
				},
				"E": {
					ParentRefs: []string{"D"},
					Service:    "E-service",
				},
			},
			expectedStatuses: map[string]string{
				"A": runtime.StatusEnabled,
				"B": runtime.StatusEnabled,
				"C": runtime.StatusEnabled,
				"D": runtime.StatusWarning,
				"E": runtime.StatusEnabled,
			},
			expectedChildRefs: map[string][]string{
				"A": {"B"},
				"B": {"C"},
				"C": {"D"},
				"D": {"E"},
			},
			expectedErrors: map[string][]string{
				"D": {"cyclic reference detected in router tree: B -> C -> D -> B"},
			},
		},
		{
			desc: "Parent router with all children having errors",
			routers: map[string]*dynamic.Router{
				"parent": {},
				"child-a": {
					ParentRefs: []string{"parent"},
					Service:    "child-a-service",
					TLS:        &dynamic.RouterTLSConfig{}, // Invalid: non-root cannot have TLS
				},
				"child-b": {
					ParentRefs: []string{"parent"},
					Service:    "child-b-service",
					TLS:        &dynamic.RouterTLSConfig{}, // Invalid: non-root cannot have TLS
				},
			},
			expectedStatuses: map[string]string{
				"parent":  runtime.StatusEnabled, // Enabled during ParseRouterTree (no config errors). Would be disabled during handler building when empty muxer is detected.
				"child-a": runtime.StatusDisabled,
				"child-b": runtime.StatusDisabled,
			},
			expectedChildRefs: map[string][]string{
				"parent":  {"child-a", "child-b"},
				"child-a": nil,
				"child-b": nil,
			},
			expectedErrors: map[string][]string{
				"child-a": {"non-root router cannot have TLS configuration"},
				"child-b": {"non-root router cannot have TLS configuration"},
			},
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			// Create runtime routers
			runtimeRouters := make(map[string]*runtime.RouterInfo)
			for name, router := range test.routers {
				runtimeRouters[name] = &runtime.RouterInfo{
					Router: router,
					Status: runtime.StatusEnabled,
				}
			}

			conf := &runtime.Configuration{
				Routers: runtimeRouters,
			}

			manager := &Manager{
				conf: conf,
			}

			// Execute the function we're testing
			manager.ParseRouterTree()

			// Verify ChildRefs are populated correctly
			for routerName, expectedChildren := range test.expectedChildRefs {
				router := runtimeRouters[routerName]
				assert.ElementsMatch(t, expectedChildren, router.ChildRefs)
			}

			// Verify statuses are set correctly
			var gotStatuses map[string]string
			for routerName, router := range runtimeRouters {
				if gotStatuses == nil {
					gotStatuses = make(map[string]string)
				}
				gotStatuses[routerName] = router.Status
			}
			assert.Equal(t, test.expectedStatuses, gotStatuses)

			// Verify errors are added correctly
			var gotErrors map[string][]string
			for routerName, router := range runtimeRouters {
				for _, err := range router.Err {
					if gotErrors == nil {
						gotErrors = make(map[string][]string)
					}
					gotErrors[routerName] = append(gotErrors[routerName], err)
				}
			}
			assert.Equal(t, test.expectedErrors, gotErrors)
		})
	}
}

func TestManager_buildChildRoutersMuxer(t *testing.T) {
	testCases := []struct {
		desc             string
		childRefs        []string
		routers          map[string]*dynamic.Router
		services         map[string]*dynamic.Service
		middlewares      map[string]*dynamic.Middleware
		expectedError    string
		expectedRequests []struct {
			path       string
			statusCode int
		}
	}{
		{
			desc:      "simple child router with service",
			childRefs: []string{"child1"},
			routers: map[string]*dynamic.Router{
				"child1": {
					Rule:    "Path(`/api`)",
					Service: "child1-service",
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{
				{path: "/api", statusCode: http.StatusOK},
				{path: "/unknown", statusCode: http.StatusNotFound},
			},
		},
		{
			desc:      "multiple child routers with different rules",
			childRefs: []string{"child1", "child2"},
			routers: map[string]*dynamic.Router{
				"child1": {
					Rule:    "Path(`/api`)",
					Service: "child1-service",
				},
				"child2": {
					Rule:    "Path(`/web`)",
					Service: "child2-service",
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
				"child2-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{
				{path: "/api", statusCode: http.StatusOK},
				{path: "/web", statusCode: http.StatusOK},
				{path: "/unknown", statusCode: http.StatusNotFound},
			},
		},
		{
			desc:      "child router with middleware",
			childRefs: []string{"child1"},
			routers: map[string]*dynamic.Router{
				"child1": {
					Rule:        "Path(`/api`)",
					Service:     "child1-service",
					Middlewares: []string{"test-middleware"},
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
			},
			middlewares: map[string]*dynamic.Middleware{
				"test-middleware": {
					Headers: &dynamic.Headers{
						CustomRequestHeaders: map[string]string{"X-Test": "value"},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{
				{path: "/api", statusCode: http.StatusOK},
				{path: "/unknown", statusCode: http.StatusNotFound},
			},
		},
		{
			desc:      "nested child routers (child with its own children)",
			childRefs: []string{"intermediate"},
			routers: map[string]*dynamic.Router{
				"intermediate": {
					Rule: "PathPrefix(`/api`)",
					// No service - this will have its own children
				},
				"leaf1": {
					Rule:       "Path(`/api/v1`)",
					Service:    "leaf1-service",
					ParentRefs: []string{"intermediate"},
				},
				"leaf2": {
					Rule:       "Path(`/api/v2`)",
					Service:    "leaf2-service",
					ParentRefs: []string{"intermediate"},
				},
			},
			services: map[string]*dynamic.Service{
				"leaf1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
				"leaf2-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{
				{path: "/api/v1", statusCode: http.StatusOK},
				{path: "/api/v2", statusCode: http.StatusOK},
				{path: "/unknown", statusCode: http.StatusNotFound},
			},
		},
		{
			desc:      "all child routers have errors - should return error",
			childRefs: []string{"child1", "child2"},
			routers: map[string]*dynamic.Router{
				"child1": {
					Rule:       "Path(`/api`)",
					Service:    "child1-service",
					ParentRefs: []string{"parent"},
					TLS:        &dynamic.RouterTLSConfig{}, // Invalid: non-root router cannot have TLS
				},
				"child2": {
					Rule:       "Path(`/web`)",
					Service:    "child2-service",
					ParentRefs: []string{"parent"},
					TLS:        &dynamic.RouterTLSConfig{}, // Invalid: non-root router cannot have TLS
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
				"child2-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
					},
				},
			},
			expectedError: "no child routers could be added to muxer (2 skipped)",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			// Create runtime routers
			runtimeRouters := make(map[string]*runtime.RouterInfo)
			for name, router := range test.routers {
				runtimeRouters[name] = &runtime.RouterInfo{
					Router: router,
				}
			}

			// Create runtime services
			runtimeServices := make(map[string]*runtime.ServiceInfo)
			for name, service := range test.services {
				runtimeServices[name] = &runtime.ServiceInfo{
					Service: service,
				}
			}

			// Create runtime middlewares
			runtimeMiddlewares := make(map[string]*runtime.MiddlewareInfo)
			for name, middleware := range test.middlewares {
				runtimeMiddlewares[name] = &runtime.MiddlewareInfo{
					Middleware: middleware,
				}
			}

			conf := &runtime.Configuration{
				Routers:     runtimeRouters,
				Services:    runtimeServices,
				Middlewares: runtimeMiddlewares,
			}

			// Set up the manager with mocks
			serviceManager := &mockServiceManager{}
			middlewareBuilder := &mockMiddlewareBuilder{}
			parser, err := httpmuxer.NewSyntaxParser()
			require.NoError(t, err)

			manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})

			// Compute multi-layer routing to populate ChildRefs
			manager.ParseRouterTree()

			// Build the child routers muxer
			ctx := t.Context()
			muxer, err := manager.buildChildRoutersMuxer(ctx, "test", test.childRefs)

			if test.expectedError != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), test.expectedError)
				return
			}

			if len(test.childRefs) == 0 {
				assert.Error(t, err)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, muxer)

			// Test that the muxer routes requests correctly
			for _, req := range test.expectedRequests {
				recorder := httptest.NewRecorder()
				request := httptest.NewRequest(http.MethodGet, req.path, nil)
				muxer.ServeHTTP(recorder, request)

				assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
			}
		})
	}
}

func TestManager_buildHTTPHandler_WithChildRouters(t *testing.T) {
	testCases := []struct {
		desc             string
		router           *runtime.RouterInfo
		childRouters     map[string]*dynamic.Router
		services         map[string]*dynamic.Service
		expectedError    string
		expectedRequests []struct {
			path       string
			statusCode int
		}
	}{
		{
			desc: "router with child routers",
			router: &runtime.RouterInfo{
				Router: &dynamic.Router{
					Rule: "PathPrefix(`/api`)",
				},
				ChildRefs: []string{"child1", "child2"},
			},
			childRouters: map[string]*dynamic.Router{
				"child1": {
					Rule:    "Path(`/api/v1`)",
					Service: "child1-service",
				},
				"child2": {
					Rule:    "Path(`/api/v2`)",
					Service: "child2-service",
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
				"child2-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{
				{path: "/unknown", statusCode: http.StatusNotFound},
			},
		},
		{
			desc: "router with service (normal case)",
			router: &runtime.RouterInfo{
				Router: &dynamic.Router{
					Rule:    "PathPrefix(`/api`)",
					Service: "main-service",
				},
			},
			services: map[string]*dynamic.Service{
				"main-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{},
		},
		{
			desc: "router with neither service nor child routers - error",
			router: &runtime.RouterInfo{
				Router: &dynamic.Router{
					Rule: "PathPrefix(`/api`)",
				},
			},
			expectedError: "router must have either a service or child routers",
		},
		{
			desc: "router with child routers but missing child - error",
			router: &runtime.RouterInfo{
				Router: &dynamic.Router{
					Rule: "PathPrefix(`/api`)",
				},
				ChildRefs: []string{"nonexistent"},
			},
			expectedError: "child router \"nonexistent\" does not exist",
		},
		{
			desc: "router with all children having errors - returns empty muxer error",
			router: &runtime.RouterInfo{
				Router: &dynamic.Router{
					Rule: "PathPrefix(`/api`)",
				},
				ChildRefs: []string{"child1", "child2"},
			},
			childRouters: map[string]*dynamic.Router{
				"child1": {
					Rule:       "Path(`/api/v1`)",
					Service:    "child1-service",
					ParentRefs: []string{"parent"},
					TLS:        &dynamic.RouterTLSConfig{}, // Invalid for non-root
				},
				"child2": {
					Rule:       "Path(`/api/v2`)",
					Service:    "child2-service",
					ParentRefs: []string{"parent"},
					TLS:        &dynamic.RouterTLSConfig{}, // Invalid for non-root
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
				"child2-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
					},
				},
			},
			expectedError: "no child routers could be added to muxer (2 skipped)",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			// Create runtime routers
			runtimeRouters := make(map[string]*runtime.RouterInfo)
			runtimeRouters["test-router"] = test.router
			for name, router := range test.childRouters {
				runtimeRouters[name] = &runtime.RouterInfo{
					Router: router,
				}
			}

			// Create runtime services
			runtimeServices := make(map[string]*runtime.ServiceInfo)
			for name, service := range test.services {
				runtimeServices[name] = &runtime.ServiceInfo{
					Service: service,
				}
			}

			conf := &runtime.Configuration{
				Routers:  runtimeRouters,
				Services: runtimeServices,
			}

			// Set up the manager with mocks
			serviceManager := &mockServiceManager{}
			middlewareBuilder := &mockMiddlewareBuilder{}
			parser, err := httpmuxer.NewSyntaxParser()
			require.NoError(t, err)

			manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})

			// Run ParseRouterTree to validate configuration and populate ChildRefs/errors
			manager.ParseRouterTree()

			// Build the HTTP handler
			ctx := t.Context()
			handler, err := manager.buildHTTPHandler(ctx, test.router, "test", "test-router")

			if test.expectedError != "" {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), test.expectedError)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, handler)

			// Test that the handler routes requests correctly
			for _, req := range test.expectedRequests {
				recorder := httptest.NewRecorder()
				request := httptest.NewRequest(http.MethodGet, req.path, nil)
				handler.ServeHTTP(recorder, request)

				assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
			}
		})
	}
}

func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
	testCases := []struct {
		desc             string
		routers          map[string]*dynamic.Router
		services         map[string]*dynamic.Service
		expectedRequests []struct {
			path       string
			statusCode int
		}
	}{
		{
			desc: "parent router with child routers",
			routers: map[string]*dynamic.Router{
				"parent": {
					EntryPoints: []string{"web"},
					Rule:        "PathPrefix(`/api`)",
				},
				"child1": {
					Rule:       "Path(`/api/v1`)",
					Service:    "child1-service",
					ParentRefs: []string{"parent"},
				},
				"child2": {
					Rule:       "Path(`/api/v2`)",
					Service:    "child2-service",
					ParentRefs: []string{"parent"},
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
				"child2-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{
				{path: "/unknown", statusCode: http.StatusNotFound},
			},
		},
		{
			desc: "multiple parent routers with children",
			routers: map[string]*dynamic.Router{
				"api-parent": {
					EntryPoints: []string{"web"},
					Rule:        "PathPrefix(`/api`)",
				},
				"web-parent": {
					EntryPoints: []string{"web"},
					Rule:        "PathPrefix(`/web`)",
				},
				"api-child": {
					Rule:       "Path(`/api/v1`)",
					Service:    "api-service",
					ParentRefs: []string{"api-parent"},
				},
				"web-child": {
					Rule:       "Path(`/web/index`)",
					Service:    "web-service",
					ParentRefs: []string{"web-parent"},
				},
			},
			services: map[string]*dynamic.Service{
				"api-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
				"web-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
					},
				},
			},
			expectedRequests: []struct {
				path       string
				statusCode int
			}{},
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			// Create runtime routers
			runtimeRouters := make(map[string]*runtime.RouterInfo)
			for name, router := range test.routers {
				runtimeRouters[name] = &runtime.RouterInfo{
					Router: router,
				}
			}

			// Create runtime services
			runtimeServices := make(map[string]*runtime.ServiceInfo)
			for name, service := range test.services {
				runtimeServices[name] = &runtime.ServiceInfo{
					Service: service,
				}
			}

			conf := &runtime.Configuration{
				Routers:  runtimeRouters,
				Services: runtimeServices,
			}

			// Set up the manager with mocks
			serviceManager := &mockServiceManager{}
			middlewareBuilder := &mockMiddlewareBuilder{}
			parser, err := httpmuxer.NewSyntaxParser()
			require.NoError(t, err)

			manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})

			// Compute multi-layer routing to set up parent-child relationships
			manager.ParseRouterTree()

			// Build handlers
			ctx := t.Context()
			handlers := manager.BuildHandlers(ctx, []string{"web"}, false)

			handler := handlers["web"]
			require.NotNil(t, handler)

			// Test that the handler routes requests correctly
			for _, req := range test.expectedRequests {
				recorder := httptest.NewRecorder()
				request := httptest.NewRequest(http.MethodGet, req.path, nil)
				request.Host = "test.com"
				handler.ServeHTTP(recorder, request)

				assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
			}
		})
	}
}

func TestManager_BuildHandlers_Deny(t *testing.T) {
	testCases := []struct {
		desc               string
		routers            map[string]*dynamic.Router
		services           map[string]*dynamic.Service
		requestPath        string
		expectedStatusCode int
	}{
		{
			desc:        "parent router without child routers, request with encoded slash",
			requestPath: "/foo%2F",
			routers: map[string]*dynamic.Router{
				"parent": {
					EntryPoints: []string{"web"},
					Rule:        "PathPrefix(`/`)",
					Service:     "service",
				},
			},
			services: map[string]*dynamic.Service{
				"service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
			},
			expectedStatusCode: http.StatusOK,
		},
		{
			desc:        "parent router with child routers, request with encoded slash",
			requestPath: "/foo%2F",
			routers: map[string]*dynamic.Router{
				"parent": {
					EntryPoints: []string{"web"},
					Rule:        "PathPrefix(`/`)",
				},
				"child1": {
					Rule:       "PathPrefix(`/`)",
					Service:    "child1-service",
					ParentRefs: []string{"parent"},
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
			},
			expectedStatusCode: http.StatusOK,
		},
		{
			desc:        "parent router disallowing encoded slash without child router",
			requestPath: "/foo%2F",
			routers: map[string]*dynamic.Router{
				"parent": {
					EntryPoints: []string{"web"},
					Rule:        "PathPrefix(`/`)",
					Service:     "service",
					DeniedEncodedPathCharacters: &dynamic.RouterDeniedEncodedPathCharacters{
						AllowEncodedSlash: false,
					},
				},
			},
			services: map[string]*dynamic.Service{
				"service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
			},
			expectedStatusCode: http.StatusBadRequest,
		},
		{
			desc:        "parent router disallowing encoded slash with child routers",
			requestPath: "/foo%2F",
			routers: map[string]*dynamic.Router{
				"parent": {
					EntryPoints: []string{"web"},
					Rule:        "PathPrefix(`/`)",
					DeniedEncodedPathCharacters: &dynamic.RouterDeniedEncodedPathCharacters{
						AllowEncodedSlash: false,
					},
				},
				"child1": {
					Rule:       "PathPrefix(`/`)",
					Service:    "child1-service",
					ParentRefs: []string{"parent"},
				},
			},
			services: map[string]*dynamic.Service{
				"child1-service": {
					LoadBalancer: &dynamic.ServersLoadBalancer{
						Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
					},
				},
			},
			expectedStatusCode: http.StatusBadRequest,
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			// Create runtime routers
			runtimeRouters := make(map[string]*runtime.RouterInfo)
			for name, router := range test.routers {
				runtimeRouters[name] = &runtime.RouterInfo{
					Router: router,
				}
			}

			// Create runtime services
			runtimeServices := make(map[string]*runtime.ServiceInfo)
			for name, service := range test.services {
				runtimeServices[name] = &runtime.ServiceInfo{
					Service: service,
				}
			}

			conf := &runtime.Configuration{
				Routers:  runtimeRouters,
				Services: runtimeServices,
			}

			// Set up the manager with mocks
			serviceManager := &mockServiceManager{}
			middlewareBuilder := &mockMiddlewareBuilder{}

			parser, err := httpmuxer.NewSyntaxParser()
			require.NoError(t, err)

			manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})

			// Compute multi-layer routing to set up parent-child relationships
			manager.ParseRouterTree()

			// Build handlers
			ctx := t.Context()
			handlers := manager.BuildHandlers(ctx, []string{"web"}, false)

			recorder := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodGet, test.requestPath, http.NoBody)

			handlers["web"].ServeHTTP(recorder, request)

			assert.Equal(t, test.expectedStatusCode, recorder.Code)
		})
	}
}

// Mock implementations for testing

type staticTransportManager struct {
	res *http.Response
}

func (s staticTransportManager) GetRoundTripper(_ string) (http.RoundTripper, error) {
	return &staticTransport{res: s.res}, nil
}

func (s staticTransportManager) GetTLSConfig(_ string) (*tls.Config, error) {
	panic("implement me")
}

func (s staticTransportManager) Get(_ string) (*dynamic.ServersTransport, error) {
	panic("implement me")
}

type staticTransport struct {
	res *http.Response
}

func (t *staticTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
	return t.res, nil
}

type mockServiceManager struct{}

func (m *mockServiceManager) BuildHTTP(_ context.Context, _ string) (http.Handler, error) {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("mock service response"))
	}), nil
}

func (m *mockServiceManager) LaunchHealthCheck(_ context.Context) {}

type mockMiddlewareBuilder struct{}

func (m *mockMiddlewareBuilder) BuildMiddlewareChain(_ context.Context, _ []string) *alice.Chain {
	chain := alice.New()
	return &chain
}

type proxyBuilderMock struct{}

func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
	return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
}

func (p proxyBuilderMock) Update(_ map[string]*dynamic.ServersTransport) {
	panic("implement me")
}

// labellingProxyBuilder builds a handler that writes the target URL host as
// the X-From response header, allowing tests to identify which backend was
// selected by the router.
type labellingProxyBuilder struct{}

func (l labellingProxyBuilder) Build(_ string, target *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
	label := target.Host
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("X-From", label)
	}), nil
}

func (l labellingProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
