package generators

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"

	generatorsMock "github.com/argoproj/argo-cd/v3/applicationset/generators/mocks"
	servicesMocks "github.com/argoproj/argo-cd/v3/applicationset/services/mocks"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)

func TestMatrixGenerate(t *testing.T) {
	gitGenerator := &v1alpha1.GitGenerator{
		RepoURL:     "RepoURL",
		Revision:    "Revision",
		Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
	}

	listGenerator := &v1alpha1.ListGenerator{
		Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url", "templated": "test-{{path.basenameNormalized}}"}`)}},
	}

	testCases := []struct {
		name           string
		baseGenerators []v1alpha1.ApplicationSetNestedGenerator
		expectedErr    error
		expected       []map[string]any
	}{
		{
			name: "happy flow - generate params",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					List: listGenerator,
				},
			},
			expected: []map[string]any{
				{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "cluster": "Cluster", "url": "Url", "templated": "test-app1"},
				{"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2", "cluster": "Cluster", "url": "Url", "templated": "test-app2"},
			},
		},
		{
			name: "happy flow - generate params from two lists",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{
							{Raw: []byte(`{"a": "1"}`)},
							{Raw: []byte(`{"a": "2"}`)},
						},
					},
				},
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{
							{Raw: []byte(`{"b": "1"}`)},
							{Raw: []byte(`{"b": "2"}`)},
						},
					},
				},
			},
			expected: []map[string]any{
				{"a": "1", "b": "1"},
				{"a": "1", "b": "2"},
				{"a": "2", "b": "1"},
				{"a": "2", "b": "2"},
			},
		},
		{
			name: "returns error if there is less than two base generators",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
			},
			expectedErr: ErrLessThanTwoGenerators,
		},
		{
			name: "returns error if there is more than two base generators",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					List: listGenerator,
				},
				{
					List: listGenerator,
				},
				{
					List: listGenerator,
				},
			},
			expectedErr: ErrMoreThanTwoGenerators,
		},
		{
			name: "returns error if there is more than one inner generator in the first base generator",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git:  gitGenerator,
					List: listGenerator,
				},
				{
					Git: gitGenerator,
				},
			},
			expectedErr: ErrMoreThenOneInnerGenerators,
		},
		{
			name: "returns error if there is more than one inner generator in the second base generator",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					List: listGenerator,
				},
				{
					Git:  gitGenerator,
					List: listGenerator,
				},
			},
			expectedErr: ErrMoreThenOneInnerGenerators,
		},
	}

	for _, testCase := range testCases {
		testCaseCopy := testCase // Since tests may run in parallel

		t.Run(testCaseCopy.name, func(t *testing.T) {
			genMock := &generatorsMock.Generator{}
			appSet := &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "set",
				},
				Spec: v1alpha1.ApplicationSetSpec{},
			}

			for _, g := range testCaseCopy.baseGenerators {
				gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
					Git:  g.Git,
					List: g.List,
				}
				genMock.EXPECT().GenerateParams(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]any{
					{
						"path":                    "app1",
						"path.basename":           "app1",
						"path.basenameNormalized": "app1",
					},
					{
						"path":                    "app2",
						"path.basename":           "app2",
						"path.basenameNormalized": "app2",
					},
				}, nil)

				genMock.EXPECT().GetTemplate(&gitGeneratorSpec).
					Return(&v1alpha1.ApplicationSetTemplate{})
			}

			matrixGenerator := NewMatrixGenerator(
				map[string]Generator{
					"Git":  genMock,
					"List": &ListGenerator{},
				},
			)

			got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
				Matrix: &v1alpha1.MatrixGenerator{
					Generators: testCaseCopy.baseGenerators,
					Template:   v1alpha1.ApplicationSetTemplate{},
				},
			}, appSet, nil)

			if testCaseCopy.expectedErr != nil {
				require.ErrorIs(t, err, testCaseCopy.expectedErr)
			} else {
				require.NoError(t, err)
				assert.Equal(t, testCaseCopy.expected, got)
			}
		})
	}
}

