// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package issuerconfig

import (
	"math/rand"
	"sort"
	"testing"
	"testing/quick"
	"time"

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

	"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
)

func TestMergeStrategy(t *testing.T) {
	t1 := metav1.Now()
	t2 := metav1.NewTime(metav1.Now().Add(-1 * time.Hour))

	tests := []struct {
		name           string
		configToUpdate v1alpha1.CredentialIssuerStatus
		strategy       v1alpha1.CredentialIssuerStrategy
		expected       v1alpha1.CredentialIssuerStatus
	}{
		{
			name: "new entry",
			configToUpdate: v1alpha1.CredentialIssuerStatus{
				Strategies: nil,
			},
			strategy: v1alpha1.CredentialIssuerStrategy{
				Type:           "Type1",
				Status:         v1alpha1.SuccessStrategyStatus,
				Reason:         "some reason",
				Message:        "some message",
				LastUpdateTime: t1,
			},
			expected: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type1",
						Status:         v1alpha1.SuccessStrategyStatus,
						Reason:         "some reason",
						Message:        "some message",
						LastUpdateTime: t1,
					},
				},
			},
		},
		{
			name: "new entry updating deprecated kubeConfigInfo",
			configToUpdate: v1alpha1.CredentialIssuerStatus{
				Strategies: nil,
			},
			strategy: v1alpha1.CredentialIssuerStrategy{
				Type:           "Type1",
				Status:         v1alpha1.SuccessStrategyStatus,
				Reason:         "some reason",
				Message:        "some message",
				LastUpdateTime: t1,
				Frontend: &v1alpha1.CredentialIssuerFrontend{
					Type: "TokenCredentialRequestAPI",
					TokenCredentialRequestAPIInfo: &v1alpha1.TokenCredentialRequestAPIInfo{
						Server:                   "https://test-server",
						CertificateAuthorityData: "test-ca-bundle",
					},
				},
			},
			expected: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type1",
						Status:         v1alpha1.SuccessStrategyStatus,
						Reason:         "some reason",
						Message:        "some message",
						LastUpdateTime: t1,
						Frontend: &v1alpha1.CredentialIssuerFrontend{
							Type: "TokenCredentialRequestAPI",
							TokenCredentialRequestAPIInfo: &v1alpha1.TokenCredentialRequestAPIInfo{
								Server:                   "https://test-server",
								CertificateAuthorityData: "test-ca-bundle",
							},
						},
					},
				},
				KubeConfigInfo: &v1alpha1.CredentialIssuerKubeConfigInfo{
					Server:                   "https://test-server",
					CertificateAuthorityData: "test-ca-bundle",
				},
			},
		},
		{
			name: "existing entry to update",
			configToUpdate: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type1",
						Status:         v1alpha1.ErrorStrategyStatus,
						Reason:         "some starting reason",
						Message:        "some starting message",
						LastUpdateTime: t2,
					},
				},
			},
			strategy: v1alpha1.CredentialIssuerStrategy{
				Type:           "Type1",
				Status:         v1alpha1.SuccessStrategyStatus,
				Reason:         "some reason",
				Message:        "some message",
				LastUpdateTime: t1,
			},
			expected: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type1",
						Status:         v1alpha1.SuccessStrategyStatus,
						Reason:         "some reason",
						Message:        "some message",
						LastUpdateTime: t1,
					},
				},
			},
		},
		{
			name: "existing entry matches except for LastUpdated time",
			configToUpdate: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type1",
						Status:         v1alpha1.ErrorStrategyStatus,
						Reason:         "some starting reason",
						Message:        "some starting message",
						LastUpdateTime: t1,
					},
				},
			},
			strategy: v1alpha1.CredentialIssuerStrategy{
				Type:           "Type1",
				Status:         v1alpha1.ErrorStrategyStatus,
				Reason:         "some starting reason",
				Message:        "some starting message",
				LastUpdateTime: t2,
			},
			expected: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type1",
						Status:         v1alpha1.ErrorStrategyStatus,
						Reason:         "some starting reason",
						Message:        "some starting message",
						LastUpdateTime: t1,
					},
				},
			},
		},
		{
			name: "new entry among others",
			configToUpdate: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type0",
						Status:         v1alpha1.ErrorStrategyStatus,
						Reason:         "some starting reason 0",
						Message:        "some starting message 0",
						LastUpdateTime: t2,
					},
					{
						Type:           "Type2",
						Status:         v1alpha1.ErrorStrategyStatus,
						Reason:         "some starting reason 0",
						Message:        "some starting message 0",
						LastUpdateTime: t2,
					},
				},
			},
			strategy: v1alpha1.CredentialIssuerStrategy{
				Type:           "Type1",
				Status:         v1alpha1.SuccessStrategyStatus,
				Reason:         "some reason",
				Message:        "some message",
				LastUpdateTime: t1,
			},
			expected: v1alpha1.CredentialIssuerStatus{
				Strategies: []v1alpha1.CredentialIssuerStrategy{
					{
						Type:           "Type0",
						Status:         v1alpha1.ErrorStrategyStatus,
						Reason:         "some starting reason 0",
						Message:        "some starting message 0",
						LastUpdateTime: t2,
					},
					// Expect the Type1 entry to be sorted alphanumerically between the existing entries.
					{
						Type:           "Type1",
						Status:         v1alpha1.SuccessStrategyStatus,
						Reason:         "some reason",
						Message:        "some message",
						LastUpdateTime: t1,
					},
					{
						Type:           "Type2",
						Status:         v1alpha1.ErrorStrategyStatus,
						Reason:         "some starting reason 0",
						Message:        "some starting message 0",
						LastUpdateTime: t2,
					},
				},
			},
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			updated := tt.configToUpdate.DeepCopy()
			mergeStrategy(updated, tt.strategy)
			require.Equal(t, &tt.expected, updated)
		})
	}
}

func TestStrategySorting(t *testing.T) {
	expected := []v1alpha1.CredentialIssuerStrategy{
		{Type: v1alpha1.KubeClusterSigningCertificateStrategyType},
		{Type: v1alpha1.ImpersonationProxyStrategyType},
		{Type: "Type1"},
		{Type: "Type2"},
		{Type: "Type3"},
	}
	require.NoError(t, quick.Check(func(seed int64) bool {
		// Create a randomly shuffled copy of the expected output.
		//nolint:gosec // this is not meant to be a secure random, just a seeded RNG for shuffling deterministically
		rng := rand.New(rand.NewSource(seed))
		output := make([]v1alpha1.CredentialIssuerStrategy, len(expected))
		copy(output, expected)
		rng.Shuffle(
			len(output),
			func(i, j int) { output[i], output[j] = output[j], output[i] },
		)

		// Sort it using the code under test.
		sort.Stable(sortableStrategies(output))

		// Assert that it's sorted back to the expected output order.
		return assert.Equal(t, expected, output)
	}, nil))
}