package agent

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes/fake"
)

func TestInitScanStatus(t *testing.T) {
	tests := []struct {
		name                    string
		persistOcsStatus        bool
		existingConfigMap       *corev1.ConfigMap
		expectedCMCreateActions int
	}{
		{
			name:                    "persistOcsStatus is false",
			persistOcsStatus:        false,
			expectedCMCreateActions: 0,
		},
		{
			name:                    "ConfigMap exists",
			persistOcsStatus:        true,
			existingConfigMap:       &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: ocsStatusConfigMapName}},
			expectedCMCreateActions: 0,
		},
		{
			name:                    "ConfigMap does not exist and is created successfully",
			persistOcsStatus:        true,
			expectedCMCreateActions: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			clientset := fake.NewSimpleClientset()
			ctx := context.Background()

			if tt.existingConfigMap != nil {
				_, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Create(ctx, tt.existingConfigMap, metav1.CreateOptions{})
				assert.NoError(t, err)
				// Increment count by 1 to factor for this config map creation.
				tt.expectedCMCreateActions = tt.expectedCMCreateActions + 1
			}

			s := &statusRecorder{
				configMaps:           clientset.CoreV1().ConfigMaps(gitlabAgentNamespace),
				persistOcsStatus:     tt.persistOcsStatus,
				gitlabAgentNamespace: gitlabAgentNamespace,
			}

			err := s.initScanStatus(ctx)
			assert.NoError(t, err)

			_, createCount, _ := countConfigMapActions(clientset)
			assert.Equal(t, tt.expectedCMCreateActions, createCount, "Unexpected number of Get actions on ocs-status ConfigMap")
		})
	}
}

func TestRecordScanStart(t *testing.T) {
	tests := []struct {
		name                   string
		persistOcsStatus       bool
		expectedCMGetActions   int
		expectedCMApplyActions int
	}{
		{
			name:                   "When persistOcsStatus is false, ocs-status config map is not accessed",
			persistOcsStatus:       false,
			expectedCMGetActions:   0,
			expectedCMApplyActions: 0,
		},
		{
			name:                   "When persistOcsStatus is true, ocs-status config map is retrieved and updated",
			persistOcsStatus:       true,
			expectedCMGetActions:   1,
			expectedCMApplyActions: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			clientset := fake.NewSimpleClientset()
			ctx := context.Background()
			log := testlogger.New(t)
			s := &statusRecorder{
				configMaps:           clientset.CoreV1().ConfigMaps(gitlabAgentNamespace),
				gitlabAgentNamespace: gitlabAgentNamespace,
				persistOcsStatus:     tt.persistOcsStatus,
			}

			// If persistOcsStatus is true, create ocs-status config map first
			if tt.persistOcsStatus {
				_, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Create(ctx, &corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{Name: ocsStatusConfigMapName, Namespace: gitlabAgentNamespace},
				}, metav1.CreateOptions{})
				assert.NoError(t, err)
			}
			s.recordScanStart(ctx, log, namespace1)

			// Verify that get and apply counts are as expected
			getCount, _, applyCount := countConfigMapActions(clientset)
			assert.Equal(t, tt.expectedCMGetActions, getCount, "Unexpected number of Get actions on ocs-status ConfigMap")
			assert.Equal(t, tt.expectedCMApplyActions, applyCount, "Unexpected number of Apply actions on ocs-status ConfigMap")

			if tt.persistOcsStatus {
				// Retrieve the ocs-status config map
				cm, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Get(ctx, ocsStatusConfigMapName, metav1.GetOptions{})
				assert.NoError(t, err)
				assert.NotNil(t, cm.Data[namespace1])

				// Parse data
				data := make(map[string]any, 1)
				err = json.Unmarshal([]byte(cm.Data[namespace1]), &data)
				assert.NoError(t, err)

				// Verify that `status` is set correctly and start_time is not nil
				assert.Equal(t, scanStatusRunning, data["status"])
				assert.NotNil(t, data["start_time"])
			}
		})
	}
}

