package converter

import (
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	openapiv2 "github.com/google/gnostic-models/openapiv2"
	"github.com/rancher/apiserver/pkg/types"
	"github.com/rancher/steve/pkg/schema/table"
	"github.com/rancher/wrangler/v3/pkg/generic/fake"
	wranglerSchema "github.com/rancher/wrangler/v3/pkg/schemas"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"
	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/kube-openapi/pkg/util/proto"
)

func TestToSchemas(t *testing.T) {
	gvkExtensionMap := map[any]any{
		gvkExtensionGroup:   "TestGroup",
		gvkExtensionVersion: "v1",
		gvkExtensionKind:    "TestResource",
	}
	gvkExtensionSlice := []any{gvkExtensionMap}
	extensionSliceYaml, err := yaml.Marshal(gvkExtensionSlice)
	require.NoError(t, err)
	gvkSchema := openapiv2.NamedSchema{
		Name: "TestResources",
		Value: &openapiv2.Schema{
			Description: "TestResources are test resource created for unit tests",
			Type: &openapiv2.TypeItem{
				Value: []string{"object"},
			},
			Properties: &openapiv2.Properties{
				AdditionalProperties: []*openapiv2.NamedSchema{},
			},
			VendorExtension: []*openapiv2.NamedAny{
				{
					Name: gvkExtensionName,
					Value: &openapiv2.Any{
						Yaml: string(extensionSliceYaml),
					},
				},
			},
		},
	}
	tests := []struct {
		name          string
		groups        []schema.GroupVersion
		resources     map[schema.GroupVersion][]metav1.APIResource
		crds          []v1.CustomResourceDefinition
		document      *openapiv2.Document
		discoveryErr  error
		documentErr   error
		crdErr        error
		wantError     bool
		desiredSchema map[string]*types.APISchema
	}{
		{
			name:   "crd listed in discovery, defined in crds",
			groups: []schema.GroupVersion{{Group: "TestGroup", Version: "v1"}},
			resources: map[schema.GroupVersion][]metav1.APIResource{
				{Group: "TestGroup", Version: "v1"}: {
					{

						Name:         "testResources",
						SingularName: "testResource",
						Kind:         "TestResource",
						Namespaced:   true,
						Verbs:        metav1.Verbs{"get"},
					},
				},
			},
			crds: []v1.CustomResourceDefinition{
				{
					Status: v1.CustomResourceDefinitionStatus{
						AcceptedNames: v1.CustomResourceDefinitionNames{
							Plural:   "testResources",
							Singular: "testResource",
							Kind:     "TestResource",
						},
					},
					Spec: v1.CustomResourceDefinitionSpec{
						Group: "TestGroup",
						Versions: []v1.CustomResourceDefinitionVersion{
							{
								Name: "v1",
								AdditionalPrinterColumns: []v1.CustomResourceColumnDefinition{
									{
										Name:     "TestColumn",
										JSONPath: "TestPath",
										Type:     "TestType",
										Format:   "TestFormat",
									},
								},
								Schema: &v1.CustomResourceValidation{
									OpenAPIV3Schema: &v1.JSONSchemaProps{
										Description: "Test Resource for unit tests",
										Required:    []string{"required"},
										Properties: map[string]v1.JSONSchemaProps{
											"required": {
												Description: "Required Property",
												Type:        "string",
											},
											"numberField": {
												Description: "NumberField - Not Required Property",
												Type:        "number",
											},
											"nullArrayField": {
												Description: "ArrayField with no type - Not Required Property",
												Type:        "array",
											},
											"nullObjectField": {
												Description: "ObjectField with no type - Not Required Property",
												Type:        "object",
											},
											"actions": {
												Description: "Reserved field - Not Required Property",
												Type:        "string",
											},
										},
									},
								},
							},
						},
						Names: v1.CustomResourceDefinitionNames{
							Plural:   "testResources",
							Singular: "testResource",
							Kind:     "TestResource",
						},
					},
				},
			},
			wantError: false,
			desiredSchema: map[string]*types.APISchema{
				"testgroup.v1.testresource": {
					Schema: &wranglerSchema.Schema{
						ID:         "testgroup.v1.testresource",
						PluralName: "TestGroup.v1.testResources",
						Attributes: map[string]interface{}{
							"group":      "TestGroup",
							"version":    "v1",
							"kind":       "TestResource",
							"resource":   "testResources",
							"verbs":      []string{"get"},
							"namespaced": true,
							"columns": []table.Column{
								{
									Name:   "TestColumn",
									Field:  "TestPath",
									Type:   "TestType",
									Format: "TestFormat",
								},
							},
						},
						Description: "Test Resource for unit tests",
					},
				},
			},
		},
		{
			name:   "listed in discovery, not defined in crds",
			crds:   []v1.CustomResourceDefinition{},
			groups: []schema.GroupVersion{{Group: "TestGroup", Version: "v1"}},
			resources: map[schema.GroupVersion][]metav1.APIResource{
				{Group: "TestGroup", Version: "v1"}: {
					{

						Name:         "testResources",
						SingularName: "testResource",
						Kind:         "TestResource",
						Namespaced:   true,
						Verbs:        metav1.Verbs{"get"},
					},
				},
			},
			wantError: false,
			desiredSchema: map[string]*types.APISchema{
				"testgroup.v1.testresource": {
					Schema: &wranglerSchema.Schema{
						ID:         "testgroup.v1.testresource",
						PluralName: "TestGroup.v1.testResources",
						Attributes: map[string]interface{}{
							"group":      "TestGroup",
							"version":    "v1",
							"kind":       "TestResource",
							"resource":   "testResources",
							"verbs":      []string{"get"},
							"namespaced": true,
						},
					},
				},
			},
		},
		{
			name:      "defined in crds, but not in discovery",
			groups:    []schema.GroupVersion{},
			resources: map[schema.GroupVersion][]metav1.APIResource{},
			crds: []v1.CustomResourceDefinition{
				{
					Status: v1.CustomResourceDefinitionStatus{
						AcceptedNames: v1.CustomResourceDefinitionNames{
							Plural:   "testResources",
							Singular: "testResource",
							Kind:     "TestResource",
						},
					},
					Spec: v1.CustomResourceDefinitionSpec{
						Group: "TestGroup",
						Versions: []v1.CustomResourceDefinitionVersion{
							{
								Name: "v1",
								AdditionalPrinterColumns: []v1.CustomResourceColumnDefinition{
									{
										Name:     "TestColumn",
										JSONPath: "TestPath",
										Type:     "TestType",
										Format:   "TestFormat",
									},
								},
								Schema: &v1.CustomResourceValidation{
									OpenAPIV3Schema: &v1.JSONSchemaProps{
										Description: "Test Resource for unit tests",
										Required:    []string{"required"},
										Properties: map[string]v1.JSONSchemaProps{
											"required": {
												Description: "Required Property",
												Type:        "string",
											},
											"numberField": {
												Description: "NumberField - Not Required Property",
												Type:        "number",
											},
											"nullArrayField": {
												Description: "ArrayField with no type - Not Required Property",
												Type:        "array",
											},
											"nullObjectField": {
												Description: "ObjectField with no type - Not Required Property",
												Type:        "object",
											},
											"actions": {
												Description: "Reserved field - Not Required Property",
												Type:        "string",
											},
										},
									},
								},
							},
						},
						Names: v1.CustomResourceDefinitionNames{
							Plural:   "testResources",
							Singular: "testResource",
							Kind:     "TestResource",
						},
					},
				},
			},
			wantError:     false,
			desiredSchema: map[string]*types.APISchema{},
		},
		{
			name:          "discovery error",
			groups:        []schema.GroupVersion{},
			resources:     map[schema.GroupVersion][]metav1.APIResource{},
			discoveryErr:  fmt.Errorf("server is down, can't use discovery"),
			crds:          []v1.CustomResourceDefinition{},
			wantError:     true,
			desiredSchema: nil,
		},
		{
			name:   "crd error",
			groups: []schema.GroupVersion{{Group: "TestGroup", Version: "v1"}},
			resources: map[schema.GroupVersion][]metav1.APIResource{
				{Group: "TestGroup", Version: "v1"}: {
					{

						Name:         "testResources",
						SingularName: "testResource",
						Kind:         "TestResource",
						Namespaced:   true,
						Verbs:        metav1.Verbs{"get"},
					},
				},
			},
			crdErr:    fmt.Errorf("unable to use crd client, insufficient permissions"),
			crds:      []v1.CustomResourceDefinition{},
			wantError: false,
			desiredSchema: map[string]*types.APISchema{
				"testgroup.v1.testresource": {
					Schema: &wranglerSchema.Schema{
						ID:         "testgroup.v1.testresource",
						PluralName: "TestGroup.v1.testResources",
						Attributes: map[string]interface{}{
							"group":      "TestGroup",
							"version":    "v1",
							"kind":       "TestResource",
							"resource":   "testResources",
							"verbs":      []string{"get"},
							"namespaced": true,
						},
					},
				},
			},
		},
		{
			name:   "adding descriptions",
			groups: []schema.GroupVersion{{Group: "TestGroup", Version: "v1"}},
			resources: map[schema.GroupVersion][]metav1.APIResource{
				{Group: "TestGroup", Version: "v1"}: {
					{

						Name:         "testResources",
						SingularName: "testResource",
						Kind:         "TestResource",
						Namespaced:   true,
						Verbs:        metav1.Verbs{"get"},
					},
				},
			},
			crdErr: nil,
			crds:   []v1.CustomResourceDefinition{},
			document: &openapiv2.Document{
				Definitions: &openapiv2.Definitions{
					AdditionalProperties: []*openapiv2.NamedSchema{&gvkSchema},
				},
			},
			wantError: false,
			desiredSchema: map[string]*types.APISchema{
				"testgroup.v1.testresource": {
					Schema: &wranglerSchema.Schema{
						ID:          "testgroup.v1.testresource",
						Description: gvkSchema.Value.Description,
						PluralName:  "TestGroup.v1.testResources",
						Attributes: map[string]interface{}{
							"group":      "TestGroup",
							"version":    "v1",
							"kind":       "TestResource",
							"resource":   "testResources",
							"verbs":      []string{"get"},
							"namespaced": true,
						},
					},
				},
			},
		},
		{
			name:   "descriptions error",
			groups: []schema.GroupVersion{{Group: "TestGroup", Version: "v1"}},
			resources: map[schema.GroupVersion][]metav1.APIResource{
				{Group: "TestGroup", Version: "v1"}: {
					{

						Name:         "testResources",
						SingularName: "testResource",
						Kind:         "TestResource",
						Namespaced:   true,
						Verbs:        metav1.Verbs{"get"},
					},
				},
			},
			crdErr:      nil,
			crds:        []v1.CustomResourceDefinition{},
			document:    nil,
			documentErr: fmt.Errorf("can't get document"),
			wantError:   true,
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			ctrl := gomock.NewController(t)
			testDiscovery := fakeDiscovery{}
			for _, gvr := range test.groups {
				gvr := gvr
				testDiscovery.AddGroup(gvr.Group, gvr.Version, false)
			}
			testDiscovery.Document = test.document
			testDiscovery.DocumentErr = test.documentErr
			for gvr, resourceSlice := range test.resources {
				for _, resource := range resourceSlice {
					resource := resource
					testDiscovery.AddResource(gvr.Group, gvr.Version, resource)
				}
			}
			testDiscovery.GroupResourcesErr = test.discoveryErr
			var crds *v1.CustomResourceDefinitionList
			if test.crds != nil {
				crds = &v1.CustomResourceDefinitionList{
					Items: test.crds,
				}
			}
			fakeClient := fake.NewMockNonNamespacedClientInterface[*v1.CustomResourceDefinition, *v1.CustomResourceDefinitionList](ctrl)
			fakeClient.EXPECT().List(gomock.Any()).Return(crds, test.crdErr).AnyTimes()

			schemas, err := ToSchemas(fakeClient, &testDiscovery)
			if test.wantError {
				assert.Error(t, err, "wanted error but didn't get one")
			} else {
				assert.NoError(t, err, "got an error but did not want one")
			}
			assert.Equal(t, test.desiredSchema, schemas, "did not get the desired schemas")
		})
	}

}

