/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package source

import (
	"errors"
	"testing"

	"sigs.k8s.io/external-dns/internal/testutils"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"sigs.k8s.io/external-dns/endpoint"
	"sigs.k8s.io/external-dns/source/annotations"
	templatetest "sigs.k8s.io/external-dns/source/template/testutil"
)

func createTestRouteGroup(ns, name string, annotations map[string]string, hosts []string, destinations []routeGroupLoadBalancer) *routeGroup {
	return &routeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Namespace:   ns,
			Name:        name,
			Annotations: annotations,
		},
		Spec: routeGroupSpec{
			Hosts: hosts,
		},
		Status: routeGroupStatus{
			LoadBalancer: routeGroupLoadBalancerStatus{
				RouteGroup: destinations,
			},
		},
	}
}

func TestEndpointsFromRouteGroups(t *testing.T) {
	t.Parallel()

	for _, tt := range []struct {
		name   string
		source *routeGroupSource
		rg     *routeGroup
		want   []*endpoint.Endpoint
	}{
		{
			name:   "Empty routegroup should return empty endpoints",
			source: &routeGroupSource{},
			rg:     &routeGroup{},
			want:   []*endpoint.Endpoint{},
		},
		{
			name:   "Routegroup without hosts and destinations create no endpoints",
			source: &routeGroupSource{},
			rg:     createTestRouteGroup("namespace1", "rg1", nil, nil, nil),
			want:   []*endpoint.Endpoint{},
		},
		{
			name:   "Routegroup without hosts create no endpoints",
			source: &routeGroupSource{},
			rg: createTestRouteGroup("namespace1", "rg1", nil, nil, []routeGroupLoadBalancer{
				{
					Hostname: "lb.example.org",
				},
			}),
			want: []*endpoint.Endpoint{},
		},
		{
			name:   "Routegroup without destinations create no endpoints",
			source: &routeGroupSource{},
			rg:     createTestRouteGroup("namespace1", "rg1", nil, []string{"rg1.k8s.example"}, nil),
			want:   []*endpoint.Endpoint{},
		},
		{
			name:   "Routegroup with hosts and destinations creates an endpoint",
			source: &routeGroupSource{},
			rg: createTestRouteGroup("namespace1", "rg1", nil, []string{"rg1.k8s.example"}, []routeGroupLoadBalancer{
				{
					Hostname: "lb.example.org",
				},
			}),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:   "Routegroup with hostname annotation, creates endpoints from the annotation ",
			source: &routeGroupSource{},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				map[string]string{
					annotations.HostnameKey: "my.example",
				},
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						Hostname: "lb.example.org",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
				{
					DNSName:    "my.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:   "Routegroup with hosts and destinations and ignoreHostnameAnnotation creates endpoints but ignores annotation",
			source: &routeGroupSource{ignoreHostnameAnnotation: true},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				map[string]string{
					annotations.HostnameKey: "my.example",
				},
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						Hostname: "lb.example.org",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:   "Routegroup with hosts and destinations and ttl creates an endpoint with ttl",
			source: &routeGroupSource{ignoreHostnameAnnotation: true},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				map[string]string{
					annotations.TtlKey: "2189",
				},
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						Hostname: "lb.example.org",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
					RecordTTL:  endpoint.TTL(2189),
				},
			},
		},
		{
			name:   "Routegroup with hosts and destination IP creates an endpoint",
			source: &routeGroupSource{},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				nil,
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						IP: "1.5.1.4",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeA,
					Targets:    endpoint.Targets([]string{"1.5.1.4"}),
				},
			},
		},
		{
			name:   "Routegroup with hosts and destination IPv6 creates an endpoint",
			source: &routeGroupSource{},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				nil,
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						IP: "2001:DB8::1",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeAAAA,
					Targets:    endpoint.Targets([]string{"2001:DB8::1"}),
				},
			},
		},
		{
			name:   "Routegroup with hosts and mixed destinations creates endpoints",
			source: &routeGroupSource{},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				nil,
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						Hostname: "lb.example.org",
						IP:       "1.5.1.4",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeA,
					Targets:    endpoint.Targets([]string{"1.5.1.4"}),
				},
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:   "Routegroup with hosts and mixed destinations (IPv6) creates endpoints",
			source: &routeGroupSource{},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				nil,
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						Hostname: "lb.example.org",
						IP:       "2001:DB8::1",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeAAAA,
					Targets:    endpoint.Targets([]string{"2001:DB8::1"}),
				},
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:   "Routegroup with provider-specific annotation creates endpoint with provider-specific property",
			source: &routeGroupSource{},
			rg: createTestRouteGroup(
				"namespace1",
				"rg1",
				map[string]string{
					annotations.AWSPrefix + "weight": "10",
				},
				[]string{"rg1.k8s.example"},
				[]routeGroupLoadBalancer{
					{
						Hostname: "lb.example.org",
					},
				},
			),
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
					ProviderSpecific: endpoint.ProviderSpecific{
						{Name: "aws/weight", Value: "10"},
					},
				},
			},
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.source.endpointsFromRouteGroup(tt.rg)

			testutils.ValidateEndpoints(t, got, tt.want)
		})
	}
}

type fakeRouteGroupClient struct {
	returnErr bool
	rg        *routeGroupList
}

func (f *fakeRouteGroupClient) getRouteGroupList(string) (*routeGroupList, error) {
	if f.returnErr {
		return nil, errors.New("Fake route group list error")
	}
	return f.rg, nil
}

