package knative

import (
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/traefik/paerser/types"
	"github.com/traefik/traefik/v3/pkg/config/dynamic"
	"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
	"k8s.io/apimachinery/pkg/runtime"
	clientfeatures "k8s.io/client-go/features"
	clientfeaturestesting "k8s.io/client-go/features/testing"
	kubefake "k8s.io/client-go/kubernetes/fake"
	kscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/utils/ptr"
	knativenetworkingv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1"
	knfake "knative.dev/networking/pkg/client/clientset/versioned/fake"
)

func init() {
	// required by k8s.MustParseYaml
	if err := knativenetworkingv1alpha1.AddToScheme(kscheme.Scheme); err != nil {
		panic(err)
	}
}

func Test_loadConfiguration(t *testing.T) {
	testCases := []struct {
		desc    string
		paths   []string
		want    *dynamic.Configuration
		wantLen int
	}{
		{
			desc:    "Wrong ingress class",
			paths:   []string{"wrong_ingress_class.yaml"},
			wantLen: 0,
			want: &dynamic.Configuration{
				HTTP: &dynamic.HTTPConfiguration{
					Routers:     map[string]*dynamic.Router{},
					Services:    map[string]*dynamic.Service{},
					Middlewares: map[string]*dynamic.Middleware{},
				},
			},
		},
		{
			desc:    "Cluster Local",
			paths:   []string{"cluster_local.yaml", "services.yaml"},
			wantLen: 1,
			want: &dynamic.Configuration{
				HTTP: &dynamic.HTTPConfiguration{
					Routers: map[string]*dynamic.Router{
						"default-helloworld-go-rule-0-path-0": {
							EntryPoints: []string{"priv-http", "priv-https"},
							Service:     "default-helloworld-go-rule-0-path-0-wrr",
							Rule:        `(Host("helloworld-go.default") || Host("helloworld-go.default.svc") || Host("helloworld-go.default.svc.cluster.local"))`,
							Middlewares: []string{},
						},
					},
					Services: map[string]*dynamic.Service{
						"default-helloworld-go-rule-0-path-0-split-0": {
							LoadBalancer: &dynamic.ServersLoadBalancer{
								Strategy:       "wrr",
								PassHostHeader: ptr.To(true),
								ResponseForwarding: &dynamic.ResponseForwarding{
									FlushInterval: types.Duration(100 * time.Millisecond),
								},
								Servers: []dynamic.Server{
									{
										URL: "http://10.43.38.208:80",
									},
								},
							},
						},
						"default-helloworld-go-rule-0-path-0-split-1": {
							LoadBalancer: &dynamic.ServersLoadBalancer{
								Strategy:       "wrr",
								PassHostHeader: ptr.To(true),
								ResponseForwarding: &dynamic.ResponseForwarding{
									FlushInterval: types.Duration(100 * time.Millisecond),
								},
								Servers: []dynamic.Server{
									{
										URL: "http://10.43.44.18:80",
									},
								},
							},
						},
						"default-helloworld-go-rule-0-path-0-wrr": {
							Weighted: &dynamic.WeightedRoundRobin{
								Services: []dynamic.WRRService{
									{
										Name:   "default-helloworld-go-rule-0-path-0-split-0",
										Weight: ptr.To(50),
										Headers: map[string]string{
											"Knative-Serving-Namespace": "default",
											"Knative-Serving-Revision":  "helloworld-go-00001",
										},
									},
									{
										Name:   "default-helloworld-go-rule-0-path-0-split-1",
										Weight: ptr.To(50),
										Headers: map[string]string{
											"Knative-Serving-Namespace": "default",
											"Knative-Serving-Revision":  "helloworld-go-00002",
										},
									},
								},
							},
						},
					},
					Middlewares: map[string]*dynamic.Middleware{},
				},
			},
		},
		{
			desc:    "External IP",
			paths:   []string{"external_ip.yaml", "services.yaml"},
			wantLen: 1,
			want: &dynamic.Configuration{
				HTTP: &dynamic.HTTPConfiguration{
					Routers: map[string]*dynamic.Router{
						"default-helloworld-go-rule-0-path-0": {
							EntryPoints: []string{"http", "https"},
							Service:     "default-helloworld-go-rule-0-path-0-wrr",
							Rule:        `(Host("helloworld-go.default") || Host("helloworld-go.default.svc") || Host("helloworld-go.default.svc.cluster.local"))`,
							Middlewares: []string{},
						},
					},
					Services: map[string]*dynamic.Service{
						"default-helloworld-go-rule-0-path-0-split-0": {
							LoadBalancer: &dynamic.ServersLoadBalancer{
								Strategy:       "wrr",
								PassHostHeader: ptr.To(true),
								ResponseForwarding: &dynamic.ResponseForwarding{
									FlushInterval: types.Duration(100 * time.Millisecond),
								},
								Servers: []dynamic.Server{
									{
										URL: "http://10.43.38.208:80",
									},
								},
							},
						},
						"default-helloworld-go-rule-0-path-0-split-1": {
							LoadBalancer: &dynamic.ServersLoadBalancer{
								Strategy:       "wrr",
								PassHostHeader: ptr.To(true),
								ResponseForwarding: &dynamic.ResponseForwarding{
									FlushInterval: types.Duration(100 * time.Millisecond),
								},
								Servers: []dynamic.Server{
									{
										URL: "http://10.43.44.18:80",
									},
								},
							},
						},
						"default-helloworld-go-rule-0-path-0-wrr": {
							Weighted: &dynamic.WeightedRoundRobin{
								Services: []dynamic.WRRService{
									{
										Name:   "default-helloworld-go-rule-0-path-0-split-0",
										Weight: ptr.To(50),
										Headers: map[string]string{
											"Knative-Serving-Namespace": "default",
											"Knative-Serving-Revision":  "helloworld-go-00001",
										},
									},
									{
										Name:   "default-helloworld-go-rule-0-path-0-split-1",
										Weight: ptr.To(50),
										Headers: map[string]string{
											"Knative-Serving-Namespace": "default",
											"Knative-Serving-Revision":  "helloworld-go-00002",
										},
									},
								},
							},
						},
					},
					Middlewares: map[string]*dynamic.Middleware{},
				},
			},
		},
		{
			desc:    "TLS",
			paths:   []string{"tls.yaml", "services.yaml"},
			wantLen: 1,
			want: &dynamic.Configuration{
				HTTP: &dynamic.HTTPConfiguration{
					Routers: map[string]*dynamic.Router{
						"default-helloworld-go-rule-0-path-0": {
							EntryPoints: []string{"http", "https"},
							Service:     "default-helloworld-go-rule-0-path-0-wrr",
							Rule:        `(Host("helloworld-go.default") || Host("helloworld-go.default.svc") || Host("helloworld-go.default.svc.cluster.local"))`,
							Middlewares: []string{},
						},
						"default-helloworld-go-rule-0-path-0-tls": {
							EntryPoints: []string{"http", "https"},
							Service:     "default-helloworld-go-rule-0-path-0-wrr",
							Rule:        `(Host("helloworld-go.default") || Host("helloworld-go.default.svc") || Host("helloworld-go.default.svc.cluster.local"))`,
							Middlewares: []string{},
							TLS:         &dynamic.RouterTLSConfig{},
						},
					},
					Services: map[string]*dynamic.Service{
						"default-helloworld-go-rule-0-path-0-split-0": {
							LoadBalancer: &dynamic.ServersLoadBalancer{
								Strategy:       "wrr",
								PassHostHeader: ptr.To(true),
								ResponseForwarding: &dynamic.ResponseForwarding{
									FlushInterval: types.Duration(100 * time.Millisecond),
								},
								Servers: []dynamic.Server{
									{
										URL: "http://10.43.38.208:80",
									},
								},
							},
						},
						"default-helloworld-go-rule-0-path-0-split-1": {
							LoadBalancer: &dynamic.ServersLoadBalancer{
								Strategy:       "wrr",
								PassHostHeader: ptr.To(true),
								ResponseForwarding: &dynamic.ResponseForwarding{
									FlushInterval: types.Duration(100 * time.Millisecond),
								},
								Servers: []dynamic.Server{
									{
										URL: "http://10.43.44.18:80",
									},
								},
							},
						},
						"default-helloworld-go-rule-0-path-0-wrr": {
							Weighted: &dynamic.WeightedRoundRobin{
								Services: []dynamic.WRRService{
									{
										Name:   "default-helloworld-go-rule-0-path-0-split-0",
										Weight: ptr.To(50),
										Headers: map[string]string{
											"Knative-Serving-Namespace": "default",
											"Knative-Serving-Revision":  "helloworld-go-00001",
										},
									},
									{
										Name:   "default-helloworld-go-rule-0-path-0-split-1",
										Weight: ptr.To(50),
										Headers: map[string]string{
											"Knative-Serving-Namespace": "default",
											"Knative-Serving-Revision":  "helloworld-go-00002",
										},
									},
								},
							},
						},
					},
					Middlewares: map[string]*dynamic.Middleware{},
				},
			},
		},
	}

	// Disable WatchListClient because the knative informers (third-party dependency)
	// don't wrap their ListWatch with cache.ToListWatcherWithWatchListSemantics,
	// which causes fake clients to hang waiting for bookmark events.
	clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, false)

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

			k8sObjects, knObjects := readResources(t, testCase.paths)

			k8sClient := kubefake.NewClientset(k8sObjects...)
			knClient := knfake.NewSimpleClientset(knObjects...)

			client := newClientImpl(knClient, k8sClient)

			eventCh, err := client.WatchAll(nil, make(chan struct{}))
			require.NoError(t, err)

			if len(k8sObjects) > 0 || len(knObjects) > 0 {
				// just wait for the first event
				<-eventCh
			}

			p := Provider{
				PublicEntrypoints:  []string{"http", "https"},
				PrivateEntrypoints: []string{"priv-http", "priv-https"},
				client:             client,
			}

			got, gotIngresses := p.loadConfiguration(t.Context())
			assert.Len(t, gotIngresses, testCase.wantLen)
			assert.Equal(t, testCase.want, got)
		})
	}
}

