/*
Copyright 2025 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 (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kubeinformers "k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes/fake"

	"sigs.k8s.io/external-dns/endpoint"
)

func TestEndpointsForHostname(t *testing.T) {
	tests := []struct {
		name             string
		hostname         string
		targets          endpoint.Targets
		ttl              endpoint.TTL
		providerSpecific endpoint.ProviderSpecific
		setIdentifier    string
		resource         string
		expected         []*endpoint.Endpoint
	}{
		{
			name:     "A record targets",
			hostname: "example.com",
			targets:  endpoint.Targets{"192.0.2.1", "192.0.2.2"},
			ttl:      endpoint.TTL(300),
			providerSpecific: endpoint.ProviderSpecific{
				{Name: "provider", Value: "value"},
			},
			setIdentifier: "identifier",
			resource:      "resource",
			expected: []*endpoint.Endpoint{
				{
					DNSName:          "example.com",
					Targets:          endpoint.Targets{"192.0.2.1", "192.0.2.2"},
					RecordType:       endpoint.RecordTypeA,
					RecordTTL:        endpoint.TTL(300),
					ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
					SetIdentifier:    "identifier",
					Labels:           map[string]string{endpoint.ResourceLabelKey: "resource"},
				},
			},
		},
		{
			name:     "AAAA record targets",
			hostname: "example.com",
			targets:  endpoint.Targets{"2001:db8::1", "2001:db8::2"},
			ttl:      endpoint.TTL(300),
			providerSpecific: endpoint.ProviderSpecific{
				{Name: "provider", Value: "value"},
			},
			setIdentifier: "identifier",
			resource:      "resource",
			expected: []*endpoint.Endpoint{
				{
					DNSName:          "example.com",
					Targets:          endpoint.Targets{"2001:db8::1", "2001:db8::2"},
					RecordType:       endpoint.RecordTypeAAAA,
					RecordTTL:        endpoint.TTL(300),
					ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
					SetIdentifier:    "identifier",
					Labels:           map[string]string{endpoint.ResourceLabelKey: "resource"},
				},
			},
		},
		{
			name:     "CNAME record targets",
			hostname: "example.com",
			targets:  endpoint.Targets{"cname.example.com"},
			ttl:      endpoint.TTL(300),
			providerSpecific: endpoint.ProviderSpecific{
				{Name: "provider", Value: "value"},
			},
			setIdentifier: "identifier",
			resource:      "resource",
			expected: []*endpoint.Endpoint{
				{
					DNSName:          "example.com",
					Targets:          endpoint.Targets{"cname.example.com"},
					RecordType:       endpoint.RecordTypeCNAME,
					RecordTTL:        endpoint.TTL(300),
					ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
					SetIdentifier:    "identifier",
					Labels:           map[string]string{endpoint.ResourceLabelKey: "resource"},
				},
			},
		},
		{
			name:             "No targets",
			hostname:         "example.com",
			targets:          endpoint.Targets{},
			ttl:              endpoint.TTL(300),
			providerSpecific: endpoint.ProviderSpecific{},
			setIdentifier:    "",
			resource:         "",
			expected:         []*endpoint.Endpoint(nil),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := EndpointsForHostname(tt.hostname, tt.targets, tt.ttl, tt.providerSpecific, tt.setIdentifier, tt.resource)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestEndpointTargetsFromServices(t *testing.T) {
	tests := []struct {
		name      string
		services  []*corev1.Service
		namespace string
		selector  map[string]string
		expected  endpoint.Targets
		wantErr   bool
	}{
		{
			name:      "no services",
			services:  []*corev1.Service{},
			namespace: "default",
			selector:  map[string]string{"app": "nginx"},
			expected:  endpoint.Targets{},
		},
		{
			name: "matching service with external IPs",
			services: []*corev1.Service{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "svc1",
						Namespace: "default",
					},
					Spec: corev1.ServiceSpec{
						Selector:    map[string]string{"app": "nginx"},
						ExternalIPs: []string{"192.0.2.1", "158.123.32.23"},
					},
				},
			},
			namespace: "default",
			selector:  map[string]string{"app": "nginx"},
			expected:  endpoint.Targets{"192.0.2.1", "158.123.32.23"},
		},
		{
			name: "no matching service as service without selector",
			services: []*corev1.Service{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "svc1",
						Namespace: "default",
					},
					Spec: corev1.ServiceSpec{
						ExternalIPs: []string{"192.0.2.1"},
					},
				},
			},
			namespace: "default",
			selector:  map[string]string{"app": "nginx"},
			expected:  endpoint.Targets{},
		},
		{
			name: "matching service with load balancer IP",
			services: []*corev1.Service{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "svc2",
						Namespace: "default",
					},
					Spec: corev1.ServiceSpec{
						Selector: map[string]string{"app": "nginx"},
					},
					Status: corev1.ServiceStatus{
						LoadBalancer: corev1.LoadBalancerStatus{
							Ingress: []corev1.LoadBalancerIngress{
								{IP: "192.0.2.2"},
							},
						},
					},
				},
			},
			namespace: "default",
			selector:  map[string]string{"app": "nginx"},
			expected:  endpoint.Targets{"192.0.2.2"},
		},
		{
			name: "matching service with load balancer hostname",
			services: []*corev1.Service{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "svc3",
						Namespace: "default",
					},
					Spec: corev1.ServiceSpec{
						Selector: map[string]string{"app": "nginx"},
					},
					Status: corev1.ServiceStatus{
						LoadBalancer: corev1.LoadBalancerStatus{
							Ingress: []corev1.LoadBalancerIngress{
								{Hostname: "lb.example.com"},
							},
						},
					},
				},
			},
			namespace: "default",
			selector:  map[string]string{"app": "nginx"},
			expected:  endpoint.Targets{"lb.example.com"},
		},
		{
			name: "no matching services",
			services: []*corev1.Service{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "svc4",
						Namespace: "default",
					},
					Spec: corev1.ServiceSpec{
						Selector: map[string]string{"app": "apache"},
					},
				},
			},
			namespace: "default",
			selector:  map[string]string{"app": "nginx"},
			expected:  endpoint.Targets{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			client := fake.NewClientset()
			informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0,
				kubeinformers.WithNamespace(tt.namespace))
			serviceInformer := informerFactory.Core().V1().Services()

			for _, svc := range tt.services {
				_, err := client.CoreV1().Services(tt.namespace).Create(context.Background(), svc, metav1.CreateOptions{})
				assert.NoError(t, err)

				err = serviceInformer.Informer().GetIndexer().Add(svc)
				assert.NoError(t, err)
			}

			result, err := EndpointTargetsFromServices(serviceInformer, tt.namespace, tt.selector)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expected, result)
			}
		})
	}
}

func TestEndpointTargetsFromServicesWithFixtures(t *testing.T) {
	svcInformer, err := svcInformerWithServices(2, 9)
	assert.NoError(t, err)

	sel := map[string]string{"app": "nginx", "env": "prod"}

	targets, err := EndpointTargetsFromServices(svcInformer, "default", sel)
	assert.NoError(t, err)
	assert.Equal(t, 2, targets.Len())
}
