/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package storageversionmigrator

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	svmv1beta1 "k8s.io/api/storagemigration/v1beta1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/discovery"
	fakediscovery "k8s.io/client-go/discovery/fake"
	"k8s.io/client-go/informers"
	svminformers "k8s.io/client-go/informers/storagemigration/v1beta1"
	"k8s.io/client-go/kubernetes"
	kubefake "k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/metadata"
	metadatafake "k8s.io/client-go/metadata/fake"
	kubetesting "k8s.io/client-go/testing"
)

func TestIsResourceMigratable(t *testing.T) {
	tcs := []struct {
		name      string
		resources []*metav1.APIResourceList
		resource  schema.GroupVersionResource
		want      bool
		wantErr   string
	}{
		{
			name: "migratable resource",
			resources: []*metav1.APIResourceList{
				{
					GroupVersion: "v1",
					APIResources: []metav1.APIResource{
						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
						{Name: "events", Namespaced: true, Kind: "Event", Verbs: []string{"get", "list", "watch", "create", "delete"}},
						{Name: "configmaps", Namespaced: true, Kind: "Event", Verbs: []string{"get", "watch", "create", "delete", "update", "patch", "delete"}},
					},
				},
			},
			resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
			want:     true,
		},
		{
			name: "non-updatable resource",
			resources: []*metav1.APIResourceList{
				{
					GroupVersion: "v1",
					APIResources: []metav1.APIResource{
						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
						{Name: "events", Namespaced: true, Kind: "Event", Verbs: []string{"get", "list", "watch", "create", "delete"}},
						{Name: "configmaps", Namespaced: true, Kind: "Event", Verbs: []string{"get", "watch", "create", "delete", "update", "patch", "delete"}},
					},
				},
			},
			resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "events"},
			want:     false,
		},
		{
			name: "non-patchable resource",
			resources: []*metav1.APIResourceList{
				{
					GroupVersion: "v1",
					APIResources: []metav1.APIResource{
						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
						{Name: "events", Namespaced: true, Kind: "Event", Verbs: []string{"get", "list", "watch", "create", "delete"}},
						{Name: "configmaps", Namespaced: true, Kind: "Configmap", Verbs: []string{"get", "watch", "create", "delete", "update", "patch", "delete"}},
						{Name: "secrets", Namespaced: true, Kind: "Secret", Verbs: []string{"get", "watch", "create", "delete", "update", "delete"}},
					},
				},
			},
			resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
			want:     false,
		},
		{
			name: "non-listable resource",
			resources: []*metav1.APIResourceList{
				{
					GroupVersion: "v1",
					APIResources: []metav1.APIResource{
						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
						{Name: "events", Namespaced: true, Kind: "Event", Verbs: []string{"get", "list", "watch", "create", "delete"}},
						{Name: "configmaps", Namespaced: true, Kind: "Event", Verbs: []string{"get", "watch", "create", "delete", "update", "patch", "delete"}},
					},
				},
			},
			resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"},
			want:     false,
		},
		{
			name: "unknown resource",
			resources: []*metav1.APIResourceList{
				{
					GroupVersion: "v1",
					APIResources: []metav1.APIResource{
						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
						{Name: "events", Namespaced: true, Kind: "Event", Verbs: []string{"get", "list", "watch", "create", "delete"}},
						{Name: "configmaps", Namespaced: true, Kind: "Configmap", Verbs: []string{"get", "watch", "create", "delete", "update", "patch", "delete"}},
					},
				},
			},
			resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "foo"},
			wantErr:  "resource \"/v1, Resource=foo\" not found in discovery",
		},
		{
			name: "multiple versions",
			resources: []*metav1.APIResourceList{
				{
					GroupVersion: "v1",
					APIResources: []metav1.APIResource{
						{Name: "foo", Namespaced: true, Kind: "Pod", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
					},
				},
				{
					GroupVersion: "v1alpha1",
					APIResources: []metav1.APIResource{
						{Name: "foo", Namespaced: true, Kind: "Pod", Verbs: []string{"get", "watch", "create", "update", "patch", "delete"}},
					},
				},
			},
			resource: schema.GroupVersionResource{Group: "", Version: "v1alpha", Resource: "foo"},
			want:     false,
		},
		{
			name: "multiple versions and groups",
			resources: []*metav1.APIResourceList{
				{
					GroupVersion: "v1",
					APIResources: []metav1.APIResource{
						{Name: "foo", Namespaced: true, Kind: "Foo", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
					},
				},
				{
					GroupVersion: "v1alpha1",
					APIResources: []metav1.APIResource{
						{Name: "foo", Namespaced: true, Kind: "Foo", Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
					},
				},
				{
					GroupVersion: "bar/v1alpha1",
					APIResources: []metav1.APIResource{
						{Name: "foo", Namespaced: true, Kind: "Foo", Group: "bar", Verbs: []string{"get", "watch", "create", "update", "patch", "delete"}},
					},
				},
			},
			resource: schema.GroupVersionResource{Group: "bar", Version: "v1alpha1", Resource: "foo"},
			want:     false,
		},
	}

	for _, tc := range tcs {
		t.Run(tc.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}))
			defer server.Close()
			discoveryClient := fakediscovery.FakeDiscovery{Fake: &kubetesting.Fake{}}
			discoveryClient.Resources = tc.resources
			rvController := ResourceVersionController{
				discoveryClient: &discoveryClient,
			}

			isMigratable, err := rvController.isResourceMigratable(tc.resource)
			if err != nil {
				if !strings.Contains(err.Error(), tc.wantErr) {
					t.Errorf("Unexpected error: %v, want: %v", err, tc.wantErr)
				}
				return
			}
			if isMigratable != tc.want {
				t.Errorf("Expected %v, got %v", tc.want, isMigratable)
			}
		})
	}
}