func TestRecordScanStart_MissingConfigMap(t *testing.T) {
	clientset := fake.NewSimpleClientset()
	ctx := context.Background()
	log := testlogger.New(t)
	s := &statusRecorder{
		configMaps:           clientset.CoreV1().ConfigMaps(gitlabAgentNamespace),
		gitlabAgentNamespace: gitlabAgentNamespace,
		persistOcsStatus:     true,
	}

	// Omit ocs-status config map creation
	s.recordScanStart(ctx, log, namespace1)

	// Expect that ocs-config map is retrieved but not updated
	getCount, _, applyCount := countConfigMapActions(clientset)
	assert.Equal(t, 1, getCount, "Unexpected number of Get actions on ocs-status ConfigMap")
	assert.Equal(t, 0, applyCount, "Unexpected number of Apply actions on ocs-status ConfigMap")
}

func TestRecordScanEnd(t *testing.T) {
	tests := []struct {
		name                   string
		persistOcsStatus       bool
		expectedCMGetActions   int
		expectedCMApplyActions int
	}{
		{
			name:                   "When persistOcsStatus is false, ocs-status config map is not accessed",
			persistOcsStatus:       false,
			expectedCMGetActions:   0,
			expectedCMApplyActions: 0,
		},
		{
			name:                   "When persistOcsStatus is true, ocs-status config map is retrieved and updated",
			persistOcsStatus:       true,
			expectedCMGetActions:   1,
			expectedCMApplyActions: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			clientset := fake.NewSimpleClientset()
			ctx := context.Background()
			log := testlogger.New(t)
			s := &statusRecorder{
				configMaps:           clientset.CoreV1().ConfigMaps(gitlabAgentNamespace),
				gitlabAgentNamespace: gitlabAgentNamespace,
				persistOcsStatus:     tt.persistOcsStatus,
			}

			// If persistOcsStatus is true, create ocs-status config map with namespace data that should have been created by RecordScanStart
			if tt.persistOcsStatus {
				namespaceData, _ := json.Marshal(map[string]any{
					"status":     scanStatusRunning,
					"start_time": time.Now().Format(time.RFC3339),
				})
				configMap := &corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      ocsStatusConfigMapName,
						Namespace: gitlabAgentNamespace,
					},
					Data: map[string]string{
						namespace1: string(namespaceData),
					},
				}
				_, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Create(ctx, configMap, metav1.CreateOptions{})
				assert.NoError(t, err)
			}

			vulnerabilitiesFound := 10
			s.recordScanEnd(ctx, log, namespace1, vulnerabilitiesFound, scanStatusSuccess)

			// Verify that get and apply counts are as expected
			getCount, _, applyCount := countConfigMapActions(clientset)
			assert.Equal(t, tt.expectedCMGetActions, getCount, "Unexpected number of Get actions on ocs-status ConfigMap")
			assert.Equal(t, tt.expectedCMApplyActions, applyCount, "Unexpected number of Apply actions on ocs-status ConfigMap")

			if tt.persistOcsStatus {
				// Retrieve the ocs-status config map
				cm, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Get(ctx, ocsStatusConfigMapName, metav1.GetOptions{})
				assert.NoError(t, err)
				assert.NotNil(t, cm.Data[namespace1])

				// Parse data
				data := make(map[string]any, 1)
				err = json.Unmarshal([]byte(cm.Data[namespace1]), &data)
				assert.NoError(t, err)

				// Verify that `status` and `vulnerabilities_found` are set correctly, end_time and start_time are not nil
				assert.Equal(t, scanStatusSuccess, data["status"])
				assert.Equal(t, vulnerabilitiesFound, int(data["vulnerabilities_found"].(float64)))
				assert.NotNil(t, data["end_time"])
				assert.NotNil(t, data["start_time"])
			}
		})
	}
}

