package task

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/go-logr/logr/testr"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/repository"
)

func TestVPCPeering_DeleteDeletesAllPeeringsForDataPlane(t *testing.T) {
	ctx := context.Background()
	vpcPeeringTask, kubeClient := newVPCPeeringTestTask(t,
		buildVPCPeering("peering-1", map[string]string{
			v1alpha1.DataPlaneIDKey: "dp-1",
		}),
		buildVPCPeering("peering-2", map[string]string{
			v1alpha1.DataPlaneIDKey: "dp-1",
			v1alpha1.VPCNameKey:     "vpc-1",
		}),
		buildVPCPeering("peering-other", map[string]string{
			v1alpha1.DataPlaneIDKey: "dp-2",
		}),
	)

	result := vpcPeeringTask.Delete(ctx)

	require.NoError(t, result.Error())
	require.True(t, result.IsDoneState())

	for _, name := range []string{"peering-1", "peering-2"} {
		peering := &v1alpha1.VPCPeering{}
		require.NoError(t, kubeClient.Get(ctx, client.ObjectKey{Namespace: "dataplane-metadata", Name: name}, peering))
		assert.Equal(t, consts.AnnoValTrue, peering.Annotations[consts.AnnotationKeyConfirmDeletion])
		assert.False(t, peering.DeletionTimestamp.IsZero())
	}

	other := &v1alpha1.VPCPeering{}
	require.NoError(t, kubeClient.Get(ctx, client.ObjectKey{Namespace: "dataplane-metadata", Name: "peering-other"}, other))
	assert.True(t, other.DeletionTimestamp.IsZero())
}

func TestVPCPeering_DeletePanicsAndStopsDestroyOnListError(t *testing.T) {
	listErr := errors.New("list vpcpeerings failed")
	vpcPeeringTask := newVPCPeeringTaskWithRW(&fakeVPCPeeringRW{listErr: listErr})
	sentinel := &sentinelTask{name: "Internet"}

	results := deploy.NewDeployer([]deploy.Task{sentinel, vpcPeeringTask}, nil, testr.New(t)).Destroy(context.Background())

	require.Len(t, results, 1)
	assert.ErrorContains(t, results[0].RawError(), listErr.Error())
	assert.Equal(t, 30*time.Second, k8s.ShouldRequeue(results[0].RawError()).RequeueAfter)
	assert.False(t, sentinel.deleted)
}

func TestVPCPeering_DeletePanicsAndStopsDestroyOnDeleteError(t *testing.T) {
	deleteErr := errors.New("delete vpcpeering failed")
	vpcPeeringTask := newVPCPeeringTaskWithRW(&fakeVPCPeeringRW{
		items:     []v1alpha1.VPCPeering{*buildVPCPeering("peering-1", map[string]string{v1alpha1.DataPlaneIDKey: "dp-1"})},
		deleteErr: deleteErr,
	})
	sentinel := &sentinelTask{name: "Internet"}

	results := deploy.NewDeployer([]deploy.Task{sentinel, vpcPeeringTask}, nil, testr.New(t)).Destroy(context.Background())

	require.Len(t, results, 1)
	assert.ErrorContains(t, results[0].RawError(), deleteErr.Error())
	assert.Equal(t, 30*time.Second, k8s.ShouldRequeue(results[0].RawError()).RequeueAfter)
	assert.False(t, sentinel.deleted)
}

func TestVPCPeering_DeleteIgnoresNotFound(t *testing.T) {
	vpcPeeringTask := newVPCPeeringTaskWithRW(&fakeVPCPeeringRW{
		items: []v1alpha1.VPCPeering{*buildVPCPeering("peering-1", map[string]string{v1alpha1.DataPlaneIDKey: "dp-1"})},
		deleteErr: apierrors.NewNotFound(v1alpha1.GroupVersion.WithResource("vpcpeerings").GroupResource(),
			"peering-1"),
	})

	result := vpcPeeringTask.Delete(context.Background())

	require.NoError(t, result.Error())
	assert.True(t, result.IsDoneState())
}

func TestVPCPeering_PostDeleteRequeuesWhenPeeringsRemain(t *testing.T) {
	vpcPeeringTask, _ := newVPCPeeringTestTask(t,
		buildVPCPeering("peering-1", map[string]string{v1alpha1.DataPlaneIDKey: "dp-1"}),
	)

	result := vpcPeeringTask.PostDelete(context.Background())

	require.Error(t, result.RawError())
	assert.ErrorContains(t, result.RawError(), "vpcpeering crs not deleted: 1 remaining")
	assert.Equal(t, 30*time.Second, k8s.ShouldRequeue(result.RawError()).RequeueAfter)
}

