package agent

import (
	"context"
	"fmt"
	"strconv"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/client-go/tools/cache"
)

func TestUpstreamChangesUpdatesWorkspaceImagePullSecret(t *testing.T) {
	client := NewMockClient()
	tracker := newImagePullSecretTracker()
	agentKey := api.AgentKey{ID: 1, Type: api.AgentTypeKubernetes}
	handler := secretEventHandler(testlogger.New(t), context.Background(), tracker, client, agentKey)
	sourceSecretName := "source-secret"
	sourceSecretNamespace := "source-secret-namespace"
	workspaceNamespace := "workspace-secret-namespace"
	secretToBeUpdated := generateImagePullSecret(
		sourceSecretName,
		workspaceNamespace,
		map[string]string{
			workspaceImagePullSecretNameLabel:      sourceSecretName,
			workspaceImagePullSecretNamespaceLabel: sourceSecretNamespace,
		},
		nil,
	)

	// Register the secret has been cloned by the agent in the workspace namespace
	tracker.set(sourceSecretNamespace, sourceSecretName, workspaceNamespace)

	// Trigger handler with a secret with name and namespace corresponding to the workspace secret labels
	oldSecret := toUnstructuredSecret(t, secretToBeUpdated)
	newSecret := toUnstructuredSecret(t, generateImagePullSecret(sourceSecretName,
		sourceSecretNamespace,
		nil,
		map[string][]byte{"data": {0, 1, 1}},
	))

	// Sanity check on empty client secret store
	assert.Empty(t, client.SecretStore)

	// Trigger handler
	handler.UpdateFunc(oldSecret, newSecret)

	// Assert client contains updated secret in the workspace namespace
	unstructuredSecret, err := client.Get(context.Background(), secretGVR, workspaceNamespace, sourceSecretName)
	require.NoError(t, err)

	expectedSecret := generateImagePullSecret(
		sourceSecretName,
		workspaceNamespace,
		map[string]string{
			workspaceImagePullSecretNameLabel:      sourceSecretName,
			workspaceImagePullSecretNamespaceLabel: sourceSecretNamespace,
			api.AgentIDKey:                         strconv.FormatInt(agentKey.ID, 10),
		},
		map[string][]byte{"data": {0, 1, 1}},
	)
	assert.Len(t, client.SecretStore, 1)
	assert.Equal(t, expectedSecret, fromUnstructuredSecret(t, unstructuredSecret))

	// Assert the tracker has only the updated namespace
	namespaces := tracker.get(sourceSecretNamespace, sourceSecretName)
	assert.Contains(t, namespaces, workspaceNamespace)
	assert.Len(t, namespaces, 1)
}

