// Copyright Istio 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 server_test

import (
	"testing"

	"github.com/google/go-cmp/cmp"
	"google.golang.org/protobuf/testing/protocmp"

	meshconfig "istio.io/api/mesh/v1alpha1"
	"istio.io/istio/pilot/pkg/features"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/serviceregistry/provider"
	"istio.io/istio/pkg/cluster"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/host"
	"istio.io/istio/pkg/config/protocol"
	dnsProto "istio.io/istio/pkg/dns/proto"
	dnsServer "istio.io/istio/pkg/dns/server"
	"istio.io/istio/pkg/test"
)

func TestNameTable(t *testing.T) {
	test.SetForTest(t, &features.EnableDualStack, true)
	mesh := &meshconfig.MeshConfig{RootNamespace: "istio-system"}
	proxy := &model.Proxy{
		IPAddresses: []string{"9.9.9.9"},
		Metadata:    &model.NodeMetadata{},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}
	nw1proxy := &model.Proxy{
		IPAddresses: []string{"9.9.9.9"},
		Metadata:    &model.NodeMetadata{Network: "nw1"},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}
	cl1proxy := &model.Proxy{
		IPAddresses: []string{"9.9.9.9"},
		Metadata:    &model.NodeMetadata{ClusterID: "cl1"},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}

	cl1DualStackproxy := &model.Proxy{
		IPAddresses: []string{"9.9.9.9", "2001:2::2001"},
		Metadata:    &model.NodeMetadata{ClusterID: "cl1"},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}

	pod1 := &model.Proxy{
		IPAddresses: []string{"1.2.3.4"},
		Metadata:    &model.NodeMetadata{},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}
	pod2 := &model.Proxy{
		IPAddresses: []string{"9.6.7.8"},
		Metadata:    &model.NodeMetadata{Network: "nw2", ClusterID: "cl2"},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}
	pod3 := &model.Proxy{
		IPAddresses: []string{"19.6.7.8"},
		Metadata:    &model.NodeMetadata{Network: "nw1"},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}
	pod4 := &model.Proxy{
		IPAddresses: []string{"9.16.7.8"},
		Metadata:    &model.NodeMetadata{ClusterID: "cl1"},
		Type:        model.SidecarProxy,
		DNSDomain:   "testns.svc.cluster.local",
	}

	headlessService := &model.Service{
		Hostname:       host.Name("headless-svc.testns.svc.cluster.local"),
		DefaultAddress: constants.UnspecifiedIP,
		Ports: model.PortList{&model.Port{
			Name:     "tcp-port",
			Port:     9000,
			Protocol: protocol.TCP,
		}},
		Resolution: model.Passthrough,
		Attributes: model.ServiceAttributes{
			Name:            "headless-svc",
			Namespace:       "testns",
			ServiceRegistry: provider.Kubernetes,
		},
	}

	headlessServiceWithPublishNonReadyEnabled := &model.Service{
		Hostname:       host.Name("headless-svc.testns.svc.cluster.local"),
		DefaultAddress: constants.UnspecifiedIP,
		Ports: model.PortList{&model.Port{
			Name:     "tcp-port",
			Port:     9000,
			Protocol: protocol.TCP,
		}},
		Resolution: model.Passthrough,
		Attributes: model.ServiceAttributes{
			Name:            "headless-svc",
			Namespace:       "testns",
			ServiceRegistry: provider.Kubernetes,
			K8sAttributes:   model.K8sAttributes{PublishNotReadyAddresses: true},
		},
	}

	headlessServiceForServiceEntry := &model.Service{
		Hostname:       host.Name("foo.bar.com"),
		DefaultAddress: constants.UnspecifiedIP,
		Ports: model.PortList{&model.Port{
			Name:     "tcp-port",
			Port:     9000,
			Protocol: protocol.TCP,
		}},
		Resolution: model.Passthrough,
		Attributes: model.ServiceAttributes{
			Name:            "foo.bar.com",
			Namespace:       "testns",
			ServiceRegistry: provider.External,
			LabelSelectors:  map[string]string{"wl": "headless-foobar"},
		},
	}

	wildcardService := &model.Service{
		Hostname:       host.Name("*.testns.svc.cluster.local"),
		DefaultAddress: "172.10.10.10",
		Ports: model.PortList{
			&model.Port{
				Name:     "tcp-port",
				Port:     9000,
				Protocol: protocol.TCP,
			},
			&model.Port{
				Name:     "http-port",
				Port:     8000,
				Protocol: protocol.HTTP,
			},
		},
		Resolution: model.ClientSideLB,
		Attributes: model.ServiceAttributes{
			Name:            "wildcard-svc",
			Namespace:       "testns",
			ServiceRegistry: provider.Kubernetes,
		},
	}

	cidrService := &model.Service{
		Hostname:       host.Name("*.testns.svc.cluster.local"),
		DefaultAddress: "172.217.0.0/16",
		Ports: model.PortList{
			&model.Port{
				Name:     "tcp-port",
				Port:     9000,
				Protocol: protocol.TCP,
			},
			&model.Port{
				Name:     "http-port",
				Port:     8000,
				Protocol: protocol.HTTP,
			},
		},
		Resolution: model.ClientSideLB,
		Attributes: model.ServiceAttributes{
			Name:            "cidr-svc",
			Namespace:       "testns",
			ServiceRegistry: provider.Kubernetes,
		},
	}

	serviceWithVIP1 := &model.Service{
		Hostname:       host.Name("mysql.foo.bar"),
		DefaultAddress: "10.0.0.5",
		Ports: model.PortList{
			&model.Port{
				Name:     "tcp",
				Port:     3306,
				Protocol: protocol.TCP,
			},
		},
		Resolution: model.Passthrough,
		Attributes: model.ServiceAttributes{
			Name:            "mysql-svc",
			Namespace:       "testns",
			ServiceRegistry: provider.External,
		},
	}
	serviceWithVIP2 := serviceWithVIP1.DeepCopy()
	serviceWithVIP2.DefaultAddress = "10.0.0.6"

	decoratedService := serviceWithVIP1.DeepCopy()
	decoratedService.DefaultAddress = "10.0.0.7"
	decoratedService.Attributes.ServiceRegistry = provider.Kubernetes

	dualStackService := &model.Service{
		Hostname:       host.Name("dual.foo.bar"),
		DefaultAddress: "10.0.0.8",
		ClusterVIPs: model.AddressMap{
			Addresses: map[cluster.ID][]string{
				"cl1": {"2001:2::", "10.0.0.8"},
			},
		},
		Ports: model.PortList{
			&model.Port{
				Name:     "tcp",
				Port:     3306,
				Protocol: protocol.TCP,
			},
		},
		Resolution: model.Passthrough,
		Attributes: model.ServiceAttributes{
			Name:            "mysql-svc",
			Namespace:       "testns",
			ServiceRegistry: provider.External,
		},
	}

	push := model.NewPushContext()
	push.Mesh = mesh
	push.AddPublicServices([]*model.Service{headlessService})
	push.AddServiceInstances(headlessService,
		makeServiceInstances(pod1, headlessService, "pod1", "headless-svc", model.Healthy))
	push.AddServiceInstances(headlessService,
		makeServiceInstances(pod2, headlessService, "pod2", "headless-svc", model.Healthy))
	push.AddServiceInstances(headlessService,
		makeServiceInstances(pod3, headlessService, "pod3", "headless-svc", model.Healthy))
	push.AddServiceInstances(headlessService,
		makeServiceInstances(pod4, headlessService, "pod4", "headless-svc", model.Healthy))

	uhpush := model.NewPushContext()
	uhpush.Mesh = mesh
	uhpush.AddPublicServices([]*model.Service{headlessService})
	uhpush.AddServiceInstances(headlessService,
		makeServiceInstances(pod1, headlessService, "pod1", "headless-svc", model.Healthy))
	uhpush.AddServiceInstances(headlessService,
		makeServiceInstances(pod2, headlessService, "pod2", "headless-svc", model.UnHealthy))

	uhreadypush := model.NewPushContext()
	uhreadypush.Mesh = mesh
	uhreadypush.AddPublicServices([]*model.Service{headlessServiceWithPublishNonReadyEnabled})
	uhreadypush.AddServiceInstances(headlessService,
		makeServiceInstances(pod1, headlessService, "pod1", "headless-svc", model.Healthy))
	uhreadypush.AddServiceInstances(headlessService,
		makeServiceInstances(pod2, headlessService, "pod2", "headless-svc", model.UnHealthy))

	wpush := model.NewPushContext()
	wpush.Mesh = mesh
	wpush.AddPublicServices([]*model.Service{wildcardService})

	cpush := model.NewPushContext()
	cpush.Mesh = mesh
	cpush.AddPublicServices([]*model.Service{cidrService})

	sepush := model.NewPushContext()
	sepush.Mesh = mesh
	sepush.AddPublicServices([]*model.Service{headlessServiceForServiceEntry})
	sepush.AddServiceInstances(headlessServiceForServiceEntry,
		makeServiceInstances(pod1, headlessServiceForServiceEntry, "", "", model.Healthy))
	sepush.AddServiceInstances(headlessServiceForServiceEntry,
		makeServiceInstances(pod2, headlessServiceForServiceEntry, "", "", model.Healthy))
	sepush.AddServiceInstances(headlessServiceForServiceEntry,
		makeServiceInstances(pod3, headlessServiceForServiceEntry, "", "", model.Healthy))
	sepush.AddServiceInstances(headlessServiceForServiceEntry,
		makeServiceInstances(pod4, headlessServiceForServiceEntry, "", "", model.Healthy))

	cases := []struct {
		name                       string
		proxy                      *model.Proxy
		push                       *model.PushContext
		enableMultiClusterHeadless bool
		expectedNameTable          *dnsProto.NameTable
	}{
		{
			name:  "headless service pods",
			proxy: proxy,
			push:  push,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"pod1.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4"},
						Registry:  "Kubernetes",
						Shortname: "pod1.headless-svc",
						Namespace: "testns",
					},
					"pod2.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod2.headless-svc",
						Namespace: "testns",
					},
					"pod3.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"19.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod3.headless-svc",
						Namespace: "testns",
					},
					"pod4.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod4.headless-svc",
						Namespace: "testns",
					},
					"headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4", "9.6.7.8", "19.6.7.8", "9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "headless-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:  "headless service pods with network isolation",
			proxy: nw1proxy,
			push:  push,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"pod1.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4"},
						Registry:  "Kubernetes",
						Shortname: "pod1.headless-svc",
						Namespace: "testns",
					},
					"pod3.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"19.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod3.headless-svc",
						Namespace: "testns",
					},
					"pod4.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod4.headless-svc",
						Namespace: "testns",
					},
					"headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4", "19.6.7.8", "9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "headless-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:  "multi cluster headless service pods",
			proxy: cl1proxy,
			push:  push,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"pod1.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4"},
						Registry:  "Kubernetes",
						Shortname: "pod1.headless-svc",
						Namespace: "testns",
					},
					"pod2.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod2.headless-svc",
						Namespace: "testns",
					},
					"pod3.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"19.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod3.headless-svc",
						Namespace: "testns",
					},
					"pod4.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod4.headless-svc",
						Namespace: "testns",
					},
					"headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4", "19.6.7.8", "9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "headless-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:                       "multi cluster headless service pods with multi cluster enabled",
			proxy:                      cl1proxy,
			push:                       push,
			enableMultiClusterHeadless: true,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"pod1.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4"},
						Registry:  "Kubernetes",
						Shortname: "pod1.headless-svc",
						Namespace: "testns",
					},
					"pod2.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod2.headless-svc",
						Namespace: "testns",
					},
					"pod3.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"19.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod3.headless-svc",
						Namespace: "testns",
					},
					"pod4.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod4.headless-svc",
						Namespace: "testns",
					},
					"headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4", "9.6.7.8", "19.6.7.8", "9.16.7.8"},
						Registry:  "Kubernetes",
						Shortname: "headless-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:                       "headless service with unhealthy pods",
			proxy:                      cl1proxy,
			push:                       uhpush,
			enableMultiClusterHeadless: true,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"pod1.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4"},
						Registry:  "Kubernetes",
						Shortname: "pod1.headless-svc",
						Namespace: "testns",
					},
					"headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4"},
						Registry:  "Kubernetes",
						Shortname: "headless-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:  "wildcard service pods",
			proxy: proxy,
			push:  wpush,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"*.testns.svc.cluster.local": {
						Ips:       []string{"172.10.10.10"},
						Registry:  "Kubernetes",
						Shortname: "wildcard-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:                       "publish unready headless service with unhealthy pods",
			proxy:                      cl1proxy,
			push:                       uhreadypush,
			enableMultiClusterHeadless: true,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"pod1.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4"},
						Registry:  "Kubernetes",
						Shortname: "pod1.headless-svc",
						Namespace: "testns",
					},
					"pod2.headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"9.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "pod2.headless-svc",
						Namespace: "testns",
					},
					"headless-svc.testns.svc.cluster.local": {
						Ips:       []string{"1.2.3.4", "9.6.7.8"},
						Registry:  "Kubernetes",
						Shortname: "headless-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:  "wildcard service pods",
			proxy: proxy,
			push:  wpush,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"*.testns.svc.cluster.local": {
						Ips:       []string{"172.10.10.10"},
						Registry:  "Kubernetes",
						Shortname: "wildcard-svc",
						Namespace: "testns",
					},
				},
			},
		},
		{
			name:  "cidr service",
			proxy: proxy,
			push:  cpush,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{},
			},
		},
		{
			name:  "service entry with resolution = NONE",
			proxy: proxy,
			push:  sepush,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"foo.bar.com": {
						Ips:      []string{"1.2.3.4", "9.6.7.8", "19.6.7.8", "9.16.7.8"},
						Registry: "External",
					},
				},
			},
		},
		{
			name:  "dual stack",
			proxy: cl1DualStackproxy,
			push: func() *model.PushContext {
				push := model.NewPushContext()
				push.Mesh = mesh
				push.AddPublicServices([]*model.Service{dualStackService})
				return push
			}(),
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"dual.foo.bar": {
						Ips:      []string{"2001:2::", "10.0.0.8"},
						Registry: "External",
					},
				},
			},
		},
		{
			name:  "service entry with resolution = NONE with network isolation",
			proxy: nw1proxy,
			push:  sepush,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"foo.bar.com": {
						Ips:      []string{"1.2.3.4", "19.6.7.8", "9.16.7.8"},
						Registry: "External",
					},
				},
			},
		},
		{
			name:  "multi cluster service entry with resolution = NONE",
			proxy: cl1proxy,
			push:  sepush,
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					"foo.bar.com": {
						Ips:      []string{"1.2.3.4", "19.6.7.8", "9.16.7.8"},
						Registry: "External",
					},
				},
			},
		},
		{
			name:  "service entry with multiple VIPs",
			proxy: proxy,
			push: func() *model.PushContext {
				push := model.NewPushContext()
				push.Mesh = mesh
				push.AddPublicServices([]*model.Service{serviceWithVIP1, serviceWithVIP2})
				return push
			}(),
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					serviceWithVIP1.Hostname.String(): {
						Ips:      []string{serviceWithVIP1.DefaultAddress},
						Registry: provider.External.String(),
					},
				},
			},
		},
		{
			name:  "service entry as a decorator(created before k8s service)",
			proxy: proxy,
			push: func() *model.PushContext {
				push := model.NewPushContext()
				push.Mesh = mesh
				push.AddPublicServices([]*model.Service{serviceWithVIP1, decoratedService})
				return push
			}(),
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					serviceWithVIP1.Hostname.String(): {
						Ips:       []string{decoratedService.DefaultAddress},
						Registry:  provider.Kubernetes.String(),
						Shortname: decoratedService.Attributes.Name,
						Namespace: decoratedService.Attributes.Namespace,
					},
				},
			},
		},
		{
			name:  "service entry as a decorator(created after k8s service)",
			proxy: proxy,
			push: func() *model.PushContext {
				push := model.NewPushContext()
				push.Mesh = mesh
				push.AddPublicServices([]*model.Service{decoratedService, serviceWithVIP2})
				return push
			}(),
			expectedNameTable: &dnsProto.NameTable{
				Table: map[string]*dnsProto.NameTable_NameInfo{
					serviceWithVIP1.Hostname.String(): {
						Ips:       []string{decoratedService.DefaultAddress},
						Registry:  provider.Kubernetes.String(),
						Shortname: decoratedService.Attributes.Name,
						Namespace: decoratedService.Attributes.Namespace,
					},
				},
			},
		},
	}
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			tt.proxy.SidecarScope = model.DefaultSidecarScopeForNamespace(tt.push, "default")
			tt.proxy.DiscoverIPMode()
			if diff := cmp.Diff(dnsServer.BuildNameTable(dnsServer.Config{
				Node:                        tt.proxy,
				Push:                        tt.push,
				MulticlusterHeadlessEnabled: tt.enableMultiClusterHeadless,
			}), tt.expectedNameTable, protocmp.Transform()); diff != "" {
				t.Fatalf("got diff: %v", diff)
			}
		})
	}
}

// nolint
func makeServiceInstances(proxy *model.Proxy, service *model.Service, hostname, subdomain string, healthStatus model.HealthStatus) map[int][]*model.IstioEndpoint {
	instances := make(map[int][]*model.IstioEndpoint)
	for _, port := range service.Ports {
		instances[port.Port] = makeInstances(proxy, service, port.Port, port.Port, healthStatus)
		instances[port.Port][0].HostName = hostname
		instances[port.Port][0].SubDomain = subdomain
		instances[port.Port][0].Network = proxy.Metadata.Network
		instances[port.Port][0].Locality.ClusterID = proxy.Metadata.ClusterID
	}
	return instances
}

func makeInstances(proxy *model.Proxy, svc *model.Service, servicePort int, targetPort int,
	healthStatus model.HealthStatus,
) []*model.IstioEndpoint {
	ret := make([]*model.IstioEndpoint, 0)
	for _, p := range svc.Ports {
		if p.Port != servicePort {
			continue
		}
		ret = append(ret, &model.IstioEndpoint{
			Addresses:       proxy.IPAddresses,
			ServicePortName: p.Name,
			EndpointPort:    uint32(targetPort),
			HealthStatus:    healthStatus,
		})
	}
	return ret
}