func TestMatrixGenerateGoTemplate(t *testing.T) {
	gitGenerator := &v1alpha1.GitGenerator{
		RepoURL:     "RepoURL",
		Revision:    "Revision",
		Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
	}

	listGenerator := &v1alpha1.ListGenerator{
		Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}},
	}

	testCases := []struct {
		name           string
		baseGenerators []v1alpha1.ApplicationSetNestedGenerator
		expectedErr    error
		expected       []map[string]any
	}{
		{
			name: "happy flow - generate params",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					List: listGenerator,
				},
			},
			expected: []map[string]any{
				{
					"path": map[string]string{
						"path":               "app1",
						"basename":           "app1",
						"basenameNormalized": "app1",
					},
					"cluster": "Cluster",
					"url":     "Url",
				},
				{
					"path": map[string]string{
						"path":               "app2",
						"basename":           "app2",
						"basenameNormalized": "app2",
					},
					"cluster": "Cluster",
					"url":     "Url",
				},
			},
		},
		{
			name: "happy flow - generate params from two lists",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{
							{Raw: []byte(`{"a": "1"}`)},
							{Raw: []byte(`{"a": "2"}`)},
						},
					},
				},
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{
							{Raw: []byte(`{"b": "1"}`)},
							{Raw: []byte(`{"b": "2"}`)},
						},
					},
				},
			},
			expected: []map[string]any{
				{"a": "1", "b": "1"},
				{"a": "1", "b": "2"},
				{"a": "2", "b": "1"},
				{"a": "2", "b": "2"},
			},
		},
		{
			name: "parameter override: first list elements take precedence",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{
							{Raw: []byte(`{"booleanFalse": false, "booleanTrue": true, "stringFalse": "false", "stringTrue": "true"}`)},
						},
					},
				},
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{
							{Raw: []byte(`{"booleanFalse": true, "booleanTrue": false, "stringFalse": "true", "stringTrue": "false"}`)},
						},
					},
				},
			},
			expected: []map[string]any{
				{"booleanFalse": false, "booleanTrue": true, "stringFalse": "false", "stringTrue": "true"},
			},
		},
		{
			name: "returns error if there is less than two base generators",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
			},
			expectedErr: ErrLessThanTwoGenerators,
		},
		{
			name: "returns error if there is more than two base generators",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					List: listGenerator,
				},
				{
					List: listGenerator,
				},
				{
					List: listGenerator,
				},
			},
			expectedErr: ErrMoreThanTwoGenerators,
		},
		{
			name: "returns error if there is more than one inner generator in the first base generator",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git:  gitGenerator,
					List: listGenerator,
				},
				{
					Git: gitGenerator,
				},
			},
			expectedErr: ErrMoreThenOneInnerGenerators,
		},
		{
			name: "returns error if there is more than one inner generator in the second base generator",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					List: listGenerator,
				},
				{
					Git:  gitGenerator,
					List: listGenerator,
				},
			},
			expectedErr: ErrMoreThenOneInnerGenerators,
		},
	}

	for _, testCase := range testCases {
		testCaseCopy := testCase // Since tests may run in parallel

		t.Run(testCaseCopy.name, func(t *testing.T) {
			genMock := &generatorsMock.Generator{}
			appSet := &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "set",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					GoTemplate: true,
				},
			}

			for _, g := range testCaseCopy.baseGenerators {
				gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
					Git:  g.Git,
					List: g.List,
				}
				genMock.EXPECT().GenerateParams(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]any{
					{
						"path": map[string]string{
							"path":               "app1",
							"basename":           "app1",
							"basenameNormalized": "app1",
						},
					},
					{
						"path": map[string]string{
							"path":               "app2",
							"basename":           "app2",
							"basenameNormalized": "app2",
						},
					},
				}, nil)

				genMock.EXPECT().GetTemplate(&gitGeneratorSpec).
					Return(&v1alpha1.ApplicationSetTemplate{})
			}

			matrixGenerator := NewMatrixGenerator(
				map[string]Generator{
					"Git":  genMock,
					"List": &ListGenerator{},
				},
			)

			got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
				Matrix: &v1alpha1.MatrixGenerator{
					Generators: testCaseCopy.baseGenerators,
					Template:   v1alpha1.ApplicationSetTemplate{},
				},
			}, appSet, nil)

			if testCaseCopy.expectedErr != nil {
				require.ErrorIs(t, err, testCaseCopy.expectedErr)
			} else {
				require.NoError(t, err)
				assert.Equal(t, testCaseCopy.expected, got)
			}
		})
	}
}

