package bundle

import (
	"context"
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	api "github.com/aws/eks-anywhere-packages/api/v1alpha1"
	ctrlmocks "github.com/aws/eks-anywhere-packages/controllers/mocks"
)

const (
	testBundleRegistry = "public.ecr.aws/l0g8r8j6"
)

func givenMockClient(t *testing.T) *ctrlmocks.MockClient {
	goMockController := gomock.NewController(t)
	return ctrlmocks.NewMockClient(goMockController)
}

func givenBundle() *api.PackageBundle {
	return &api.PackageBundle{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testBundleName,
			Namespace: api.PackageNamespace,
		},
		Spec: api.PackageBundleSpec{
			Packages: []api.BundlePackage{
				{
					Name: "hello-eks-anywhere",
					Source: api.BundlePackageSource{
						Registry:   "public.ecr.aws/l0g8r8j6",
						Repository: "hello-eks-anywhere",
						Versions: []api.SourceVersion{
							{Name: "0.1.1", Digest: "sha256:deadbeef"},
							{Name: "0.1.0", Digest: "sha256:cafebabe"},
						},
					},
				},
			},
		},
	}
}

func givenPackageBundleController() *api.PackageBundleController {
	return &api.PackageBundleController{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster01",
			Namespace: api.PackageNamespace,
		},
		Spec: api.PackageBundleControllerSpec{
			ActiveBundle:         testBundleName,
			DefaultRegistry:      "public.ecr.aws/l0g8r8j6",
			DefaultImageRegistry: "783794618700.dkr.ecr.us-west-2.amazonaws.com",
			BundleRepository:     "eks-anywhere-packages-bundles",
		},
		Status: api.PackageBundleControllerStatus{
			State: api.BundleControllerStateActive,
		},
	}
}

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

	t.Run("golden path", func(t *testing.T) {
		t.Parallel()

		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)

		assert.NotNil(t, bundleClient)
	})
}

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

	ctx := context.Background()
	pbc := givenPackageBundleController()

	t.Run("golden path", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		testBundle := givenBundle()

		mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.AssignableToTypeOf(pbc)).SetArg(2, *pbc)
		mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.AssignableToTypeOf(testBundle)).SetArg(2, *testBundle)

		bundle, err := bundleClient.GetActiveBundle(ctx, "billy")

		assert.Equal(t, bundle.Name, testBundleName)
		assert.Equal(t, "hello-eks-anywhere", bundle.Spec.Packages[0].Name)
		assert.Equal(t, "public.ecr.aws/l0g8r8j6", bundle.Spec.Packages[0].Source.Registry)
		assert.NoError(t, err)
	})

	t.Run("no active bundle", func(t *testing.T) {
		pbc := givenPackageBundleController()
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		pbc.Spec.ActiveBundle = ""
		mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.AssignableToTypeOf(pbc)).SetArg(2, *pbc)

		bundle, err := bundleClient.GetActiveBundle(ctx, "billy")

		assert.Nil(t, bundle)
		assert.EqualError(t, err, "no activeBundle set in PackageBundleController")
	})

	t.Run("error path", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).Return(fmt.Errorf("oops"))

		_, err := bundleClient.GetActiveBundle(ctx, "billy")

		assert.EqualError(t, err, "getting PackageBundleController: oops")
	})

	t.Run("other error path", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.AssignableToTypeOf(pbc)).SetArg(2, *pbc)
		mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).Return(fmt.Errorf("oops"))

		_, err := bundleClient.GetActiveBundle(ctx, "billy")

		assert.EqualError(t, err, "oops")
	})
}

func doAndReturnBundleList(source *api.PackageBundleList) func(context.Context, *api.PackageBundleList, *client.ListOptions) error {
	return func(ctx context.Context, target *api.PackageBundleList, options *client.ListOptions) error {
		source.DeepCopyInto(target)
		return nil
	}
}

func doAndReturnBundle(_ context.Context, nn types.NamespacedName, theBundle *api.PackageBundle, _ ...client.GetOption) error {
	theBundle.Name = nn.Name
	return nil
}

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

	ctx := context.Background()
	namedBundle := &api.PackageBundle{}
	namedBundle.Name = "v1-21-1003"
	key := types.NamespacedName{
		Name:      "v1-21-1003",
		Namespace: "eksa-packages",
	}

	t.Run("already exists", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, key, gomock.Any()).DoAndReturn(doAndReturnBundle)

		actualBundle, err := bundleClient.GetBundle(ctx, "v1-21-1003")

		assert.NotNil(t, actualBundle)
		assert.NoError(t, err)
	})

	t.Run("already exists error", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(namedBundle)).Return(fmt.Errorf("boom"))

		actualBundle, err := bundleClient.GetBundle(ctx, "v1-21-1003")

		assert.Nil(t, actualBundle)
		assert.EqualError(t, err, "boom")
	})

	t.Run("returns nil when bundle does not", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		groupResource := schema.GroupResource{
			Group:    key.Name,
			Resource: "Namespace",
		}
		notFoundError := errors.NewNotFound(groupResource, key.Name)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(namedBundle)).Return(notFoundError)

		actualBundle, err := bundleClient.GetBundle(ctx, "v1-21-1003")

		assert.Nil(t, actualBundle)
		assert.NoError(t, err)
	})
}

