package restmapper

import (
	"sync"
	"testing"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	discoveryfake "k8s.io/client-go/discovery/fake"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/restmapper"
	coretesting "k8s.io/client-go/testing"
	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)

var fakeResources = []*metav1.APIResourceList{
	{
		GroupVersion: "apps/v1",
		APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}},
	},
	{
		GroupVersion: "v1",
		APIResources: []metav1.APIResource{{Name: "pods", Namespaced: true, Kind: "Pod"}},
	},
	{
		GroupVersion: "v2",
		APIResources: []metav1.APIResource{{Name: "pods", Namespaced: true, Kind: "Pod"}},
	},
	{
		GroupVersion: "extensions/v1beta",
		APIResources: []metav1.APIResource{{Name: "jobs", Namespaced: true, Kind: "Job"}},
	},
}

// getGVRTestCases organizes the test cases for GetGroupVersionResource.
// It can be shared by both benchmark and unit test.
var getGVRTestCases = []struct {
	name        string
	inputGVK    schema.GroupVersionKind
	expectedGVR schema.GroupVersionResource
	expectErr   bool
}{
	{
		name:        "v1,Pod cache miss",
		inputGVK:    schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
		expectedGVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
		expectErr:   false,
	},
	{
		name:        "v2,Pod cache miss",
		inputGVK:    schema.GroupVersionKind{Group: "", Version: "v2", Kind: "Pod"},
		expectedGVR: schema.GroupVersionResource{Group: "", Version: "v2", Resource: "pods"},
		expectErr:   false,
	},
	{
		name:        "extensions/v1beta,Job cache miss",
		inputGVK:    schema.GroupVersionKind{Group: "extensions", Version: "v1beta", Kind: "Job"},
		expectedGVR: schema.GroupVersionResource{Group: "extensions", Version: "v1beta", Resource: "jobs"},
		expectErr:   false,
	},
	{
		name:        "v1,Pod cache hit once",
		inputGVK:    schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
		expectedGVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
		expectErr:   false,
	},
	{
		name:        "v1,Pod cache hit twice",
		inputGVK:    schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
		expectedGVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
		expectErr:   false,
	},
	{
		name:        "v2,Pod cache hit once",
		inputGVK:    schema.GroupVersionKind{Group: "", Version: "v2", Kind: "Pod"},
		expectedGVR: schema.GroupVersionResource{Group: "", Version: "v2", Resource: "pods"},
		expectErr:   false,
	},
	{
		name:        "v2,Pod cache hit twice",
		inputGVK:    schema.GroupVersionKind{Group: "", Version: "v2", Kind: "Pod"},
		expectedGVR: schema.GroupVersionResource{Group: "", Version: "v2", Resource: "pods"},
		expectErr:   false,
	},
	{
		name:        "extensions/v1beta,Job cache hit once",
		inputGVK:    schema.GroupVersionKind{Group: "extensions", Version: "v1beta", Kind: "Job"},
		expectedGVR: schema.GroupVersionResource{Group: "extensions", Version: "v1beta", Resource: "jobs"},
		expectErr:   false,
	},
	{
		name:        "extensions/v1beta,Job cache hit twice",
		inputGVK:    schema.GroupVersionKind{Group: "extensions", Version: "v1beta", Kind: "Job"},
		expectedGVR: schema.GroupVersionResource{Group: "extensions", Version: "v1beta", Resource: "jobs"},
		expectErr:   false,
	},
	{
		name:      "cache miss and invalidate the cache",
		inputGVK:  schema.GroupVersionKind{Group: "non-existence", Version: "non-existence", Kind: "non-existence"},
		expectErr: true,
	},
}

var discoveryClient = &discoveryfake.FakeDiscovery{Fake: &coretesting.Fake{Resources: fakeResources}}

func BenchmarkGetGroupVersionResource(b *testing.B) {
	var option = apiutil.WithCustomMapper(func() (meta.RESTMapper, error) {
		groupResources, err := restmapper.GetAPIGroupResources(discoveryClient)
		if err != nil {
			return nil, err
		}
		return restmapper.NewDiscoveryRESTMapper(groupResources), nil
	})

	mapper, err := apiutil.NewDynamicRESTMapper(&rest.Config{}, option)
	if err != nil {
		b.Error(err)
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for _, tc := range getGVRTestCases {
			_, err := GetGroupVersionResource(mapper, tc.inputGVK)
			if (err != nil && !tc.expectErr) || (err == nil && tc.expectErr) {
				b.Errorf("GetGroupVersionResource For %#v Error: %v, wantErr: %v", tc.inputGVK, err, tc.expectErr)
			}
		}
	}
}

func BenchmarkGetGroupVersionResourceWithCache(b *testing.B) {
	cachedMapper := &cachedRESTMapper{}

	var option = apiutil.WithCustomMapper(func() (meta.RESTMapper, error) {
		groupResources, err := restmapper.GetAPIGroupResources(discoveryClient)
		if err != nil {
			return nil, err
		}
		// clear the cache map when reloading DiscoveryRESTMapper
		cachedMapper.gvkToGVR = sync.Map{}
		return restmapper.NewDiscoveryRESTMapper(groupResources), nil
	})

	mapper, err := apiutil.NewDynamicRESTMapper(&rest.Config{}, option)
	if err != nil {
		b.Error(err)
	}

	cachedMapper.restMapper = mapper

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for _, tc := range getGVRTestCases {
			_, err := GetGroupVersionResource(cachedMapper, tc.inputGVK)
			if (err != nil && !tc.expectErr) || (err == nil && tc.expectErr) {
				b.Errorf("GetGroupVersionResource For %#v Error: %v, wantErr: %v", tc.inputGVK, err, tc.expectErr)
			}
		}
	}
}

func TestGetGroupVersionResourceWithCache(t *testing.T) {
	cachedMapper := &cachedRESTMapper{}

	var option = apiutil.WithCustomMapper(func() (meta.RESTMapper, error) {
		groupResources, err := restmapper.GetAPIGroupResources(discoveryClient)
		if err != nil {
			return nil, err
		}
		// clear the cache map when reloading DiscoveryRESTMapper
		cachedMapper.gvkToGVR = sync.Map{}
		return restmapper.NewDiscoveryRESTMapper(groupResources), nil
	})

	mapper, err := apiutil.NewDynamicRESTMapper(&rest.Config{}, option)
	if err != nil {
		t.Error(err)
	}

	cachedMapper.restMapper = mapper

	for _, tc := range getGVRTestCases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := GetGroupVersionResource(cachedMapper, tc.inputGVK)
			if (err != nil && !tc.expectErr) || (err == nil && tc.expectErr) {
				t.Fatalf("GetGroupVersionResource (%#v) error: %v, wantErr: %v", tc.inputGVK, err, tc.expectErr)
			}

			if got != tc.expectedGVR {
				t.Fatalf("GetGroupVersionResource(%#v) = %#v, want: %#v", tc.inputGVK, got, tc.expectedGVR)
			}
		})
	}
}