func TestGVKToVersionedSchemaID(t *testing.T) {
	tests := []struct {
		name string
		gvk  schema.GroupVersionKind
		want string
	}{
		{
			name: "basic gvk",
			gvk: schema.GroupVersionKind{
				Group:   "TestGroup",
				Version: "v1",
				Kind:    "TestKind",
			},
			want: "testgroup.v1.testkind",
		},
		{
			name: "core resource",
			gvk: schema.GroupVersionKind{
				Group:   "",
				Version: "v1",
				Kind:    "TestKind",
			},
			want: "core.v1.testkind",
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			require.Equal(t, test.want, GVKToVersionedSchemaID(test.gvk))
		})
	}

}

func TestGVKToSchemaID(t *testing.T) {
	tests := []struct {
		name string
		gvk  schema.GroupVersionKind
		want string
	}{
		{
			name: "basic gvk",
			gvk: schema.GroupVersionKind{
				Group:   "TestGroup",
				Version: "v1",
				Kind:    "TestKind",
			},
			want: "testgroup.testkind",
		},
		{
			name: "core resource",
			gvk: schema.GroupVersionKind{
				Group:   "",
				Version: "v1",
				Kind:    "TestKind",
			},
			want: "testkind",
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			require.Equal(t, test.want, GVKToSchemaID(test.gvk))
		})
	}
}