func TestBundleClient_GetBundleList(t *testing.T) {
	t.Parallel()
	ctx := context.Background()

	t.Run("golden path", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		testBundleList := &api.PackageBundleList{
			Items: []api.PackageBundle{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "v1-16-1003",
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "v1-21-1002",
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "v1-21-1001",
					},
				},
			},
		}

		mockClient.EXPECT().
			List(ctx, gomock.Any(), &client.ListOptions{Namespace: api.PackageNamespace}).
			DoAndReturn(doAndReturnBundleList(testBundleList))

		bundleItems, err := bundleClient.GetBundleList(ctx)

		require.NoError(t, err)
		assert.ElementsMatch(t, bundleItems, testBundleList.Items)
	})

	t.Run("error scenario", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		actualList := &api.PackageBundleList{}
		mockClient.EXPECT().List(ctx, actualList, &client.ListOptions{Namespace: api.PackageNamespace}).Return(fmt.Errorf("oops"))

		bundleItems, err := bundleClient.GetBundleList(ctx)

		assert.Nil(t, bundleItems)
		assert.EqualError(t, err, "listing package bundles: oops")
	})
}

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

	ctx := context.Background()

	t.Run("golden path", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		actualBundle := &api.PackageBundle{}
		mockClient.EXPECT().Create(ctx, actualBundle).Return(nil)

		err := bundleClient.CreateBundle(ctx, actualBundle)

		assert.NoError(t, err)
	})

	t.Run("error scenario", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		actualBundle := &api.PackageBundle{}
		mockClient.EXPECT().Create(ctx, actualBundle).Return(fmt.Errorf("oops"))

		err := bundleClient.CreateBundle(ctx, actualBundle)

		assert.EqualError(t, err, "creating new package bundle: oops")
	})
}

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

	ctx := context.Background()
	ns := &v1.Namespace{}
	ns.Name = "eksa-packages-bobby"
	key := types.NamespacedName{
		Name: "eksa-packages-bobby",
	}

	t.Run("already exists", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(ns)).Return(nil)

		err := bundleClient.CreateClusterNamespace(ctx, "bobby")

		assert.NoError(t, err)
	})

	t.Run("get error", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(ns)).Return(fmt.Errorf("boom"))

		err := bundleClient.CreateClusterNamespace(ctx, "bobby")

		assert.EqualError(t, err, "boom")
	})

	t.Run("create namespace", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		groupResource := schema.GroupResource{
			Group:    key.Name,
			Resource: "Namespace",
		}
		notFoundError := errors.NewNotFound(groupResource, key.Name)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(ns)).Return(notFoundError)
		mockClient.EXPECT().Create(ctx, ns).Return(nil)

		err := bundleClient.CreateClusterNamespace(ctx, "bobby")

		assert.NoError(t, err)
	})

	t.Run("create namespace error", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		groupResource := schema.GroupResource{
			Group:    key.Name,
			Resource: "Namespace",
		}
		notFoundError := errors.NewNotFound(groupResource, key.Name)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(ns)).Return(notFoundError)
		mockClient.EXPECT().Create(ctx, ns).Return(fmt.Errorf("boom"))

		err := bundleClient.CreateClusterNamespace(ctx, "bobby")

		assert.EqualError(t, err, "boom")
	})
}

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

	ctx := context.Background()
	name := "ns-secret-map"
	namespace := "eksa-packages-bobby"
	key := types.NamespacedName{
		Name:      name,
		Namespace: namespace,
	}

	cm := &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
	}
	cm.Data = make(map[string]string)
	cm.Data[namespace] = "eksa-package-controller"
	cm.Data["emissary-system"] = "eksa-package-placeholder"

	t.Run("already exists", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(cm)).Return(nil)

		err := bundleClient.CreateClusterConfigMap(ctx, "bobby")

		assert.NoError(t, err)
	})

	t.Run("get error", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(cm)).Return(fmt.Errorf("boom"))

		err := bundleClient.CreateClusterConfigMap(ctx, "bobby")

		assert.EqualError(t, err, "boom")
	})

	t.Run("create configmap", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		groupResource := schema.GroupResource{
			Group:    key.Name,
			Resource: "Namespace",
		}
		notFoundError := errors.NewNotFound(groupResource, key.Name)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(cm)).Return(notFoundError)
		mockClient.EXPECT().Create(ctx, cm).Return(nil)

		err := bundleClient.CreateClusterConfigMap(ctx, "bobby")

		assert.NoError(t, err)
	})

	t.Run("create configmap error", func(t *testing.T) {
		mockClient := givenMockClient(t)
		bundleClient := NewManagerClient(mockClient)
		groupResource := schema.GroupResource{
			Group:    key.Name,
			Resource: "Namespace",
		}
		notFoundError := errors.NewNotFound(groupResource, key.Name)
		mockClient.EXPECT().Get(ctx, key, gomock.AssignableToTypeOf(cm)).Return(notFoundError)
		mockClient.EXPECT().Create(ctx, cm).Return(fmt.Errorf("boom"))

		err := bundleClient.CreateClusterConfigMap(ctx, "bobby")

		assert.EqualError(t, err, "boom")
	})
}