func TestRVSync(t *testing.T) {
	testCases := []struct {
		name               string
		key                string
		svm                *svmv1beta1.StorageVersionMigration
		discoveryResources *metav1.APIResourceList
		metadataList       *metav1.List
		metadataErr        bool
		expectErr          string
		expectKubeActions  []kubetesting.Action
	}{
		{
			name: "Migration not active",
			key:  "inactive-svm",
			svm:  newSVMWithConditions("inactive-svm", "", []metav1.Condition{}),
		},
		{
			name: "Successful RV acquisition",
			key:  "test-svm",
			svm: newSVMWithConditions("test-svm", "", []metav1.Condition{
				{
					Type:   string(svmv1beta1.MigrationRunning),
					Status: metav1.ConditionTrue,
				},
			}),
			discoveryResources: &metav1.APIResourceList{
				GroupVersion: "apps/v1",
				APIResources: []metav1.APIResource{
					{Name: "deployments", Namespaced: true, Kind: "Deployment", Verbs: []string{"list", "update", "patch"}},
				},
			},
			metadataList: &metav1.List{
				ListMeta: metav1.ListMeta{
					ResourceVersion: "12345",
				},
			},
			expectKubeActions: []kubetesting.Action{
				kubetesting.NewUpdateAction(
					svmv1beta1.SchemeGroupVersion.WithResource("storageversionmigrations"),
					"",
					newSVMWithConditions("test-svm", "12345", []metav1.Condition{
						{
							Type:   string(svmv1beta1.MigrationRunning),
							Status: metav1.ConditionTrue,
						},
					}),
				),
			},
		},
		{
			name: "SVM not found",
			key:  "non-existent-svm",
			svm:  nil,
		},
		{
			name: "SVM has no CRD condition",
			key:  "succeeded-svm",
			svm:  newSVM("succeeded-svm", ""),
		},
		{
			name: "SVM already succeeded",
			key:  "succeeded-svm",
			svm: newSVMWithConditions("succeeded-svm", "100", []metav1.Condition{
				{
					Type:   string(svmv1beta1.MigrationSucceeded),
					Status: metav1.ConditionTrue,
				},
			}),
		},
		{
			name: "SVM already failed",
			key:  "failed-svm",
			svm: newSVMWithConditions("failed-svm", "100", []metav1.Condition{
				{
					Type:   string(svmv1beta1.MigrationFailed),
					Status: metav1.ConditionTrue,
				},
			}),
		},
		{
			name: "RV already set",
			key:  "rv-set-svm",
			svm: newSVMWithConditions("rv-set-svm", "123", []metav1.Condition{
				{
					Type:   string(svmv1beta1.MigrationRunning),
					Status: metav1.ConditionTrue,
				},
			}),
		},
		{
			name: "Resource not migratable",
			key:  "not-migratable-svm",
			svm: newSVMWithConditions("not-migratable-svm", "", []metav1.Condition{
				{
					Type:   string(svmv1beta1.MigrationRunning),
					Status: metav1.ConditionTrue,
				},
			}),
			discoveryResources: &metav1.APIResourceList{
				GroupVersion: "apps/v1",
				APIResources: []metav1.APIResource{
					{Name: "deployments", Namespaced: true, Kind: "Deployment", Verbs: []string{"update", "patch"}}, // Missing "list"
				},
			},
			expectKubeActions: []kubetesting.Action{
				kubetesting.NewUpdateAction(
					svmv1beta1.SchemeGroupVersion.WithResource("storageversionmigrations"),
					"",
					newSVMWithConditions("not-migratable-svm", "", []metav1.Condition{
						{
							Type:   string(svmv1beta1.MigrationRunning),
							Status: metav1.ConditionTrue,
						},
						{
							Type:   string(svmv1beta1.MigrationFailed),
							Status: metav1.ConditionTrue,
						},
					}),
				),
			},
		},
		{
			name: "Metadata list error",
			key:  "metadata-error-svm",
			svm: newSVMWithConditions("metadata-error-svm", "", []metav1.Condition{
				{
					Type:   string(svmv1beta1.MigrationRunning),
					Status: metav1.ConditionTrue,
				},
			}),
			discoveryResources: &metav1.APIResourceList{
				GroupVersion: "apps/v1",
				APIResources: []metav1.APIResource{
					{Name: "deployments", Namespaced: true, Kind: "Deployment", Verbs: []string{"list", "update", "patch"}},
				},
			},
			metadataList: &metav1.List{},
			metadataErr:  true,
			expectErr:    "error getting latest resourceVersion for apps/v1",
		},
		{
			name: "Invalid RV returned",
			key:  "invalid-rv-svm",
			svm: newSVMWithConditions("invalid-rv-svm", "", []metav1.Condition{
				{
					Type:   string(svmv1beta1.MigrationRunning),
					Status: metav1.ConditionTrue,
				},
			}),
			discoveryResources: &metav1.APIResourceList{
				GroupVersion: "apps/v1",
				APIResources: []metav1.APIResource{
					{Name: "deployments", Namespaced: true, Kind: "Deployment", Verbs: []string{"list", "update", "patch"}},
				},
			},
			metadataList: &metav1.List{
				ListMeta: metav1.ListMeta{
					ResourceVersion: "abcde",
				},
			},
			expectKubeActions: []kubetesting.Action{
				kubetesting.NewUpdateAction(
					svmv1beta1.SchemeGroupVersion.WithResource("storageversionmigrations"),
					"",
					newSVMWithConditions("invalid-rv-svm", "", []metav1.Condition{
						{
							Type:   string(svmv1beta1.MigrationRunning),
							Status: metav1.ConditionTrue,
						},
						{
							Type:   string(svmv1beta1.MigrationFailed),
							Status: metav1.ConditionTrue,
						},
					}),
				),
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := context.Background()
			var initialSVMs []runtime.Object
			if tc.svm != nil {
				initialSVMs = append(initialSVMs, tc.svm)
			}
			kubeClient := kubefake.NewClientset(initialSVMs...)
			kubeInformerFactory := informers.NewSharedInformerFactory(kubeClient, 0)
			svmInformer := kubeInformerFactory.Storagemigration().V1beta1().StorageVersionMigrations()

			if tc.svm != nil {
				err := svmInformer.Informer().GetStore().Add(tc.svm)
				require.NoError(t, err)
			}

			// Setup fake discovery client
			discoveryClient := &fakediscovery.FakeDiscovery{Fake: &kubetesting.Fake{}}
			if tc.discoveryResources != nil {
				discoveryClient.Resources = []*metav1.APIResourceList{tc.discoveryResources}
			}

			// Setup fake metadata client
			metadatascheme := metadatafake.NewTestScheme()
			err := svmv1beta1.AddToScheme(metadatascheme)
			require.NoError(t, err)
			err = metav1.AddMetaToScheme(metadatascheme)
			require.NoError(t, err)
			metadataClient := metadatafake.NewSimpleMetadataClient(metadatascheme)

			metadataClient.Fake.PrependReactor("list", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
				listAction, ok := action.(kubetesting.ListAction)
				require.True(t, ok, "expected ListAction")

				require.Equal(t, testGVR, listAction.GetResource(), "GVR in metadata list action does not match testGVR")

				isNamespaced := false
				if tc.discoveryResources != nil && len(tc.discoveryResources.APIResources) > 0 {
					isNamespaced = tc.discoveryResources.APIResources[0].Namespaced
				}

				if isNamespaced {
					require.Equal(t, fakeSVMNamespaceName, listAction.GetNamespace(), "expected list on fake namespace")
				} else {
					require.Empty(t, listAction.GetNamespace(), "expected cluster-scoped list")
				}

				if tc.metadataErr {
					return true, nil, fmt.Errorf("failed to list")
				}
				return true, tc.metadataList, nil
			})

			controller := newTestRVController(kubeClient, discoveryClient, metadataClient, svmInformer)

			err = controller.sync(ctx, tc.key)

			if tc.expectErr != "" {
				require.ErrorContains(t, err, tc.expectErr)
			} else {
				require.NoError(t, err)
			}

			kubeActions := filterListWatchActions(kubeClient.Actions())
			if tc.expectKubeActions == nil {
				require.Empty(t, kubeActions, "expected zero kube client actions")
				return
			}

			require.Len(t, kubeActions, len(tc.expectKubeActions), "mismatched number of kube client actions")
			for i, expected := range tc.expectKubeActions {
				actual := kubeActions[i]
				require.Equal(t, expected.GetVerb(), actual.GetVerb(), "kube action %d: verb mismatch", i)
				require.Equal(t, expected.GetResource(), actual.GetResource(), "kube action %d: resource mismatch", i)

				actualSvm := actual.(kubetesting.UpdateAction).GetObject().(*svmv1beta1.StorageVersionMigration)
				expectedSvm := expected.(kubetesting.UpdateAction).GetObject().(*svmv1beta1.StorageVersionMigration)

				// Check the important parts: ResourceVersion and Conditions
				require.Equal(t, expectedSvm.Status.ResourceVersion, actualSvm.Status.ResourceVersion, "kube action %d: status.resourceVersion mismatch", i)

				expectedConditions := expectedSvm.Status.Conditions
				actualConditions := actualSvm.Status.Conditions
				require.Len(t, actualConditions, len(expectedConditions), "kube action %d: conditions length mismatch", i)

				for j, expectedCondition := range expectedConditions {
					actualCondition := actualConditions[j]
					require.Equal(t, expectedCondition.Type, actualCondition.Type, "kube action %d: condition %d type mismatch", i, j)
					require.Equal(t, expectedCondition.Status, actualCondition.Status, "kube action %d: condition %d status mismatch", i, j)
				}
			}
		})
	}
}

// filterListWatchActions filters out list/watch actions from the client-go fake client.
func filterListWatchActions(actions []kubetesting.Action) []kubetesting.Action {
	var filteredActions []kubetesting.Action
	for _, action := range actions {
		if action.GetVerb() == "list" || action.GetVerb() == "watch" {
			continue
		}
		filteredActions = append(filteredActions, action)
	}
	return filteredActions
}

// newTestRVController creates a new ResourceVersionController for testing.
func newTestRVController(
	kubeClient kubernetes.Interface,
	discoveryClient discovery.DiscoveryInterface,
	metadataClient metadata.Interface,
	svmInformer svminformers.StorageVersionMigrationInformer,
) *ResourceVersionController {
	mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{testGVK.GroupVersion()})
	mapper.Add(testGVK, meta.RESTScopeNamespace)

	rvController := &ResourceVersionController{
		kubeClient:      kubeClient,
		discoveryClient: discoveryClient,
		metadataClient:  metadataClient,
		svmListers:      svmInformer.Lister(),
		svmSynced:       func() bool { return true },
		mapper:          mapper,
	}
	return rvController
}