func TestGVRToPluralName(t *testing.T) {
	tests := []struct {
		name string
		gvr  schema.GroupVersionResource
		want string
	}{
		{
			name: "basic gvk",
			gvr: schema.GroupVersionResource{
				Group:    "TestGroup",
				Version:  "v1",
				Resource: "TestResources",
			},
			want: "TestGroup.TestResources",
		},
		{
			name: "core resource",
			gvr: schema.GroupVersionResource{
				Group:    "",
				Version:  "v1",
				Resource: "TestResources",
			},
			want: "TestResources",
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			require.Equal(t, test.want, GVRToPluralName(test.gvr))
		})
	}
}

func TestGetGVKForKind(t *testing.T) {
	tests := []struct {
		name    string
		kind    *proto.Kind
		wantGVK *schema.GroupVersionKind
	}{
		{
			name: "basic kind",
			kind: &proto.Kind{
				BaseSchema: proto.BaseSchema{
					Extensions: map[string]any{
						gvkExtensionName: []any{
							"some other extension",
							map[any]any{
								gvkExtensionGroup:   "TestGroup",
								gvkExtensionVersion: "v1",
								gvkExtensionKind:    "TestKind",
							},
						},
					},
				},
			},
			wantGVK: &schema.GroupVersionKind{
				Group:   "TestGroup",
				Version: "v1",
				Kind:    "TestKind",
			},
		},
		{
			name: "kind missing gvkExtension",
			kind: &proto.Kind{
				BaseSchema: proto.BaseSchema{
					Extensions: map[string]any{},
				},
			},
			wantGVK: nil,
		},
		{
			name: "kind missing gvk map",
			kind: &proto.Kind{
				BaseSchema: proto.BaseSchema{
					Extensions: map[string]any{
						gvkExtensionName: []any{"some value"},
					},
				},
			},
			wantGVK: nil,
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, test.wantGVK, GetGVKForKind(test.kind))
		})
	}

}