func TestMatrixGetRequeueAfter(t *testing.T) {
	gitGenerator := &v1alpha1.GitGenerator{
		RepoURL:     "RepoURL",
		Revision:    "Revision",
		Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
	}

	listGenerator := &v1alpha1.ListGenerator{
		Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}},
	}

	pullRequestGenerator := &v1alpha1.PullRequestGenerator{}

	scmGenerator := &v1alpha1.SCMProviderGenerator{}

	duckTypeGenerator := &v1alpha1.DuckTypeGenerator{}

	testCases := []struct {
		name               string
		baseGenerators     []v1alpha1.ApplicationSetNestedGenerator
		gitGetRequeueAfter time.Duration
		expected           time.Duration
	}{
		{
			name: "return NoRequeueAfter if all the inner baseGenerators returns it",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					List: listGenerator,
				},
			},
			gitGetRequeueAfter: NoRequeueAfter,
			expected:           NoRequeueAfter,
		},
		{
			name: "returns the minimal time",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					List: listGenerator,
				},
			},
			gitGetRequeueAfter: time.Duration(1),
			expected:           time.Duration(1),
		},
		{
			name: "returns the minimal time for pull request",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					PullRequest: pullRequestGenerator,
				},
			},
			gitGetRequeueAfter: time.Duration(15 * time.Second),
			expected:           time.Duration(15 * time.Second),
		},
		{
			name: "returns the default time if no requeueAfterSeconds is provided",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					PullRequest: pullRequestGenerator,
				},
			},
			expected: time.Duration(30 * time.Minute),
		},
		{
			name: "returns the default time for duck type generator",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					ClusterDecisionResource: duckTypeGenerator,
				},
			},
			expected: time.Duration(3 * time.Minute),
		},
		{
			name: "returns the default time for scm generator",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					SCMProvider: scmGenerator,
				},
			},
			expected: time.Duration(30 * time.Minute),
		},
	}

	for _, testCase := range testCases {
		testCaseCopy := testCase // Since tests may run in parallel

		t.Run(testCaseCopy.name, func(t *testing.T) {
			mock := &generatorsMock.Generator{}

			for _, g := range testCaseCopy.baseGenerators {
				gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
					Git:                     g.Git,
					List:                    g.List,
					PullRequest:             g.PullRequest,
					SCMProvider:             g.SCMProvider,
					ClusterDecisionResource: g.ClusterDecisionResource,
				}
				mock.EXPECT().GetRequeueAfter(&gitGeneratorSpec).Return(testCaseCopy.gitGetRequeueAfter)
			}

			matrixGenerator := NewMatrixGenerator(
				map[string]Generator{
					"Git":                     mock,
					"List":                    &ListGenerator{},
					"PullRequest":             &PullRequestGenerator{},
					"SCMProvider":             &SCMProviderGenerator{},
					"ClusterDecisionResource": &DuckTypeGenerator{},
				},
			)

			got := matrixGenerator.GetRequeueAfter(&v1alpha1.ApplicationSetGenerator{
				Matrix: &v1alpha1.MatrixGenerator{
					Generators: testCaseCopy.baseGenerators,
					Template:   v1alpha1.ApplicationSetTemplate{},
				},
			})

			assert.Equal(t, testCaseCopy.expected, got)
		})
	}
}

