package e2e

import (
	"fmt"
	"testing"
	"time"

	"github.com/argoproj/argo-cd/v3/test/e2e/fixture/project"

	"github.com/stretchr/testify/require"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/argoproj/argo-cd/v3/test/e2e/fixture"
	. "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app"
)

const (
	WaitDuration    = time.Second
	TimeoutDuration = time.Second * 3
)

func TestSyncWithFeatureDisabled(t *testing.T) {
	Given(t).
		Path("guestbook").
		When().
		WithImpersonationDisabled().
		CreateFromFile(func(app *v1alpha1.Application) {
			app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
		}).
		Then().
		// With the impersonation feature disabled, Application sync should continue to use
		// the control plane service account for the sync operation and the sync should succeed.
		ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeSynced), WaitDuration, TimeoutDuration).
		Expect(OperationMessageContains("successfully synced"))
}

func TestSyncWithNoDestinationServiceAccountsInProject(t *testing.T) {
	Given(t).
		Path("guestbook").
		When().
		WithImpersonationEnabled("", nil).
		CreateFromFile(func(app *v1alpha1.Application) {
			app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
		}).
		Then().
		// With the impersonation feature enabled, Application sync must fail
		// when there are no destination service accounts configured in AppProject
		ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeOutOfSync), WaitDuration, TimeoutDuration).
		Expect(OperationMessageContains("no matching service account found for destination server"))
}

func TestSyncWithNoDestinationServiceAccountsAndEnforcementDisabled(t *testing.T) {
	Given(t).
		Path("guestbook").
		When().
		WithImpersonationEnabled("", nil).
		WithImpersonationEnforcementDisabled().
		CreateFromFile(func(app *v1alpha1.Application) {
			app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
		}).
		Then().
		// With the impersonation feature enabled, but not enforced, Application sync must succeed
		// by using the controller service account for the sync operation.
		ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeSynced), WaitDuration, TimeoutDuration).
		Expect(OperationMessageContains("successfully synced"))
}

func TestSyncWithImpersonateWithSyncServiceAccount(t *testing.T) {
	serviceAccountName := "test-account"

	projectCtx := project.Given(t)
	appCtx := GivenWithSameState(projectCtx)

	projectCtx.
		Name("sync-test-project").
		SourceNamespaces([]string{"*"}).
		SourceRepositories([]string{"*"}).
		Destination("*,*").
		DestinationServiceAccounts(
			[]string{
				fmt.Sprintf("%s,%s,%s", "*", projectCtx.DeploymentNamespace(), serviceAccountName),
				fmt.Sprintf("%s,%s,%s", v1alpha1.KubernetesInternalAPIServerAddr, projectCtx.DeploymentNamespace(), "missing-serviceAccount"),
			}).
		When().
		Create().
		Then().
		Expect()

	appCtx.
		Project(projectCtx.GetName()).
		SetTrackingMethod("annotation").
		Path("guestbook").
		When().
		WithImpersonationEnabled(serviceAccountName, []rbacv1.PolicyRule{
			{
				APIGroups: []string{"apps", ""},
				Resources: []string{"deployments"},
				Verbs:     []string{"*"},
			},
			{
				APIGroups: []string{""},
				Resources: []string{"services"},
				Verbs:     []string{"*"},
			},
		}).
		CreateFromFile(func(app *v1alpha1.Application) {
			app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
		}).
		Then().
		// With the impersonation feature enabled, Application sync should succeed
		// as there is a valid match found in the available destination service accounts configured in AppProject
		ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeSynced), WaitDuration, TimeoutDuration).
		Expect(OperationMessageContains("successfully synced"))
}

func TestSyncWithMissingServiceAccount(t *testing.T) {
	serviceAccountName := "test-account"

	projectCtx := project.Given(t)
	appCtx := GivenWithSameState(projectCtx)

	projectCtx.
		Name("false-test-project").
		SourceNamespaces([]string{"*"}).
		SourceRepositories([]string{"*"}).
		Destination("*,*").
		DestinationServiceAccounts(
			[]string{
				fmt.Sprintf("%s,%s,%s", v1alpha1.KubernetesInternalAPIServerAddr, projectCtx.DeploymentNamespace(), "missing-serviceAccount"),
				fmt.Sprintf("%s,%s,%s", "*", projectCtx.DeploymentNamespace(), serviceAccountName),
			}).
		When().
		Create().
		Then().
		Expect()

	appCtx.
		Project(projectCtx.GetName()).
		SetTrackingMethod("annotation").
		Path("guestbook").
		When().
		WithImpersonationEnabled(serviceAccountName, []rbacv1.PolicyRule{
			{
				APIGroups: []string{"apps", ""},
				Resources: []string{"deployments"},
				Verbs:     []string{"*"},
			},
			{
				APIGroups: []string{""},
				Resources: []string{"services"},
				Verbs:     []string{"*"},
			},
		}).
		CreateFromFile(func(app *v1alpha1.Application) {
			app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
		}).
		Then().
		// With the impersonation feature enabled, Application sync must fail
		// when there is a valid match found in the available destination service accounts configured in AppProject,
		// but the matching service account is missing.
		ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeOutOfSync), WaitDuration, TimeoutDuration).
		Expect(OperationMessageContains("one or more objects failed to apply"))
}

func TestSyncWithValidSAButDisallowedDestination(t *testing.T) {
	serviceAccountName := "test-account"

	projectCtx := project.Given(t)
	appCtx := GivenWithSameState(projectCtx)

	projectCtx.
		Name("negation-test-project").
		SourceNamespaces([]string{"*"}).
		SourceRepositories([]string{"*"}).
		Destination("*,*").
		DestinationServiceAccounts(
			[]string{
				fmt.Sprintf("%s,%s,%s", "*", projectCtx.DeploymentNamespace(), serviceAccountName),
			}).
		When().
		Create().
		Then().
		Expect()

	appCtx.
		Project(projectCtx.GetName()).
		SetTrackingMethod("annotation").
		Path("guestbook").
		When().
		WithImpersonationEnabled(serviceAccountName, []rbacv1.PolicyRule{
			{
				APIGroups: []string{"apps", ""},
				Resources: []string{"deployments"},
				Verbs:     []string{"*"},
			},
			{
				APIGroups: []string{""},
				Resources: []string{"services"},
				Verbs:     []string{"*"},
			},
		}).
		CreateFromFile(func(app *v1alpha1.Application) {
			app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
		}).
		Then().
		Expect(SyncStatusIs(v1alpha1.SyncStatusCodeSynced)).
		When().
		And(func() {
			// Patch destination to disallow target destination namespace
			patch := fmt.Appendf(nil, `{"spec": {"destinations": [{"namespace": %q}]}}`, "!"+appCtx.DeploymentNamespace())

			_, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Patch(t.Context(), projectCtx.GetName(), types.MergePatchType, patch, metav1.PatchOptions{})
			require.NoError(t, err)
		}).
		Refresh(v1alpha1.RefreshTypeNormal).
		Then().
		// With the impersonation feature enabled, Application sync must fail
		// as there is a valid match found in the available destination service accounts configured in AppProject
		// but the destination namespace is now disallowed.
		ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeUnknown), WaitDuration, TimeoutDuration)
}
