//go:build unit || all
// +build unit all

package gke

import (
	"context"
	"testing"

	"github.com/crossplane-contrib/provider-gcp/apis/v1beta1"
	. "github.com/onsi/gomega"
	helmaction "helm.sh/helm/v3/pkg/action"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"go.uber.org/mock/gomock"
)

const (
	testGKEClusterName   = "shared-gke"
	testDataplaneProject = "dataplane-project"
	testCentralProject   = "central-project"
)

type stubEnvProvider struct {
	clusterName string
	account     string
}

func (s *stubEnvProvider) GetClient() client.Client                 { return nil }
func (s *stubEnvProvider) GetHelmConfig() *helmaction.Configuration { return nil }
func (s *stubEnvProvider) GetRestConfig() *rest.Config              { return nil }
func (s *stubEnvProvider) GetRegion() string                        { return "us-west1" }
func (s *stubEnvProvider) GetClusterName() string                   { return s.clusterName }
func (s *stubEnvProvider) GetClusterEndpoint() string               { return "" }
func (s *stubEnvProvider) GetClusterCA() []byte                     { return nil }
func (s *stubEnvProvider) GetAccount() string                       { return s.account }
func (s *stubEnvProvider) GetNetwork() string                       { return "" }
func (s *stubEnvProvider) AWS() dataplane.AWSInterface              { return nil }
func (s *stubEnvProvider) GCP() dataplane.GCPInterface              { return nil }

var _ dataplane.EnvProvider = (*stubEnvProvider)(nil)

type policyBindingCall struct {
	project string
	role    string
	member  string
}

type fakeResourceManager struct {
	delBindings []policyBindingCall
}

func (f *fakeResourceManager) GetProjectIdentity(context.Context, string) (gcp.ProjectIdentity, error) {
	return gcp.ProjectIdentity{}, nil
}

func (f *fakeResourceManager) EnsureProjectCustomRole(context.Context, string, string, gcp.ProjectCustomRole) (string, error) {
	return "", nil
}

func (f *fakeResourceManager) AddProjectPolicyBinding(context.Context, string, string, string) error {
	return nil
}

func (f *fakeResourceManager) DelProjectPolicyBinding(_ context.Context, project, role, member string) error {
	f.delBindings = append(f.delBindings, policyBindingCall{
		project: project,
		role:    role,
		member:  member,
	})
	return nil
}

func newTestProviderConfigTask(t *testing.T, clusterName string, rm *fakeResourceManager) *ProviderConfig {
	t.Helper()

	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      clusterName,
			Namespace: "test-ns",
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{
				Name: testGKEClusterName,
			},
		},
	}

	ctrl := gomock.NewController(t)
	mockGCPCli := gcp.NewMockClient(ctrl)
	mockGCPCli.EXPECT().ResourceManager().Return(rm).AnyTimes()

	return &ProviderConfig{
		BaseTask: task.BaseTask{
			Context: &task.Context{
				Cluster: cluster,
				Config: config.RootConfig{
					ControllerConfig: config.ControllerConfig{
						FullManage: config.FullManage{
							GCPProjectID: testCentralProject,
						},
					},
				},
				EnvProvider: &stubEnvProvider{
					clusterName: testGKEClusterName,
					account:     testDataplaneProject,
				},
			},
		},
		gcpCli: mockGCPCli,
	}
}

func newProviderConfigClient(t *testing.T, pc *v1beta1.ProviderConfig) client.Client {
	t.Helper()

	s := runtime.NewScheme()
	if err := scheme.AddToScheme(s); err != nil {
		t.Fatalf("add core scheme: %v", err)
	}
	if err := v1beta1.SchemeBuilder.AddToScheme(s); err != nil {
		t.Fatalf("add gcp provider scheme: %v", err)
	}

	return fake.NewClientBuilder().WithScheme(s).WithObjects(pc).Build()
}

func TestProviderConfigDelete(t *testing.T) {
	pcName := namer.NewName(testGKEClusterName, "pc", "", -1)
	expectedMember := namer.NewName(testGKEClusterName, "np-sa", "", namer.GKEServiceAccountNameLimit) +
		"@" + testDataplaneProject + ".iam.gserviceaccount.com"

	cases := map[string]struct {
		clusterName  string
		annotations  map[string]string
		expectRevoke bool
	}{
		"skip when another cluster still references provider config": {
			clusterName: "cluster-a",
			annotations: map[string]string{
				annoKeyPrefixUsedByCluster + "cluster-b": annoValInUse,
			},
			expectRevoke: false,
		},
		"revoke artifact registry policy when reference count is zero": {
			clusterName:  "cluster-a",
			annotations:  map[string]string{},
			expectRevoke: true,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)

			rm := &fakeResourceManager{}
			taskObj := newTestProviderConfigTask(t, tc.clusterName, rm)
			pc := &v1beta1.ProviderConfig{
				ObjectMeta: metav1.ObjectMeta{
					Name:        pcName,
					Annotations: tc.annotations,
				},
			}
			taskObj.Local = newProviderConfigClient(t, pc)

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

			if tc.expectRevoke {
				g.Expect(rm.delBindings).To(HaveLen(1))
				g.Expect(rm.delBindings[0].project).To(Equal(testCentralProject))
				g.Expect(rm.delBindings[0].role).To(Equal(roleArtifactRegistryReader))
				g.Expect(rm.delBindings[0].member).To(Equal("serviceAccount:" + expectedMember))
				// DeleteObjectCompleted requeues until ProviderConfig is fully removed.
				g.Expect(result.Error()).To(MatchError(ContainSubstring("still exist")))
			} else {
				g.Expect(result).To(Equal(deploy.Done()))
				g.Expect(rm.delBindings).To(BeEmpty())
			}
		})
	}
}