func TestInterpolatedMatrixGenerate(t *testing.T) {
	interpolatedGitGenerator := &v1alpha1.GitGenerator{
		RepoURL:  "RepoURL",
		Revision: "Revision",
		Files: []v1alpha1.GitFileGeneratorItem{
			{Path: "examples/git-generator-files-discovery/cluster-config/dev/config.json"},
			{Path: "examples/git-generator-files-discovery/cluster-config/prod/config.json"},
		},
	}

	interpolatedClusterGenerator := &v1alpha1.ClusterGenerator{
		Selector: metav1.LabelSelector{
			MatchLabels:      map[string]string{"environment": "{{path.basename}}"},
			MatchExpressions: nil,
		},
	}
	testCases := []struct {
		name           string
		baseGenerators []v1alpha1.ApplicationSetNestedGenerator
		expectedErr    error
		expected       []map[string]any
		clientError    bool
	}{
		{
			name: "happy flow - generate interpolated params",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: interpolatedGitGenerator,
				},
				{
					Clusters: interpolatedClusterGenerator,
				},
			},
			expected: []map[string]any{
				{"path": "examples/git-generator-files-discovery/cluster-config/dev/config.json", "path.basename": "dev", "path.basenameNormalized": "dev", "name": "dev-01", "nameNormalized": "dev-01", "server": "https://dev-01.example.com", "metadata.labels.environment": "dev", "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "project": ""},
				{"path": "examples/git-generator-files-discovery/cluster-config/prod/config.json", "path.basename": "prod", "path.basenameNormalized": "prod", "name": "prod-01", "nameNormalized": "prod-01", "server": "https://prod-01.example.com", "metadata.labels.environment": "prod", "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "project": ""},
			},
			clientError: false,
		},
	}
	clusters := []client.Object{
		&corev1.Secret{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Secret",
				APIVersion: "v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:      "dev-01",
				Namespace: "namespace",
				Labels: map[string]string{
					"argocd.argoproj.io/secret-type": "cluster",
					"environment":                    "dev",
				},
			},
			Data: map[string][]byte{
				"config": []byte("{}"),
				"name":   []byte("dev-01"),
				"server": []byte("https://dev-01.example.com"),
			},
			Type: corev1.SecretType("Opaque"),
		},
		&corev1.Secret{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Secret",
				APIVersion: "v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:      "prod-01",
				Namespace: "namespace",
				Labels: map[string]string{
					"argocd.argoproj.io/secret-type": "cluster",
					"environment":                    "prod",
				},
			},
			Data: map[string][]byte{
				"config": []byte("{}"),
				"name":   []byte("prod-01"),
				"server": []byte("https://prod-01.example.com"),
			},
			Type: corev1.SecretType("Opaque"),
		},
	}

	for _, testCase := range testCases {
		testCaseCopy := testCase // Since tests may run in parallel

		t.Run(testCaseCopy.name, func(t *testing.T) {
			genMock := &generatorsMock.Generator{}
			appSet := &v1alpha1.ApplicationSet{}

			fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
			cl := &possiblyErroringFakeCtrlRuntimeClient{
				fakeClient,
				testCase.clientError,
			}
			clusterGenerator := NewClusterGenerator(cl, "namespace")

			for _, g := range testCaseCopy.baseGenerators {
				gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
					Git:      g.Git,
					Clusters: g.Clusters,
				}
				genMock.EXPECT().GenerateParams(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]any{
					{
						"path":                    "examples/git-generator-files-discovery/cluster-config/dev/config.json",
						"path.basename":           "dev",
						"path.basenameNormalized": "dev",
					},
					{
						"path":                    "examples/git-generator-files-discovery/cluster-config/prod/config.json",
						"path.basename":           "prod",
						"path.basenameNormalized": "prod",
					},
				}, nil)
				genMock.EXPECT().GetTemplate(&gitGeneratorSpec).
					Return(&v1alpha1.ApplicationSetTemplate{})
			}
			matrixGenerator := NewMatrixGenerator(
				map[string]Generator{
					"Git":      genMock,
					"Clusters": clusterGenerator,
				},
			)

			got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
				Matrix: &v1alpha1.MatrixGenerator{
					Generators: testCaseCopy.baseGenerators,
					Template:   v1alpha1.ApplicationSetTemplate{},
				},
			}, appSet, nil)

			if testCaseCopy.expectedErr != nil {
				require.ErrorIs(t, err, testCaseCopy.expectedErr)
			} else {
				require.NoError(t, err)
				assert.Equal(t, testCaseCopy.expected, got)
			}
		})
	}
}

