package generators

import (
	"context"
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
	"github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider"
	argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)

func TestSCMProviderGenerateParams(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name          string
		repos         []*scm_provider.Repository
		values        map[string]string
		expected      []map[string]any
		expectedError error
	}{
		{
			name: "Multiple repos with labels",
			repos: []*scm_provider.Repository{
				{
					Organization: "myorg",
					Repository:   "repo1",
					RepositoryId: 190320251,
					URL:          "git@github.com:myorg/repo1.git",
					Branch:       "main",
					SHA:          "0bc57212c3cbbec69d20b34c507284bd300def5b",
					Labels:       []string{"prod", "staging"},
				},
				{
					Organization: "myorg",
					Repository:   "repo2",
					RepositoryId: 190320252,
					URL:          "git@github.com:myorg/repo2.git",
					Branch:       "main",
					SHA:          "59d0",
				},
			},
			expected: []map[string]any{
				{
					"organization":     "myorg",
					"repository":       "repo1",
					"repository_id":    190320251,
					"url":              "git@github.com:myorg/repo1.git",
					"branch":           "main",
					"branchNormalized": "main",
					"sha":              "0bc57212c3cbbec69d20b34c507284bd300def5b",
					"short_sha":        "0bc57212",
					"short_sha_7":      "0bc5721",
					"labels":           "prod,staging",
				},
				{
					"organization":     "myorg",
					"repository":       "repo2",
					"repository_id":    190320252,
					"url":              "git@github.com:myorg/repo2.git",
					"branch":           "main",
					"branchNormalized": "main",
					"sha":              "59d0",
					"short_sha":        "59d0",
					"short_sha_7":      "59d0",
					"labels":           "",
				},
			},
		},
		{
			name: "Value interpolation",
			repos: []*scm_provider.Repository{
				{
					Organization: "myorg",
					Repository:   "repo3",
					RepositoryId: 190320253,
					URL:          "git@github.com:myorg/repo3.git",
					Branch:       "main",
					SHA:          "0bc57212c3cbbec69d20b34c507284bd300def5b",
					Labels:       []string{"prod", "staging"},
				},
			},
			values: map[string]string{
				"foo":                    "bar",
				"should_i_force_push_to": "{{ branch }}?",
			},
			expected: []map[string]any{
				{
					"organization":                  "myorg",
					"repository":                    "repo3",
					"repository_id":                 190320253,
					"url":                           "git@github.com:myorg/repo3.git",
					"branch":                        "main",
					"branchNormalized":              "main",
					"sha":                           "0bc57212c3cbbec69d20b34c507284bd300def5b",
					"short_sha":                     "0bc57212",
					"short_sha_7":                   "0bc5721",
					"labels":                        "prod,staging",
					"values.foo":                    "bar",
					"values.should_i_force_push_to": "main?",
				},
			},
		},
		{
			name: "Repos with and without id",
			repos: []*scm_provider.Repository{
				{
					Organization: "myorg",
					Repository:   "repo4",
					RepositoryId: "idaz09",
					URL:          "git@github.com:myorg/repo4.git",
					Branch:       "main",
					SHA:          "0bc57212c3cbbec69d20b34c507284bd300def5b",
				},
				{
					Organization: "myorg",
					Repository:   "repo5",
					URL:          "git@github.com:myorg/repo5.git",
					Branch:       "main",
					SHA:          "0bc57212c3cbbec69d20b34c507284bd300def5b",
				},
			},
			expected: []map[string]any{
				{
					"organization":     "myorg",
					"repository":       "repo4",
					"repository_id":    "idaz09",
					"url":              "git@github.com:myorg/repo4.git",
					"branch":           "main",
					"branchNormalized": "main",
					"sha":              "0bc57212c3cbbec69d20b34c507284bd300def5b",
					"short_sha":        "0bc57212",
					"short_sha_7":      "0bc5721",
					"labels":           "",
				},
				{
					"organization":     "myorg",
					"repository":       "repo5",
					"repository_id":    nil,
					"url":              "git@github.com:myorg/repo5.git",
					"branch":           "main",
					"branchNormalized": "main",
					"sha":              "0bc57212c3cbbec69d20b34c507284bd300def5b",
					"short_sha":        "0bc57212",
					"short_sha_7":      "0bc5721",
					"labels":           "",
				},
			},
		},
	}

	for _, testCase := range cases {
		testCaseCopy := testCase

		t.Run(testCaseCopy.name, func(t *testing.T) {
			t.Parallel()

			mockProvider := &scm_provider.MockProvider{
				Repos: testCaseCopy.repos,
			}
			scmGenerator := &SCMProviderGenerator{overrideProvider: mockProvider, SCMConfig: SCMConfig{enableSCMProviders: true}}
			applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "set",
				},
				Spec: argoprojiov1alpha1.ApplicationSetSpec{
					Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
						SCMProvider: &argoprojiov1alpha1.SCMProviderGenerator{
							Values: testCaseCopy.values,
						},
					}},
				},
			}

			got, err := scmGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, nil)

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