func TestVPCPeering_PostDeleteRequeuesOnListError(t *testing.T) {
	listErr := errors.New("list vpcpeerings failed")
	vpcPeeringTask := newVPCPeeringTaskWithRW(&fakeVPCPeeringRW{listErr: listErr})

	result := vpcPeeringTask.PostDelete(context.Background())

	require.Error(t, result.RawError())
	assert.ErrorContains(t, result.RawError(), listErr.Error())
	assert.Equal(t, 30*time.Second, k8s.ShouldRequeue(result.RawError()).RequeueAfter)
}

func newVPCPeeringTestTask(t *testing.T, peerings ...*v1alpha1.VPCPeering) (*VPCPeering, client.Client) {
	t.Helper()

	scheme := runtime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(scheme))

	objects := make([]client.Object, 0, len(peerings))
	for _, peering := range peerings {
		objects = append(objects, peering)
	}
	kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()

	return newVPCPeeringTaskWithRW(repository.NewVPCPeeringReaderWriter(kubeClient)), kubeClient
}

func newVPCPeeringTaskWithRW(rw repository.VPCPeeringReaderWriter) *VPCPeering {
	return NewVPCPeering(&Context{
		DataPlane: &v1alpha1.DataPlane{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "dataplane-metadata",
				Name:      "dp-1",
			},
		},
		VpcPeeringRW: rw,
	})
}

func buildVPCPeering(name string, labels map[string]string) *v1alpha1.VPCPeering {
	return &v1alpha1.VPCPeering{
		ObjectMeta: metav1.ObjectMeta{
			Name:       name,
			Namespace:  "dataplane-metadata",
			Labels:     labels,
			Finalizers: []string{v1alpha1.FinalizerVPCPeering},
		},
		Spec: v1alpha1.VPCPeeringSpec{
			DataPlane: v1alpha1.Reference{
				Namespace: "dataplane-metadata",
				Name:      "dp-1",
			},
			Provider: consts.ProviderTypeEKS,
			Region:   "us-west-2",
			Target: v1alpha1.Target{AWS: &v1alpha1.AWS{
				AccountID: "5678",
				VpcID:     "vpc-peer",
				PeerCIDR:  "10.0.0.0/16",
				Region:    "us-west-2",
			}},
		},
	}
}

type fakeVPCPeeringRW struct {
	items     []v1alpha1.VPCPeering
	listErr   error
	deleteErr error
}

func (f *fakeVPCPeeringRW) Get(_ context.Context, _ client.ObjectKey, _ *v1alpha1.VPCPeering) error {
	return nil
}

func (f *fakeVPCPeeringRW) List(_ context.Context, list *v1alpha1.VPCPeeringList, _ ...client.ListOption) error {
	if f.listErr != nil {
		return f.listErr
	}
	list.Items = append(list.Items, f.items...)
	return nil
}

func (f *fakeVPCPeeringRW) Create(_ context.Context, _ *v1alpha1.VPCPeering) error {
	return nil
}

func (f *fakeVPCPeeringRW) Update(_ context.Context, _ *v1alpha1.VPCPeering) error {
	return nil
}

func (f *fakeVPCPeeringRW) Delete(_ context.Context, _ *v1alpha1.VPCPeering) error {
	return f.deleteErr
}

func (f *fakeVPCPeeringRW) Patch(_ context.Context, _ *v1alpha1.VPCPeering, _ client.Patch) error {
	return nil
}

func (f *fakeVPCPeeringRW) UpdateStatus(_ context.Context, _ *v1alpha1.VPCPeering) error {
	return nil
}

type sentinelTask struct {
	name    string
	deleted bool
}

func (t *sentinelTask) Name() string {
	return t.name
}

func (t *sentinelTask) Category() deploy.TaskCategory {
	return deploy.ClusterTask
}

func (t *sentinelTask) Create(context.Context) deploy.Result {
	return deploy.Done()
}

func (t *sentinelTask) PostCreate(context.Context) deploy.Result {
	return deploy.Done()
}

func (t *sentinelTask) Delete(context.Context) deploy.Result {
	t.deleted = true
	return deploy.Done()
}

func (t *sentinelTask) PostDelete(context.Context) deploy.Result {
	return deploy.Done()
}

func (t *sentinelTask) MetricsStartTime() *time.Time {
	return nil
}