func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
	interpolatedGitGenerator := &v1alpha1.GitGenerator{
		RepoURL:  "RepoURL",
		Revision: "Revision",
		Files: []v1alpha1.GitFileGeneratorItem{
			{Path: "examples/git-generator-files-discovery/cluster-config/dev/config.json"},
			{Path: "examples/git-generator-files-discovery/cluster-config/prod/config.json"},
		},
	}

	interpolatedClusterGenerator := &v1alpha1.ClusterGenerator{
		Selector: metav1.LabelSelector{
			MatchLabels:      map[string]string{"environment": "{{.path.basename}}"},
			MatchExpressions: nil,
		},
	}
	testCases := []struct {
		name           string
		baseGenerators []v1alpha1.ApplicationSetNestedGenerator
		expectedErr    error
		expected       []map[string]any
		clientError    bool
	}{
		{
			name: "happy flow - generate interpolated params",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: interpolatedGitGenerator,
				},
				{
					Clusters: interpolatedClusterGenerator,
				},
			},
			expected: []map[string]any{
				{
					"path": map[string]string{
						"path":               "examples/git-generator-files-discovery/cluster-config/dev/config.json",
						"basename":           "dev",
						"basenameNormalized": "dev",
					},
					"name":           "dev-01",
					"nameNormalized": "dev-01",
					"server":         "https://dev-01.example.com",
					"project":        "",
					"metadata": map[string]any{
						"labels": map[string]string{
							"environment":                    "dev",
							"argocd.argoproj.io/secret-type": "cluster",
						},
					},
				},
				{
					"path": map[string]string{
						"path":               "examples/git-generator-files-discovery/cluster-config/prod/config.json",
						"basename":           "prod",
						"basenameNormalized": "prod",
					},
					"name":           "prod-01",
					"nameNormalized": "prod-01",
					"server":         "https://prod-01.example.com",
					"project":        "",
					"metadata": map[string]any{
						"labels": map[string]string{
							"environment":                    "prod",
							"argocd.argoproj.io/secret-type": "cluster",
						},
					},
				},
			},
			clientError: false,
		},
	}
	clusters := []client.Object{
		&corev1.Secret{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Secret",
				APIVersion: "v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:      "dev-01",
				Namespace: "namespace",
				Labels: map[string]string{
					"argocd.argoproj.io/secret-type": "cluster",
					"environment":                    "dev",
				},
			},
			Data: map[string][]byte{
				"config": []byte("{}"),
				"name":   []byte("dev-01"),
				"server": []byte("https://dev-01.example.com"),
			},
			Type: corev1.SecretType("Opaque"),
		},
		&corev1.Secret{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Secret",
				APIVersion: "v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:      "prod-01",
				Namespace: "namespace",
				Labels: map[string]string{
					"argocd.argoproj.io/secret-type": "cluster",
					"environment":                    "prod",
				},
			},
			Data: map[string][]byte{
				"config": []byte("{}"),
				"name":   []byte("prod-01"),
				"server": []byte("https://prod-01.example.com"),
			},
			Type: corev1.SecretType("Opaque"),
		},
	}

	for _, testCase := range testCases {
		testCaseCopy := testCase // Since tests may run in parallel

		t.Run(testCaseCopy.name, func(t *testing.T) {
			genMock := &generatorsMock.Generator{}
			appSet := &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					GoTemplate: true,
				},
			}

			fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
			cl := &possiblyErroringFakeCtrlRuntimeClient{
				fakeClient,
				testCase.clientError,
			}
			clusterGenerator := NewClusterGenerator(cl, "namespace")

			for _, g := range testCaseCopy.baseGenerators {
				gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
					Git:      g.Git,
					Clusters: g.Clusters,
				}
				genMock.EXPECT().GenerateParams(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]any{
					{
						"path": map[string]string{
							"path":               "examples/git-generator-files-discovery/cluster-config/dev/config.json",
							"basename":           "dev",
							"basenameNormalized": "dev",
						},
					},
					{
						"path": map[string]string{
							"path":               "examples/git-generator-files-discovery/cluster-config/prod/config.json",
							"basename":           "prod",
							"basenameNormalized": "prod",
						},
					},
				}, nil)
				genMock.EXPECT().GetTemplate(&gitGeneratorSpec).
					Return(&v1alpha1.ApplicationSetTemplate{})
			}
			matrixGenerator := NewMatrixGenerator(
				map[string]Generator{
					"Git":      genMock,
					"Clusters": clusterGenerator,
				},
			)

			got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
				Matrix: &v1alpha1.MatrixGenerator{
					Generators: testCaseCopy.baseGenerators,
					Template:   v1alpha1.ApplicationSetTemplate{},
				},
			}, appSet, nil)

			if testCaseCopy.expectedErr != nil {
				require.ErrorIs(t, err, testCaseCopy.expectedErr)
			} else {
				require.NoError(t, err)
				assert.Equal(t, testCaseCopy.expected, got)
			}
		})
	}
}

