package aks

import (
	"context"
	"testing"

	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	azurenetworkv1beta1 "github.com/upbound/provider-azure/apis/network/v1beta1"
	gomock "go.uber.org/mock/gomock"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/yaml"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/azure"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkendpoint/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/repository"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/test/testdata"
)

func fakeTaskContext(t *testing.T) *task.Context {
	t.Helper()

	s := runtime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(s))
	require.NoError(t, azurenetworkv1beta1.SchemeBuilder.AddToScheme(s))

	fakeVPC := &v1alpha1.VPC{}
	bs, err := testdata.LoadAKSVPCYAML()
	require.NoError(t, err)
	require.NoError(t, yaml.Unmarshal(bs, fakeVPC))
	fakeVPC.Status.VPC.ResourceID = "/subscriptions/subscription-id-1/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet"

	fakePLE := &v1alpha1.PrivateLinkEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-ple",
			Namespace: "test",
		},
		Spec: v1alpha1.PrivateLinkEndpointSpec{
			Provider:       "aks",
			ServiceName:    "pls-alias-123",
			RootDomainName: "privatelink.example.com",
			Region:         "eastus",
			VPCRef: v1alpha1.Reference{
				Namespace: "test",
				Name:      "test-vpc",
			},
		},
	}

	cli := fake.NewClientBuilder().
		WithScheme(s).
		WithObjects(fakeVPC, fakePLE).
		Build()

	c := &task.Context{
		Local:                 cli,
		Logger:                logr.Discard(),
		VPC:                   fakeVPC.DeepCopy(),
		PrivateLinkEndpoint:   fakePLE.DeepCopy(),
		PrivateLinkEndpointRW: repository.NewPrivateLinkEndpointReaderWriter(cli),
		Config: config.RootConfig{
			ResourceNamePrefix: "tidbcloud",
			AdditionalCloudTags: map[string]string{
				"extra": "tag",
			},
		},
	}
	c, err = c.Default()
	require.NoError(t, err)
	return c
}

func newFakeEndpointTask(c *task.Context) *endpoint {
	return NewEndpoint(c, nil).(*endpoint)
}

func newFakeEndpointTaskWithAzureCli(c *task.Context, cli azure.Client) *endpoint {
	return NewEndpoint(c, cli).(*endpoint)
}

func TestEndpoint_Create(t *testing.T) {
	tests := []struct {
		name    string
		mutate  func(*task.Context)
		wantErr bool
		verify  func(*testing.T, *task.Context, *endpoint)
	}{
		{
			name: "missing resource group",
			mutate: func(c *task.Context) {
				c.VPC.Status.ResourceGroup.ID = ""
			},
			wantErr: true,
		},
		{
			name: "missing service alias",
			mutate: func(c *task.Context) {
				c.PrivateLinkEndpoint.Spec.ServiceName = ""
			},
			wantErr: true,
		},
		{
			name: "missing subnets",
			mutate: func(c *task.Context) {
				c.VPC.Status.NodeSubnets = nil
				c.VPC.Status.PodSubnets = nil
			},
			wantErr: true,
		},
		{
			name:    "success",
			wantErr: false,
			verify: func(t *testing.T, c *task.Context, epTask *endpoint) {
				t.Helper()

				pe := &azurenetworkv1beta1.PrivateEndpoint{}
				err := c.Local.Get(context.Background(), types.NamespacedName{Name: epTask.EndpointName()}, pe)
				require.NoError(t, err)

				expectedLabels := util.MergeMaps(c.VPC.Labels, map[string]string{
					v1alpha1.PrivateLinkEndpointNameKey: c.PrivateLinkEndpoint.Name,
				})
				assert.Equal(t, expectedLabels, pe.Labels)
				assert.Equal(t, epTask.ProviderConfigName(), pe.Spec.ProviderConfigReference.Name)

				assert.Equal(t, ptr.To(c.PrivateLinkEndpoint.Spec.Region), pe.Spec.ForProvider.Location)
				assert.Equal(t, ptr.To(c.VPC.Status.ResourceGroup.ID), pe.Spec.ForProvider.ResourceGroupName)
				assert.Equal(t, ptr.To(c.VPC.Status.NodeSubnets[0].SubnetID), pe.Spec.ForProvider.SubnetID)
				require.Len(t, pe.Spec.ForProvider.PrivateServiceConnection, 1)
				assert.Equal(t, ptr.To(epTask.EndpointName()), pe.Spec.ForProvider.PrivateServiceConnection[0].Name)
				assert.Equal(t, ptr.To(true), pe.Spec.ForProvider.PrivateServiceConnection[0].IsManualConnection)
				assert.Equal(t, ptr.To(c.PrivateLinkEndpoint.Spec.ServiceName), pe.Spec.ForProvider.PrivateServiceConnection[0].PrivateConnectionResourceAlias)
				// Default PLS alias should have nil SubresourceNames and changefeed request message
				assert.Nil(t, pe.Spec.ForProvider.PrivateServiceConnection[0].SubresourceNames)
				assert.Equal(t, ptr.To("approve the connection to use changefeed on tidbcloud"), pe.Spec.ForProvider.PrivateServiceConnection[0].RequestMessage)

				expectedTags := controllers.ConvertToCloudTags(c.VPC.Labels, consts.ProviderTypeAKS)
				expectedTags = util.MergeMaps(expectedTags, c.Config.AdditionalCloudTags)
				assert.Equal(t, util.ToMapPointer(expectedTags), pe.Spec.ForProvider.Tags)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := fakeTaskContext(t)
			if tt.mutate != nil {
				tt.mutate(c)
			}
			epTask := newFakeEndpointTask(c)

			result := epTask.Create(context.Background())
			if tt.wantErr {
				assert.Error(t, result.Error())
				return
			}
			assert.True(t, result.IsDoneState())
			assert.NoError(t, result.Error())
			if tt.verify != nil {
				tt.verify(t, c, epTask)
			}
		})
	}
}