func TestRouteGroupsEndpoints(t *testing.T) {
	for _, tt := range []struct {
		name        string
		source      *routeGroupSource
		templates   string
		combineFQDN bool
		want        []*endpoint.Endpoint
		wantErr     bool
	}{
		{
			name: "Empty routegroup should return empty endpoints",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{},
				},
			},
			want:    []*endpoint.Endpoint{},
			wantErr: false,
		},
		{
			name: "Single routegroup should return endpoints",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								nil,
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:        "Single routegroup with combineFQDNAnnotation with fqdn template should return endpoints from fqdnTemplate and routegroup",
			templates:   "{{.Metadata.Name}}.{{.Metadata.Namespace}}.example",
			combineFQDN: true,
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								nil,
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
				{
					DNSName:    "rg1.namespace1.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:      "Single routegroup without, with fqdn template should return endpoints from fqdnTemplate",
			templates: "{{.Metadata.Name}}.{{.Metadata.Namespace}}.example",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								nil,
								nil,
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.namespace1.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name:      "Single routegroup without combineFQDNAnnotation with fqdn template should return endpoints not from fqdnTemplate",
			templates: "{{.Metadata.Name}}.{{.Metadata.Namespace}}.example",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								nil,
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name: "Single routegroup with TTL should return endpoint with TTL",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								map[string]string{
									annotations.TtlKey: "2189",
								},
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
					RecordTTL:  endpoint.TTL(2189),
				},
			},
		},
		{
			name: "Routegroup with hosts and mixed destinations creates endpoints",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								nil,
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
										IP:       "1.5.1.4",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeA,
					Targets:    endpoint.Targets([]string{"1.5.1.4"}),
				},
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name: "multiple routegroups should return endpoints",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								nil,
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace1",
								"rg2",
								nil,
								[]string{"rg2.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace2",
								"rg3",
								nil,
								[]string{"rg3.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace3",
								"rg",
								nil,
								[]string{"rg.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb2.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
				{
					DNSName:    "rg2.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
				{
					DNSName:    "rg3.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
				{
					DNSName:    "rg.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb2.example.org"}),
				},
			},
		},
		{
			name: "multiple routegroups with filter annotations should return only filtered endpoints",
			source: &routeGroupSource{
				annotationFilter: "kubernetes.io/ingress.class=skipper",
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								map[string]string{
									"kubernetes.io/ingress.class": "skipper",
								},
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace1",
								"rg2",
								map[string]string{
									"kubernetes.io/ingress.class": "nginx",
								},
								[]string{"rg2.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace2",
								"rg3",
								map[string]string{
									"kubernetes.io/ingress.class": "",
								},
								[]string{"rg3.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace3",
								"rg",
								nil,
								[]string{"rg.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb2.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name: "multiple routegroups with set operation annotation filter should return only filtered endpoints",
			source: &routeGroupSource{
				annotationFilter: "kubernetes.io/ingress.class in (nginx, skipper)",
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								map[string]string{
									"kubernetes.io/ingress.class": "skipper",
								},
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace1",
								"rg2",
								map[string]string{
									"kubernetes.io/ingress.class": "nginx",
								},
								[]string{"rg2.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace2",
								"rg3",
								map[string]string{
									"kubernetes.io/ingress.class": "",
								},
								[]string{"rg3.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace3",
								"rg",
								nil,
								[]string{"rg.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb2.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
				{
					DNSName:    "rg2.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
		{
			name: "multiple routegroups with controller annotation filter should not return filtered endpoints",
			source: &routeGroupSource{
				cli: &fakeRouteGroupClient{
					rg: &routeGroupList{
						Items: []*routeGroup{
							createTestRouteGroup(
								"namespace1",
								"rg1",
								map[string]string{
									annotations.ControllerKey: annotations.ControllerValue,
								},
								[]string{"rg1.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace1",
								"rg2",
								map[string]string{
									annotations.ControllerKey: "dns",
								},
								[]string{"rg2.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
							createTestRouteGroup(
								"namespace2",
								"rg3",
								nil,
								[]string{"rg3.k8s.example"},
								[]routeGroupLoadBalancer{
									{
										Hostname: "lb.example.org",
									},
								},
							),
						},
					},
				},
			},
			want: []*endpoint.Endpoint{
				{
					DNSName:    "rg1.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
				{
					DNSName:    "rg3.k8s.example",
					RecordType: endpoint.RecordTypeCNAME,
					Targets:    endpoint.Targets([]string{"lb.example.org"}),
				},
			},
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			if tt.templates != "" {
				if tt.combineFQDN {
					tt.source.templateEngine = templatetest.MustEngine(t, tt.templates, "", "", true)
				} else {
					tt.source.templateEngine = templatetest.MustEngine(t, tt.templates, "", "", false)
				}
			}

			got, err := tt.source.Endpoints(t.Context())
			if err != nil && !tt.wantErr {
				t.Errorf("Got error, but does not want to get an error: %v", err)
			}
			if tt.wantErr && err == nil {
				t.Fatal("Got no error, but we want to get an error")
			}

			testutils.ValidateEndpoints(t, got, tt.want)
		})
	}
}

func TestResourceLabelIsSet(t *testing.T) {
	source := &routeGroupSource{
		cli: &fakeRouteGroupClient{
			rg: &routeGroupList{
				Items: []*routeGroup{
					createTestRouteGroup(
						"namespace1",
						"rg1",
						nil,
						[]string{"rg1.k8s.example"},
						[]routeGroupLoadBalancer{
							{
								Hostname: "lb.example.org",
							},
						},
					),
				},
			},
		},
	}

	got, _ := source.Endpoints(t.Context())
	for _, ep := range got {
		if _, ok := ep.Labels[endpoint.ResourceLabelKey]; !ok {
			t.Errorf("Failed to set resource label on ep %v", ep)
		}
	}
}