func TestMatrixGenerateListElementsYaml(t *testing.T) {
	gitGenerator := &v1alpha1.GitGenerator{
		RepoURL:  "RepoURL",
		Revision: "Revision",
		Files: []v1alpha1.GitFileGeneratorItem{
			{Path: "config.yaml"},
		},
	}

	listGenerator := &v1alpha1.ListGenerator{
		Elements:     []apiextensionsv1.JSON{},
		ElementsYaml: "{{ .foo.bar | toJson }}",
	}

	testCases := []struct {
		name           string
		baseGenerators []v1alpha1.ApplicationSetNestedGenerator
		expectedErr    error
		expected       []map[string]any
	}{
		{
			name: "happy flow - generate params",
			baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
				{
					Git: gitGenerator,
				},
				{
					List: listGenerator,
				},
			},
			expected: []map[string]any{
				{
					"chart":   "a",
					"version": "1",
					"foo": map[string]any{
						"bar": []any{
							map[string]any{
								"chart":   "a",
								"version": "1",
							},
							map[string]any{
								"chart":   "b",
								"version": "2",
							},
						},
					},
					"path": map[string]any{
						"basename":           "dir",
						"basenameNormalized": "dir",
						"filename":           "file_name.yaml",
						"filenameNormalized": "file-name.yaml",
						"path":               "path/dir",
						"segments": []string{
							"path",
							"dir",
						},
					},
				},
				{
					"chart":   "b",
					"version": "2",
					"foo": map[string]any{
						"bar": []any{
							map[string]any{
								"chart":   "a",
								"version": "1",
							},
							map[string]any{
								"chart":   "b",
								"version": "2",
							},
						},
					},
					"path": map[string]any{
						"basename":           "dir",
						"basenameNormalized": "dir",
						"filename":           "file_name.yaml",
						"filenameNormalized": "file-name.yaml",
						"path":               "path/dir",
						"segments": []string{
							"path",
							"dir",
						},
					},
				},
			},
		},
	}

	for _, testCase := range testCases {
		testCaseCopy := testCase // Since tests may run in parallel

		t.Run(testCaseCopy.name, func(t *testing.T) {
			genMock := &generatorsMock.Generator{}
			appSet := &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "set",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					GoTemplate: true,
				},
			}

			for _, g := range testCaseCopy.baseGenerators {
				gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
					Git:  g.Git,
					List: g.List,
				}
				genMock.EXPECT().GenerateParams(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]any{{
					"foo": map[string]any{
						"bar": []any{
							map[string]any{
								"chart":   "a",
								"version": "1",
							},
							map[string]any{
								"chart":   "b",
								"version": "2",
							},
						},
					},
					"path": map[string]any{
						"basename":           "dir",
						"basenameNormalized": "dir",
						"filename":           "file_name.yaml",
						"filenameNormalized": "file-name.yaml",
						"path":               "path/dir",
						"segments": []string{
							"path",
							"dir",
						},
					},
				}}, nil)
				genMock.EXPECT().GetTemplate(&gitGeneratorSpec).
					Return(&v1alpha1.ApplicationSetTemplate{})
			}

			matrixGenerator := NewMatrixGenerator(
				map[string]Generator{
					"Git":  genMock,
					"List": &ListGenerator{},
				},
			)

			got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
				Matrix: &v1alpha1.MatrixGenerator{
					Generators: testCaseCopy.baseGenerators,
					Template:   v1alpha1.ApplicationSetTemplate{},
				},
			}, appSet, nil)

			if testCaseCopy.expectedErr != nil {
				require.ErrorIs(t, err, testCaseCopy.expectedErr)
			} else {
				require.NoError(t, err)
				assert.Equal(t, testCaseCopy.expected, got)
			}
		})
	}
}

func TestGitGenerator_GenerateParams_list_x_git_matrix_generator(t *testing.T) {
	// Given a matrix generator over a list generator and a git files generator, the nested git files generator should
	// be treated as a files generator, and it should produce parameters.

	// This tests for a specific bug where a nested git files generator was being treated as a directory generator. This
	// happened because, when the matrix generator was being processed, the nested git files generator was being
	// interpolated by the deeplyReplace function. That function cannot differentiate between a nil slice and an empty
	// slice. So it was replacing the `Directories` field with an empty slice, which the ApplicationSet controller
	// interpreted as meaning this was a directory generator, not a files generator.

	// Now instead of checking for nil, we check whether the field is a non-empty slice. This test prevents a regression
	// of that bug.

	listGeneratorMock := &generatorsMock.Generator{}
	listGeneratorMock.EXPECT().GenerateParams(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).Return([]map[string]any{
		{"some": "value"},
	}, nil)
	listGeneratorMock.EXPECT().GetTemplate(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator")).Return(&v1alpha1.ApplicationSetTemplate{})

	gitGeneratorSpec := &v1alpha1.GitGenerator{
		RepoURL: "https://git.example.com",
		Files: []v1alpha1.GitFileGeneratorItem{
			{Path: "some/path.json"},
		},
	}

	repoServiceMock := &servicesMocks.Repos{}
	repoServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string][]byte{
		"some/path.json": []byte("test: content"),
	}, nil).Maybe()
	gitGenerator := NewGitGenerator(repoServiceMock, "")

	matrixGenerator := NewMatrixGenerator(map[string]Generator{
		"List": listGeneratorMock,
		"Git":  gitGenerator,
	})

	matrixGeneratorSpec := &v1alpha1.MatrixGenerator{
		Generators: []v1alpha1.ApplicationSetNestedGenerator{
			{
				List: &v1alpha1.ListGenerator{
					Elements: []apiextensionsv1.JSON{
						{
							Raw: []byte(`{"some": "value"}`),
						},
					},
				},
			},
			{
				Git: gitGeneratorSpec,
			},
		},
	}

	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	appProject := v1alpha1.AppProject{}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()

	params, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
		Matrix: matrixGeneratorSpec,
	}, &v1alpha1.ApplicationSet{}, client)
	require.NoError(t, err)
	assert.Equal(t, []map[string]any{{
		"path":                    "some",
		"path.basename":           "some",
		"path.basenameNormalized": "some",
		"path.filename":           "path.json",
		"path.filenameNormalized": "path.json",
		"path[0]":                 "some",
		"some":                    "value",
		"test":                    "content",
	}}, params)
}