func TestEndpoint_PostCreate(t *testing.T) {
	tests := []struct {
		name    string
		setup   func(*testing.T, *task.Context, *endpoint)
		wantErr bool
		verify  func(*testing.T, *task.Context)
	}{
		{
			name:    "private endpoint not found",
			wantErr: true,
		},
		{
			name: "private endpoint not ready",
			setup: func(t *testing.T, c *task.Context, epTask *endpoint) {
				t.Helper()
				require.True(t, epTask.Create(context.Background()).IsDoneState())
			},
			wantErr: true,
			verify: func(t *testing.T, c *task.Context) {
				t.Helper()
				assert.Equal(t, v1alpha1.PrivateLinkEndpointPhaseCreating, c.PrivateLinkEndpoint.Status.Phase)
			},
		},
		{
			name: "ready but missing private ip",
			setup: func(t *testing.T, c *task.Context, epTask *endpoint) {
				t.Helper()
				require.True(t, epTask.Create(context.Background()).IsDoneState())

				pe := &azurenetworkv1beta1.PrivateEndpoint{
					ObjectMeta: metav1.ObjectMeta{
						Name: epTask.EndpointName(),
					},
				}
				require.NoError(t, c.Local.Get(context.Background(), types.NamespacedName{Name: epTask.EndpointName()}, pe))
				pe.Status.SetConditions(xpv1.Available())
				pe.Status.AtProvider.ID = ptr.To("pe-id")
				require.NoError(t, c.Local.Update(context.Background(), pe))
			},
			wantErr: true,
		},
		{
			name: "azure client not initialized",
			setup: func(t *testing.T, c *task.Context, epTask *endpoint) {
				t.Helper()
				require.True(t, epTask.Create(context.Background()).IsDoneState())

				pe := &azurenetworkv1beta1.PrivateEndpoint{
					ObjectMeta: metav1.ObjectMeta{
						Name: epTask.EndpointName(),
					},
				}
				require.NoError(t, c.Local.Get(context.Background(), types.NamespacedName{Name: epTask.EndpointName()}, pe))
				pe.Status.SetConditions(xpv1.Available())
				pe.Status.AtProvider.ID = ptr.To("pe-id")
				pe.Status.AtProvider.PrivateServiceConnection = []azurenetworkv1beta1.PrivateServiceConnectionObservation{
					{
						PrivateIPAddress: ptr.To("10.0.0.4"),
					},
				}
				require.NoError(t, c.Local.Update(context.Background(), pe))
			},
			wantErr: true,
			verify: func(t *testing.T, c *task.Context) {
				t.Helper()
				assert.Equal(t, "pe-id", c.PrivateLinkEndpoint.Status.ID)
				assert.Equal(t, "10.0.0.4", c.PrivateLinkEndpoint.Status.EndpointAddress)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := fakeTaskContext(t)
			epTask := newFakeEndpointTask(c)
			if tt.setup != nil {
				tt.setup(t, c, epTask)
			}

			result := epTask.PostCreate(context.Background())
			if tt.wantErr {
				assert.Error(t, result.Error())
			} else {
				assert.True(t, result.IsDoneState())
				assert.NoError(t, result.Error())
			}
			if tt.verify != nil {
				tt.verify(t, c)
			}
		})
	}
}