func TestRecordScanEnd_NoNamespaceData(t *testing.T) {
	clientset := fake.NewSimpleClientset()
	ctx := context.Background()
	log := testlogger.New(t)
	s := &statusRecorder{
		configMaps:           clientset.CoreV1().ConfigMaps(gitlabAgentNamespace),
		gitlabAgentNamespace: gitlabAgentNamespace,
		persistOcsStatus:     true,
	}

	// create ocs-status config map with no namespace data
	_, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Create(ctx, &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{Name: ocsStatusConfigMapName, Namespace: gitlabAgentNamespace},
	}, metav1.CreateOptions{})
	assert.NoError(t, err)

	vulnerabilitiesFound := 10
	s.recordScanEnd(ctx, log, namespace1, vulnerabilitiesFound, scanStatusSuccess)

	// Retrieve the ocs-status config map
	cm, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Get(ctx, ocsStatusConfigMapName, metav1.GetOptions{})
	assert.NoError(t, err)
	assert.NotNil(t, cm.Data[namespace1])

	// Parse data
	data := make(map[string]any, 1)
	err = json.Unmarshal([]byte(cm.Data[namespace1]), &data)
	assert.NoError(t, err)

	// Verify that `status` and `vulnerabilities_found` are set correctly, end_time is not nil but start_time is an empty string
	assert.Equal(t, scanStatusSuccess, data["status"])
	assert.Equal(t, vulnerabilitiesFound, int(data["vulnerabilities_found"].(float64)))
	assert.NotNil(t, data["end_time"])
	assert.Empty(t, data["start_time"])
}

func TestRecordScanEnd_MissingConfigMap(t *testing.T) {
	clientset := fake.NewSimpleClientset()
	ctx := context.Background()
	log := testlogger.New(t)
	s := &statusRecorder{
		configMaps:           clientset.CoreV1().ConfigMaps(gitlabAgentNamespace),
		gitlabAgentNamespace: gitlabAgentNamespace,
		persistOcsStatus:     true,
	}

	// Omit ocs-status config map creation
	s.recordScanEnd(ctx, log, namespace1, 10, scanStatusSuccess)

	// Expect that ocs-config map is retrieved but not updated as it is not present
	getCount, _, applyCount := countConfigMapActions(clientset)
	assert.Equal(t, 1, getCount, "Unexpected number of Get actions on ocs-status ConfigMap")
	assert.Equal(t, 0, applyCount, "Unexpected number of Apply actions on ocs-status ConfigMap")
}

func TestRecordScanEnd_NamespaceDataNotJSON(t *testing.T) {
	clientset := fake.NewSimpleClientset()
	ctx := context.Background()
	log := testlogger.New(t)
	s := &statusRecorder{
		configMaps:           clientset.CoreV1().ConfigMaps(gitlabAgentNamespace),
		gitlabAgentNamespace: gitlabAgentNamespace,
		persistOcsStatus:     true,
	}

	// create ocs-status config map with namespace data that is not in JSON format
	namespaceData := "not json"
	configMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      ocsStatusConfigMapName,
			Namespace: gitlabAgentNamespace,
		},
		Data: map[string]string{
			namespace1: string(namespaceData),
		},
	}
	_, err := clientset.CoreV1().ConfigMaps(gitlabAgentNamespace).Create(ctx, configMap, metav1.CreateOptions{})
	assert.NoError(t, err)

	vulnerabilitiesFound := 10
	s.recordScanEnd(ctx, log, namespace1, vulnerabilitiesFound, scanStatusSuccess)

	// Expect that ocs-config map is retrieved but not updated
	getCount, _, applyCount := countConfigMapActions(clientset)
	assert.Equal(t, 1, getCount, "Unexpected number of Get actions on ocs-status ConfigMap")
	assert.Equal(t, 0, applyCount, "Unexpected number of Apply actions on ocs-status ConfigMap")
}

func countConfigMapActions(clientset *fake.Clientset) (getCount, createCount, applyCount int) {
	actions := clientset.Actions()

	for _, action := range actions {
		if action.GetResource().Resource != "configmaps" {
			continue
		}
		switch action.GetVerb() {
		case "get":
			getCount++
		case "create":
			createCount++
		case "patch":
			applyCount++
		}

	}
	return getCount, createCount, applyCount
}