func TestGitGenerator_GenerateParams_list_x_git_matrix_generator_error_includes_repo(t *testing.T) {
	gitGeneratorSpec := &v1alpha1.GitGenerator{
		RepoURL:  "{{url}}",
		Revision: "HEAD",
		Files: []v1alpha1.GitFileGeneratorItem{
			{Path: "ste1/values.yaml"},
		},
	}

	repoServiceMock := &servicesMocks.Repos{}
	repoServiceMock.EXPECT().GetFiles(
		mock.Anything,
		"https://git.example.com/repo-a.git",
		mock.Anything,
		mock.Anything,
		"ste1/values.yaml",
		mock.Anything,
		mock.Anything,
	).Return(map[string][]byte{
		"ste1/values.yaml": []byte("invalid: ["),
	}, nil)
	gitGenerator := NewGitGenerator(repoServiceMock, "")

	matrixGenerator := NewMatrixGenerator(map[string]Generator{
		"List": &ListGenerator{},
		"Git":  gitGenerator,
	})

	matrixGeneratorSpec := &v1alpha1.MatrixGenerator{
		Generators: []v1alpha1.ApplicationSetNestedGenerator{
			{
				List: &v1alpha1.ListGenerator{
					Elements: []apiextensionsv1.JSON{
						{
							Raw: []byte(`{"url": "https://git.example.com/repo-a.git"}`),
						},
					},
				},
			},
			{
				Git: gitGeneratorSpec,
			},
		},
	}

	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	appProject := v1alpha1.AppProject{}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()

	_, err = matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
		Matrix: matrixGeneratorSpec,
	}, &v1alpha1.ApplicationSet{}, client)
	require.Error(t, err)
	require.ErrorContains(t, err, "https://git.example.com/repo-a.git")
	require.ErrorContains(t, err, "ste1/values.yaml")
}

func TestGitGenerator_GenerateParams_list_x_git_matrix_generator_go_templates_values(t *testing.T) {
	// Given a matrix generator over a list generator and a git  generator with values,
	// that contain a template that refers to got generator output parameters.
	// This tests for a specific bug where the second generator in the matrix
	// failed to evaluate value templates that referred to generator output parameters.

	listGeneratorMock := &generatorsMock.Generator{}
	listGeneratorMock.EXPECT().GenerateParams(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).Return([]map[string]any{
		{"some": "value"},
	}, nil)
	listGeneratorMock.EXPECT().GetTemplate(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator")).Return(&v1alpha1.ApplicationSetTemplate{})

	gitGeneratorSpec := &v1alpha1.GitGenerator{
		RepoURL: "https://git.example.com",
		Files: []v1alpha1.GitFileGeneratorItem{
			{Path: "some/path.json"},
		},
		Values: map[string]string{
			"foo": "{{.path.basename}}",
		},
	}

	repoServiceMock := &servicesMocks.Repos{}
	repoServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string][]byte{
		"some/path.json": []byte("test: content"),
	}, nil).Maybe()
	gitGenerator := NewGitGenerator(repoServiceMock, "")

	matrixGenerator := NewMatrixGenerator(map[string]Generator{
		"List": listGeneratorMock,
		"Git":  gitGenerator,
	})

	matrixGeneratorSpec := &v1alpha1.MatrixGenerator{
		Generators: []v1alpha1.ApplicationSetNestedGenerator{
			{
				List: &v1alpha1.ListGenerator{
					Elements: []apiextensionsv1.JSON{
						{
							Raw: []byte(`{"some": "value"}`),
						},
					},
				},
			},
			{
				Git: gitGeneratorSpec,
			},
		},
	}

	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	appProject := v1alpha1.AppProject{}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()

	params, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
		Matrix: matrixGeneratorSpec,
	}, &v1alpha1.ApplicationSet{
		Spec: v1alpha1.ApplicationSetSpec{
			GoTemplate: true,
		},
	}, client)
	require.NoError(t, err)
	assert.Equal(t, []map[string]any{{
		"path": map[string]any{
			"basename":           "some",
			"basenameNormalized": "some",
			"filename":           "path.json",
			"filenameNormalized": "path.json",
			"path":               "some",
			"segments":           []string{"some"},
		},
		"some": "value",
		"test": "content",
		"values": map[string]string{
			"foo": "some",
		},
	}}, params)
}