func TestEndpoint_Delete(t *testing.T) {
	t.Run("delete non-existent resource", func(t *testing.T) {
		c := fakeTaskContext(t)
		epTask := newFakeEndpointTask(c)

		result := epTask.Delete(context.Background())
		assert.True(t, result.IsDoneState())
	})

	t.Run("delete existing resource", func(t *testing.T) {
		c := fakeTaskContext(t)
		epTask := newFakeEndpointTask(c)

		require.True(t, epTask.Create(context.Background()).IsDoneState())
		result := epTask.Delete(context.Background())
		assert.True(t, result.IsDoneState())

		pe := &azurenetworkv1beta1.PrivateEndpoint{}
		err := c.Local.Get(context.Background(), types.NamespacedName{Name: epTask.EndpointName()}, pe)
		assert.Error(t, err)
		assert.True(t, apierrors.IsNotFound(err))
	})
}

func TestGetSubresourceNames(t *testing.T) {
	tests := []struct {
		name        string
		serviceName string
		want        []*string
	}{
		{
			name:        "PLS alias returns nil",
			serviceName: "pls-alias-123",
			want:        nil,
		},
		{
			name:        "MySQL resource by provider path",
			serviceName: "/subscriptions/sub-1/resourceGroups/rg/providers/Microsoft.DBforMySQL/servers/mydb",
			want:        []*string{ptr.To("mysqlServer")},
		},
		{
			name:        "MySQL resource by mysqlservers path",
			serviceName: "/subscriptions/sub-1/resourceGroups/rg/mysqlservers/mydb",
			want:        []*string{ptr.To("mysqlServer")},
		},
		{
			name:        "Storage account",
			serviceName: "/subscriptions/sub-1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/sa1",
			want:        []*string{ptr.To("blob")},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := getSubresourceNames(tt.serviceName)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestGetRequestMessage(t *testing.T) {
	tests := []struct {
		name        string
		serviceName string
		want        string
	}{
		{
			name:        "PLS alias returns changefeed message",
			serviceName: "pls-alias-123",
			want:        "approve the connection to use changefeed on tidbcloud",
		},
		{
			name:        "MySQL resource",
			serviceName: "/subscriptions/sub-1/providers/Microsoft.DBforMySQL/servers/mydb",
			want:        "approve the connection to use dm on tidbcloud",
		},
		{
			name:        "Storage account",
			serviceName: "/subscriptions/sub-1/providers/Microsoft.Storage/storageAccounts/sa1",
			want:        "approve the connection to use import on tidbcloud",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := getRequestMessage(tt.serviceName)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestMapAzureConnectionStatus(t *testing.T) {
	tests := []struct {
		name   string
		status string
		want   v1alpha1.PrivateLinkEndpointPhase
	}{
		{name: "approved", status: "Approved", want: v1alpha1.PrivateLinkEndpointPhaseEndpointAvailable},
		{name: "pending", status: "Pending", want: v1alpha1.PrivateLinkEndpointPhasePendingAcceptance},
		{name: "rejected", status: "Rejected", want: v1alpha1.PrivateLinkEndpointPhaseRejected},
		{name: "removed", status: "Removed", want: v1alpha1.PrivateLinkEndpointPhaseDeleting},
		{name: "unknown", status: "SomethingElse", want: v1alpha1.PrivateLinkEndpointPhaseFailed},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := mapAzureConnectionStatus(tt.status)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestEndpoint_PostCreate_WithAzureClient(t *testing.T) {
	setupReadyPE := func(t *testing.T, c *task.Context, epTask *endpoint) {
		t.Helper()
		require.True(t, epTask.Create(context.Background()).IsDoneState())

		pe := &azurenetworkv1beta1.PrivateEndpoint{}
		require.NoError(t, c.Local.Get(context.Background(), types.NamespacedName{Name: epTask.EndpointName()}, pe))
		pe.Status.SetConditions(xpv1.Available())
		pe.Status.AtProvider.ID = ptr.To("pe-id")
		pe.Status.AtProvider.PrivateServiceConnection = []azurenetworkv1beta1.PrivateServiceConnectionObservation{
			{PrivateIPAddress: ptr.To("10.0.0.4")},
		}
		require.NoError(t, c.Local.Update(context.Background(), pe))
	}

	t.Run("connection approved", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		mockCli := azure.NewMockClient(ctrl)
		mockPE := azure.NewMockPrivateEndpointsAPI(ctrl)

		c := fakeTaskContext(t)
		epTask := newFakeEndpointTaskWithAzureCli(c, mockCli)
		setupReadyPE(t, c, epTask)

		mockCli.EXPECT().PrivateEndpoints(c.VPC.Spec.AccountRef.AccountID).Return(mockPE, nil)
		mockPE.EXPECT().Get(gomock.Any(), c.VPC.Status.ResourceGroup.ID, epTask.EndpointName()).Return(
			armnetwork.PrivateEndpointsClientGetResponse{
				PrivateEndpoint: armnetwork.PrivateEndpoint{
					Properties: &armnetwork.PrivateEndpointProperties{
						ManualPrivateLinkServiceConnections: []*armnetwork.PrivateLinkServiceConnection{
							{
								Properties: &armnetwork.PrivateLinkServiceConnectionProperties{
									PrivateLinkServiceConnectionState: &armnetwork.PrivateLinkServiceConnectionState{
										Status: ptr.To("Approved"),
									},
								},
							},
						},
					},
				},
			}, nil)

		result := epTask.PostCreate(context.Background())
		assert.True(t, result.IsDoneState())
		assert.Equal(t, v1alpha1.PrivateLinkEndpointPhaseEndpointAvailable, c.PrivateLinkEndpoint.Status.Phase)
		assert.Equal(t, "10.0.0.4", c.PrivateLinkEndpoint.Status.EndpointAddress)
	})

	t.Run("connection pending acceptance", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		mockCli := azure.NewMockClient(ctrl)
		mockPE := azure.NewMockPrivateEndpointsAPI(ctrl)

		c := fakeTaskContext(t)
		epTask := newFakeEndpointTaskWithAzureCli(c, mockCli)
		setupReadyPE(t, c, epTask)

		mockCli.EXPECT().PrivateEndpoints(c.VPC.Spec.AccountRef.AccountID).Return(mockPE, nil)
		mockPE.EXPECT().Get(gomock.Any(), c.VPC.Status.ResourceGroup.ID, epTask.EndpointName()).Return(
			armnetwork.PrivateEndpointsClientGetResponse{
				PrivateEndpoint: armnetwork.PrivateEndpoint{
					Properties: &armnetwork.PrivateEndpointProperties{
						ManualPrivateLinkServiceConnections: []*armnetwork.PrivateLinkServiceConnection{
							{
								Properties: &armnetwork.PrivateLinkServiceConnectionProperties{
									PrivateLinkServiceConnectionState: &armnetwork.PrivateLinkServiceConnectionState{
										Status: ptr.To("Pending"),
									},
								},
							},
						},
					},
				},
			}, nil)

		result := epTask.PostCreate(context.Background())
		assert.Error(t, result.Error())
		assert.Equal(t, v1alpha1.PrivateLinkEndpointPhasePendingAcceptance, c.PrivateLinkEndpoint.Status.Phase)
	})

	t.Run("connection rejected", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		mockCli := azure.NewMockClient(ctrl)
		mockPE := azure.NewMockPrivateEndpointsAPI(ctrl)

		c := fakeTaskContext(t)
		epTask := newFakeEndpointTaskWithAzureCli(c, mockCli)
		setupReadyPE(t, c, epTask)

		mockCli.EXPECT().PrivateEndpoints(c.VPC.Spec.AccountRef.AccountID).Return(mockPE, nil)
		mockPE.EXPECT().Get(gomock.Any(), c.VPC.Status.ResourceGroup.ID, epTask.EndpointName()).Return(
			armnetwork.PrivateEndpointsClientGetResponse{
				PrivateEndpoint: armnetwork.PrivateEndpoint{
					Properties: &armnetwork.PrivateEndpointProperties{
						ManualPrivateLinkServiceConnections: []*armnetwork.PrivateLinkServiceConnection{
							{
								Properties: &armnetwork.PrivateLinkServiceConnectionProperties{
									PrivateLinkServiceConnectionState: &armnetwork.PrivateLinkServiceConnectionState{
										Status: ptr.To("Rejected"),
									},
								},
							},
						},
					},
				},
			}, nil)

		result := epTask.PostCreate(context.Background())
		assert.Error(t, result.Error())
		assert.Equal(t, v1alpha1.PrivateLinkEndpointPhaseRejected, c.PrivateLinkEndpoint.Status.Phase)
	})
}