func Test_buildRule(t *testing.T) {
	testCases := []struct {
		desc    string
		hosts   []string
		headers map[string]knativenetworkingv1alpha1.HeaderMatch
		path    string
		want    string
	}{
		{
			desc:  "single host, no headers, no path",
			hosts: []string{"example.com"},
			want:  `(Host("example.com"))`,
		},
		{
			desc:  "multiple hosts, no headers, no path",
			hosts: []string{"example.com", "foo.com"},
			want:  `(Host("example.com") || Host("foo.com"))`,
		},
		{
			desc:  "single host, single header, no path",
			hosts: []string{"example.com"},
			headers: map[string]knativenetworkingv1alpha1.HeaderMatch{
				"X-Header": {Exact: "value"},
			},
			want: `(Host("example.com")) && (Header("X-Header","value"))`,
		},
		{
			desc:  "single host, multiple headers, no path",
			hosts: []string{"example.com"},
			headers: map[string]knativenetworkingv1alpha1.HeaderMatch{
				"X-Header":  {Exact: "value"},
				"X-Header2": {Exact: "value2"},
			},
			want: `(Host("example.com")) && (Header("X-Header","value") && Header("X-Header2","value2"))`,
		},
		{
			desc:  "single host, multiple headers, with path",
			hosts: []string{"example.com"},
			headers: map[string]knativenetworkingv1alpha1.HeaderMatch{
				"X-Header":  {Exact: "value"},
				"X-Header2": {Exact: "value2"},
			},
			path: "/foo",
			want: `(Host("example.com")) && (Header("X-Header","value") && Header("X-Header2","value2")) && PathPrefix("/foo")`,
		},
		{
			desc:  "single host, no headers, with path",
			hosts: []string{"example.com"},
			path:  "/foo",
			want:  `(Host("example.com")) && PathPrefix("/foo")`,
		},
	}

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

			got := buildRule(test.hosts, test.headers, test.path)
			assert.Equal(t, test.want, got)
		})
	}
}