func TestAllowedSCMProvider(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name           string
		providerConfig *argoprojiov1alpha1.SCMProviderGenerator
	}{
		{
			name: "Error Github",
			providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
				Github: &argoprojiov1alpha1.SCMProviderGeneratorGithub{
					API: "https://myservice.mynamespace.svc.cluster.local",
				},
			},
		},
		{
			name: "Error Gitlab",
			providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
				Gitlab: &argoprojiov1alpha1.SCMProviderGeneratorGitlab{
					API: "https://myservice.mynamespace.svc.cluster.local",
				},
			},
		},
		{
			name: "Error Gitea",
			providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
				Gitea: &argoprojiov1alpha1.SCMProviderGeneratorGitea{
					API: "https://myservice.mynamespace.svc.cluster.local",
				},
			},
		},
		{
			name: "Error Bitbucket",
			providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
				BitbucketServer: &argoprojiov1alpha1.SCMProviderGeneratorBitbucketServer{
					API: "https://myservice.mynamespace.svc.cluster.local",
				},
			},
		},
		{
			name: "Error AzureDevops",
			providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
				AzureDevOps: &argoprojiov1alpha1.SCMProviderGeneratorAzureDevOps{
					API: "https://myservice.mynamespace.svc.cluster.local",
				},
			},
		},
	}

	for _, testCase := range cases {
		testCaseCopy := testCase

		t.Run(testCaseCopy.name, func(t *testing.T) {
			t.Parallel()

			scmGenerator := &SCMProviderGenerator{
				SCMConfig: SCMConfig{
					allowedSCMProviders: []string{
						"github.myorg.com",
						"gitlab.myorg.com",
						"gitea.myorg.com",
						"bitbucket.myorg.com",
						"azuredevops.myorg.com",
					},
					enableSCMProviders: true,
				},
			}

			applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "set",
				},
				Spec: argoprojiov1alpha1.ApplicationSetSpec{
					Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
						SCMProvider: testCaseCopy.providerConfig,
					}},
				},
			}

			_, err := scmGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, nil)

			require.Error(t, err, "Must return an error")
			var expectedError ErrDisallowedSCMProvider
			assert.ErrorAs(t, err, &expectedError)
		})
	}
}

func TestSCMProviderDisabled_SCMGenerator(t *testing.T) {
	generator := &SCMProviderGenerator{SCMConfig: SCMConfig{enableSCMProviders: false}}

	applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name: "set",
		},
		Spec: argoprojiov1alpha1.ApplicationSetSpec{
			Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
				SCMProvider: &argoprojiov1alpha1.SCMProviderGenerator{
					Github: &argoprojiov1alpha1.SCMProviderGeneratorGithub{
						API: "https://myservice.mynamespace.svc.cluster.local",
					},
				},
			}},
		},
	}

	_, err := generator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, nil)
	assert.ErrorIs(t, err, ErrSCMProvidersDisabled)
}