func TestCertainUpdatesToImagePullSecretAreIgnored(t *testing.T) {
	sourceSecretName := "source-secret"
	sourceSecretNamespace := "source-secret-namespace"
	agentKey := api.AgentKey{ID: 1, Type: api.AgentTypeKubernetes}
	var testcases = []struct {
		name      string
		oldSecret *unstructured.Unstructured
		newSecret *unstructured.Unstructured
	}{
		{
			name: "when an agentKey is present it signifies this is a secret we manage",
			oldSecret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				nil,
				map[string][]byte{"data": {0, 1, 1}},
			)),
			newSecret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				map[string]string{api.AgentIDKey: "1"},
				map[string][]byte{"data": {0, 1, 1}},
			)),
		},

		{
			name: "when an update is received from an upstream secret which does not have kubernetes secret type prefix",
			oldSecret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				nil,
				map[string][]byte{"data": {0, 1, 1}},
			)),
			newSecret: func() *unstructured.Unstructured {
				secretWithInvalidType := generateImagePullSecret(
					sourceSecretName,
					sourceSecretNamespace,
					nil,
					map[string][]byte{"data": {0, 1, 3}},
				)
				secretWithInvalidType.Type = "notKubernetes.io/dockercfg"
				return toUnstructuredSecret(t, secretWithInvalidType)
			}(),
		},
		{
			name: "secret does not contain type information",
			oldSecret: &unstructured.Unstructured{
				Object: map[string]any{
					"metadata": map[string]any{
						"name":      "not-a-secret",
						"namespace": "not-a-namespace",
					},
					"data": map[string]any{"password": 123145},
				},
			},
			newSecret: &unstructured.Unstructured{
				Object: map[string]any{
					"metadata": map[string]any{
						"name":      "not-a-secret-a",
						"namespace": "not-a-namespace-a",
					},
					"data": map[string]any{"password": 123145},
				},
			},
		},
		{
			name: "when an update is not a valid secret",
			oldSecret: &unstructured.Unstructured{
				Object: map[string]any{
					"apiVersion": "apps/v1",
					"metadata": map[string]any{
						"name":      "not-a-secret",
						"namespace": "not-a-namespace",
					},
					"data": map[string]any{"password": 123145},
					"type": "kubernetes.io/dockercfg",
				},
			},
			newSecret: &unstructured.Unstructured{
				Object: map[string]any{
					"apiVersion": "apps/v1",
					"metadata": map[string]any{
						"name":      "not-a-secret-a",
						"namespace": "not-a-namespace-a",
					},
					"data": map[string]any{"password": 123145},
					"type": "kubernetes.io/dockercfg",
				},
			},
		},

		{
			name: "when an update is received from an upstream secret which has not changed semantically, ignore it",
			oldSecret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				nil,
				map[string][]byte{"data": {0, 1, 1}},
			)),
			newSecret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				nil,
				map[string][]byte{"data": {0, 1, 1}},
			)),
		},
		{
			name: "when an update is received from a workspace image pull secret, but we are not tracking any namespaces, ignore it",
			oldSecret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				nil,
				map[string][]byte{"data": {0, 2, 1}},
			)),
			newSecret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				nil,
				map[string][]byte{"data": {0, 1, 1}},
			)),
		},
	}
	for _, testcase := range testcases {
		t.Run(testcase.name, func(t *testing.T) {
			client := NewMockClient()
			tracker := newImagePullSecretTracker()
			handler := secretEventHandler(testlogger.New(t), context.Background(), tracker, client, agentKey)

			// Trigger handler
			handler.UpdateFunc(testcase.oldSecret, testcase.newSecret)

			// Assert client contains no mock secrets
			assert.Empty(t, client.SecretStore)

			// Tracker must be empty since it was never initialized with any secrets to track
			// and all updates were ignored
			namespaces := tracker.get(sourceSecretNamespace, sourceSecretName)
			assert.Nil(t, namespaces)
		})
	}

}

func TestUpstreamDeletionDeletesWorkspaceImagePullSecret(t *testing.T) {
	client := NewMockClient()
	tracker := newImagePullSecretTracker()
	agentKey := api.AgentKey{ID: 1, Type: api.AgentTypeKubernetes}
	handler := secretEventHandler(testlogger.New(t), context.Background(), tracker, client, agentKey)
	sourceSecretName := "source-secret"
	sourceSecretNamespace := "source-secret-namespace"
	workspaceNamespace := "workspace-secret-namespace"
	secretToBeDeleted := generateImagePullSecret(
		sourceSecretName,
		workspaceNamespace,
		map[string]string{
			workspaceImagePullSecretNameLabel:      sourceSecretName,
			workspaceImagePullSecretNamespaceLabel: sourceSecretNamespace,
		},
		nil,
	)

	// Register the secret has been cloned by the agent in the workspace namespace
	tracker.set(sourceSecretNamespace, sourceSecretName, workspaceNamespace)

	secret := toUnstructuredSecret(t, generateImagePullSecret(sourceSecretName,
		sourceSecretNamespace,
		nil,
		map[string][]byte{"data": {0, 1, 1}},
	))

	_, err := client.UpdateOrCreate(context.Background(), secretGVR, secretToBeDeleted.GetNamespace(), secretToBeDeleted, true)
	require.NoError(t, err)

	// Trigger handler
	handler.DeleteFunc(secret)

	assert.Empty(t, client.SecretStore[secretToBeDeleted.GetNamespace()])
	namespaces := tracker.get(sourceSecretName, sourceSecretNamespace)
	assert.Nil(t, namespaces)
}