func TestInterpolatedMatrixGenerateGoTemplate_ClusterValuesFromFirstGenerator(t *testing.T) {
	const guestBook = "guestbook"

	gitGeneratorSpec, genMock, clusterGeneratorSpec := getGenerators()

	fakeClient := fake.NewClientBuilder().WithObjects([]client.Object{clusterSecret()}...).Build()
	clusterGenerator := NewClusterGenerator(fakeClient, "argocd")

	matrixGenerator := NewMatrixGenerator(map[string]Generator{
		"Git":      genMock,
		"Clusters": clusterGenerator,
	})

	params, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
		Matrix: &v1alpha1.MatrixGenerator{
			Generators: []v1alpha1.ApplicationSetNestedGenerator{
				{Git: gitGeneratorSpec},
				{Clusters: clusterGeneratorSpec},
			},
		},
	}, appSet(), fakeClient)

	require.NoError(t, err, "Expected cluster values referencing git params to resolve correctly")
	require.Len(t, params, 1, "Expected exactly one combined param set")

	assert.Equal(t, map[string]string{
		"path":               guestBook,
		"basename":           guestBook,
		"basenameNormalized": guestBook,
	}, params[0]["path"], "Git path params should be present")

	assert.Equal(t, map[string]string{
		"env": guestBook,
	}, params[0]["values"], "Cluster values.env should be resolved from git params")

	assert.Equal(t, "guestbook-cluster", params[0]["name"])
	assert.Equal(t, "https://guestbook.example.com", params[0]["server"])
}

func appSet() *v1alpha1.ApplicationSet {
	appSet := &v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "repro",
			Namespace: "argocd",
		},
		Spec: v1alpha1.ApplicationSetSpec{
			GoTemplate: true,
			// This is the most important part of the test because this is telling the code to return an error
			GoTemplateOptions: []string{"missingkey=error"},
		},
	}
	return appSet
}

func clusterSecret() *corev1.Secret {
	clusterSecret := &corev1.Secret{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Secret",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "guestbook-cluster",
			Namespace: "argocd",
			Labels: map[string]string{
				"argocd.argoproj.io/secret-type": "cluster",
				"environment":                    "guestbook",
			},
		},
		Data: map[string][]byte{
			"config": []byte("{}"),
			"name":   []byte("guestbook-cluster"),
			"server": []byte("https://guestbook.example.com"),
		},
		Type: corev1.SecretType("Opaque"),
	}
	return clusterSecret
}

func getGenerators() (*v1alpha1.GitGenerator, *generatorsMock.Generator, *v1alpha1.ClusterGenerator) {
	const guestBook = "guestbook"
	gitGeneratorSpec := &v1alpha1.GitGenerator{
		RepoURL:  "https://github.com/argoproj/argocd-example-apps.git",
		Revision: "HEAD",
		Directories: []v1alpha1.GitDirectoryGeneratorItem{
			{Path: guestBook},
		},
	}

	genMock := &generatorsMock.Generator{}
	genMock.EXPECT().
		GenerateParams(
			mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"),
			mock.AnythingOfType("*v1alpha1.ApplicationSet"),
			mock.Anything).
		Return([]map[string]any{
			{
				"path": map[string]string{
					"path":               guestBook,
					"basename":           guestBook,
					"basenameNormalized": guestBook,
				},
			},
		}, nil)
	genMock.EXPECT().
		GetTemplate(mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator")).
		Return(&v1alpha1.ApplicationSetTemplate{})

	clusterGeneratorSpec := &v1alpha1.ClusterGenerator{
		Selector: metav1.LabelSelector{
			MatchLabels: map[string]string{
				"environment": "{{.path.basename}}",
			},
		},
		Values: map[string]string{
			"env": "{{.path.basename}}",
		},
	}
	return gitGeneratorSpec, genMock, clusterGeneratorSpec
}