func TestNewSCMHTTPClient(t *testing.T) {
	t.Run("no proxy", func(t *testing.T) {
		g := &SCMProviderGenerator{
			SCMConfig: SCMConfig{
				scmProxyURL: "",
			},
		}
		client := g.newSCMHTTPClient()
		assert.NotNil(t, client)
		assert.Nil(t, client.Transport)
	})

	t.Run("with proxy", func(t *testing.T) {
		proxyURL := "http://proxy.example.com:8080"
		g := &SCMProviderGenerator{
			SCMConfig: SCMConfig{
				scmProxyURL: proxyURL,
				scmNoProxy:  "example.com",
			},
		}
		client := g.newSCMHTTPClient()
		assert.NotNil(t, client)
		require.NotNil(t, client.Transport)
		tr, ok := client.Transport.(*http.Transport)
		assert.True(t, ok)
		assert.NotNil(t, tr.Proxy)

		req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://github.com", http.NoBody)
		u, err := tr.Proxy(req)
		require.NoError(t, err)
		assert.NotNil(t, u)
		assert.Equal(t, "proxy.example.com:8080", u.Host)

		reqNoProxy, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com", http.NoBody)
		u, err = tr.Proxy(reqNoProxy)
		require.NoError(t, err)
		assert.Nil(t, u)
	})
}

func TestNewSCMConfig(t *testing.T) {
	scmRootCAPath := "/path/to/ca"
	allowedSCMProviders := []string{"github.com", "gitlab.com"}
	enableSCMProviders := true
	enableGitHubAPIMetrics := true
	gitHubApps := github_app_auth.Credentials(nil)
	tokenRefStrictMode := true

	t.Run("default config", func(t *testing.T) {
		config := NewSCMConfig(scmRootCAPath, allowedSCMProviders, enableSCMProviders, enableGitHubAPIMetrics, gitHubApps, tokenRefStrictMode)
		assert.Equal(t, scmRootCAPath, config.scmRootCAPath)
		assert.Equal(t, allowedSCMProviders, config.allowedSCMProviders)
		assert.Equal(t, enableSCMProviders, config.enableSCMProviders)
		assert.Equal(t, enableGitHubAPIMetrics, config.enableGitHubAPIMetrics)
		assert.Equal(t, gitHubApps, config.GitHubApps)
		assert.Equal(t, tokenRefStrictMode, config.tokenRefStrictMode)
		assert.Empty(t, config.scmProxyURL)
		assert.Empty(t, config.scmNoProxy)
	})

	t.Run("config with options", func(t *testing.T) {
		proxyURL := "http://proxy.example.com"
		noProxy := "localhost,127.0.0.1"
		config := NewSCMConfig(scmRootCAPath, allowedSCMProviders, enableSCMProviders, enableGitHubAPIMetrics, gitHubApps, tokenRefStrictMode,
			WithProxyURL(proxyURL),
			WithNoProxyList(noProxy),
		)
		assert.Equal(t, proxyURL, config.scmProxyURL)
		assert.Equal(t, noProxy, config.scmNoProxy)
	})
}

func TestGithubProvider_ProxyWithoutMetrics(t *testing.T) {
	proxyURL := "http://proxy.example.com:8080"

	t.Run("token-based auth passes httpClient when metrics disabled", func(t *testing.T) {
		g := &SCMProviderGenerator{
			SCMConfig: SCMConfig{
				enableGitHubAPIMetrics: false,
				scmProxyURL:            proxyURL,
			},
		}
		httpClient := g.newSCMHTTPClient()

		// Verify the client has a proxy-configured transport
		require.NotNil(t, httpClient.Transport)
		tr, ok := httpClient.Transport.(*http.Transport)
		require.True(t, ok)
		require.NotNil(t, tr.Proxy)

		req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://github.com", http.NoBody)
		u, err := tr.Proxy(req)
		require.NoError(t, err)
		assert.Equal(t, "proxy.example.com:8080", u.Host)
	})

	t.Run("no proxy configured returns bare client when metrics disabled", func(t *testing.T) {
		g := &SCMProviderGenerator{
			SCMConfig: SCMConfig{
				enableGitHubAPIMetrics: false,
				scmProxyURL:            "",
			},
		}
		httpClient := g.newSCMHTTPClient()

		assert.NotNil(t, httpClient)
		assert.Nil(t, httpClient.Transport) // bare &http.Client{} — uses DefaultTransport
	})
}