func Test_mergeHTTPConfigs(t *testing.T) {
	testCases := []struct {
		desc    string
		configs []*dynamic.HTTPConfiguration
		want    *dynamic.HTTPConfiguration
	}{
		{
			desc: "one empty configuration",
			configs: []*dynamic.HTTPConfiguration{
				{
					Routers: map[string]*dynamic.Router{
						"router1": {Rule: `Host("example.com")`},
					},
					Middlewares: map[string]*dynamic.Middleware{
						"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
					},
					Services: map[string]*dynamic.Service{
						"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
					},
				},
				{
					Routers:     map[string]*dynamic.Router{},
					Middlewares: map[string]*dynamic.Middleware{},
					Services:    map[string]*dynamic.Service{},
				},
			},
			want: &dynamic.HTTPConfiguration{
				Routers: map[string]*dynamic.Router{
					"router1": {Rule: `Host("example.com")`},
				},
				Middlewares: map[string]*dynamic.Middleware{
					"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
				},
				Services: map[string]*dynamic.Service{
					"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
				},
			},
		},
		{
			desc: "merging two non-empty configurations",
			configs: []*dynamic.HTTPConfiguration{
				{
					Routers: map[string]*dynamic.Router{
						"router1": {Rule: `Host("example.com")`},
					},
					Middlewares: map[string]*dynamic.Middleware{
						"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
					},
					Services: map[string]*dynamic.Service{
						"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
					},
				},
				{
					Routers: map[string]*dynamic.Router{
						"router2": {Rule: `PathPrefix("/test")`},
					},
					Middlewares: map[string]*dynamic.Middleware{
						"middleware2": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
					},
					Services: map[string]*dynamic.Service{
						"service2": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
					},
				},
			},
			want: &dynamic.HTTPConfiguration{
				Routers: map[string]*dynamic.Router{
					"router1": {Rule: `Host("example.com")`},
					"router2": {Rule: `PathPrefix("/test")`},
				},
				Middlewares: map[string]*dynamic.Middleware{
					"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
					"middleware2": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
				},
				Services: map[string]*dynamic.Service{
					"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
					"service2": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
				},
			},
		},
	}

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

			got := mergeHTTPConfigs(test.configs...)
			assert.Equal(t, test.want, got)
		})
	}
}

func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Object) {
	t.Helper()

	var (
		k8sObjects []runtime.Object
		knObjects  []runtime.Object
	)
	for _, path := range paths {
		yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path))
		if err != nil {
			panic(err)
		}

		objects := k8s.MustParseYaml(yamlContent)
		for _, obj := range objects {
			switch obj.GetObjectKind().GroupVersionKind().Group {
			case "networking.internal.knative.dev":
				knObjects = append(knObjects, obj)
			default:
				k8sObjects = append(k8sObjects, obj)
			}
		}
	}

	return k8sObjects, knObjects
}