func TestMissedUpstreamDeletionDeletesWorkspaceImagePullSecret(t *testing.T) {
	client := NewMockClient()
	tracker := newImagePullSecretTracker()
	agentKey := api.AgentKey{ID: 1, Type: api.AgentTypeKubernetes}
	handler := secretEventHandler(testlogger.New(t), context.Background(), tracker, client, agentKey)
	sourceSecretName := "source-secret"
	sourceSecretNamespace := "source-secret-namespace"
	workspaceNamespace := "workspace-secret-namespace"
	secretToBeDeleted := generateImagePullSecret(
		sourceSecretName,
		workspaceNamespace,
		map[string]string{
			workspaceImagePullSecretNameLabel:      sourceSecretName,
			workspaceImagePullSecretNamespaceLabel: sourceSecretNamespace,
		},
		nil,
	)

	// Register the secret has been cloned by the agent in the workspace namespace
	tracker.set(sourceSecretNamespace, sourceSecretName, workspaceNamespace)

	secret := toUnstructuredSecret(t, generateImagePullSecret(sourceSecretName,
		sourceSecretNamespace,
		nil,
		map[string][]byte{"data": {0, 1, 1}},
	))

	_, err := client.UpdateOrCreate(context.Background(), secretGVR, secretToBeDeleted.GetNamespace(), secretToBeDeleted, true)
	require.NoError(t, err)

	// Trigger handler
	handler.DeleteFunc(
		cache.DeletedFinalStateUnknown{
			Key: fmt.Sprintf("%s/%s", sourceSecretName, sourceSecretName),
			Obj: secret,
		})

	assert.Empty(t, client.SecretStore[secretToBeDeleted.GetNamespace()])
	namespaces := tracker.get(sourceSecretNamespace, sourceSecretName)
	assert.Nil(t, namespaces)
}

func TestCertainDeletionsOfImagePullSecretAreIgnored(t *testing.T) {
	sourceSecretName := "source-secret"
	sourceSecretNamespace := "source-secret-namespace"
	agentKey := api.AgentKey{ID: 1, Type: api.AgentTypeKubernetes}
	var testcases = []struct {
		name   string
		secret *unstructured.Unstructured
	}{
		{
			name: "when an agentKey is present it signifies this is a secret we manage",
			secret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				map[string]string{api.AgentIDKey: "1"},
				map[string][]byte{"data": {0, 1, 1}},
			)),
		},

		{
			name: "when an delete is received from an upstream secret which does not have kubernetes secret type prefix",
			secret: func() *unstructured.Unstructured {
				secretWithInvalidType := generateImagePullSecret(
					sourceSecretName,
					sourceSecretNamespace,
					nil,
					map[string][]byte{"data": {0, 1, 3}},
				)
				secretWithInvalidType.Type = "notKubernetes.io/dockercfg"
				return toUnstructuredSecret(t, secretWithInvalidType)
			}(),
		},
		{
			name: "when secret does not contain type information",
			secret: &unstructured.Unstructured{
				Object: map[string]any{
					"metadata": map[string]any{
						"name":      "not-a-secret-a",
						"namespace": "not-a-namespace-a",
					},
					"data": map[string]any{"password": 123145},
				},
			},
		},

		{
			name: "when a delete is received from a workspace image pull secret, but we are not tracking any namespaces, ignore it",
			secret: toUnstructuredSecret(t, generateImagePullSecret(
				sourceSecretName,
				sourceSecretNamespace,
				nil,
				map[string][]byte{"data": {0, 1, 1}},
			)),
		},
	}
	for _, testcase := range testcases {
		t.Run(testcase.name, func(t *testing.T) {
			client := NewMockClient()
			secretToBeDeleted := generateImagePullSecret(
				sourceSecretName,
				"workspace-secret-namespace",
				map[string]string{
					workspaceImagePullSecretNameLabel:      sourceSecretName,
					workspaceImagePullSecretNamespaceLabel: sourceSecretNamespace,
				},
				nil,
			)

			_, err := client.UpdateOrCreate(context.Background(), secretGVR, secretToBeDeleted.GetNamespace(), secretToBeDeleted, true)
			require.NoError(t, err)

			tracker := newImagePullSecretTracker()
			handler := secretEventHandler(testlogger.New(t), context.Background(), tracker, client, agentKey)

			// Trigger handler
			handler.DeleteFunc(testcase.secret)

			// Assert client contains no mock secrets
			assert.Len(t, client.SecretStore, 1)
		})
	}

}
