/*
Copyright 2023 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 openapi3

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/openapi"
	"k8s.io/client-go/openapi/openapitest"
)

func TestOpenAPIV3Root_GroupVersions(t *testing.T) {
	tests := []struct {
		name        string
		paths       map[string]openapi.GroupVersion
		expectedGVs []schema.GroupVersion
		forcedErr   error
	}{
		{
			name:        "OpenAPI V3 Root: No openapi.Paths() equals no GroupVersions.",
			expectedGVs: []schema.GroupVersion{},
		},
		{
			name: "OpenAPI V3 Root: Single openapi.Path equals one GroupVersion.",
			paths: map[string]openapi.GroupVersion{
				"apis/apps/v1": nil,
			},
			expectedGVs: []schema.GroupVersion{
				{Group: "apps", Version: "v1"},
			},
		},
		{
			name: "OpenAPI V3 Root: Multiple openapi.Paths equals multiple GroupVersions.",
			paths: map[string]openapi.GroupVersion{
				"apis/apps/v1":       nil,
				"api/v1":             nil,
				"apis/batch/v1beta1": nil,
			},
			// Alphabetical ordering, since GV's are returned sorted.
			expectedGVs: []schema.GroupVersion{
				{Group: "apps", Version: "v1"},
				{Group: "batch", Version: "v1beta1"},
				{Group: "", Version: "v1"},
			},
		},
		{
			name: "Multiple GroupVersions, some invalid",
			paths: map[string]openapi.GroupVersion{
				"apis/batch/v1beta1":              nil,
				"api/v1":                          nil,
				"foo/apps/v1":                     nil, // bad prefix
				"apis/networking.k8s.io/v1alpha1": nil,
				"api":                             nil, // No version
				"apis/apps":                       nil, // Missing Version
				"apis/apps/v1":                    nil,
			},
			// Alphabetical ordering, since GV's are returned sorted.
			expectedGVs: []schema.GroupVersion{
				{Group: "apps", Version: "v1"},
				{Group: "batch", Version: "v1beta1"},
				{Group: "networking.k8s.io", Version: "v1alpha1"},
				{Group: "", Version: "v1"},
			},
		},
		{
			name:      "OpenAPI V3 Root: Forced error returns error.",
			forcedErr: fmt.Errorf("openapi client error"),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			fakeClient := openapitest.FakeClient{
				PathsMap:  test.paths,
				ForcedErr: test.forcedErr,
			}
			root := NewRoot(fakeClient)
			actualGVs, err := root.GroupVersions()
			if test.forcedErr != nil {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
			if !reflect.DeepEqual(test.expectedGVs, actualGVs) {
				t.Errorf("expected GroupVersions (%s), got (%s): (%s)\n",
					test.expectedGVs, actualGVs, err)
			}
		})
	}
}

func TestOpenAPIV3Root_GVSpec(t *testing.T) {
	tests := []struct {
		name          string
		gv            schema.GroupVersion
		expectedPaths []string
		err           error
	}{
		{
			name: "OpenAPI V3 for apps/v1 works",
			gv:   schema.GroupVersion{Group: "apps", Version: "v1"},
			expectedPaths: []string{
				"/apis/apps/v1/",
				"/apis/apps/v1/deployments",
				"/apis/apps/v1/replicasets",
				"/apis/apps/v1/daemonsets",
			},
		},
		{
			name: "OpenAPI V3 for networking/v1alpha1 works",
			gv:   schema.GroupVersion{Group: "networking.k8s.io", Version: "v1alpha1"},
			expectedPaths: []string{
				"/apis/networking.k8s.io/v1alpha1/",
			},
		},
		{
			name: "OpenAPI V3 for batch/v1 works",
			gv:   schema.GroupVersion{Group: "batch", Version: "v1"},
			expectedPaths: []string{
				"/apis/batch/v1/",
				"/apis/batch/v1/jobs",
				"/apis/batch/v1/cronjobs",
			},
		},
		{
			name: "OpenAPI V3 spec not found",
			gv:   schema.GroupVersion{Group: "not", Version: "found"},
			err:  &GroupVersionNotFoundError{gv: schema.GroupVersion{Group: "not", Version: "found"}},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			client := openapitest.NewFileClient(t)
			root := NewRoot(client)
			gvSpec, err := root.GVSpec(test.gv)
			if test.err != nil {
				assert.True(t, reflect.DeepEqual(test.err, err))
				return
			}
			require.NoError(t, err)
			for _, path := range test.expectedPaths {
				if _, found := gvSpec.Paths.Paths[path]; !found {
					assert.True(t, found, "expected path not found (%s)\n", path)
				}
			}
		})
	}
}

func TestOpenAPIV3Root_GVSpecAsMap(t *testing.T) {
	tests := []struct {
		name          string
		gv            schema.GroupVersion
		expectedPaths []string
		err           error
	}{
		{
			name: "OpenAPI V3 for apps/v1 works",
			gv:   schema.GroupVersion{Group: "apps", Version: "v1"},
			expectedPaths: []string{
				"/apis/apps/v1/",
				"/apis/apps/v1/deployments",
				"/apis/apps/v1/replicasets",
				"/apis/apps/v1/daemonsets",
			},
		},
		{
			name: "OpenAPI V3 for networking/v1alpha1 works",
			gv:   schema.GroupVersion{Group: "networking.k8s.io", Version: "v1alpha1"},
			expectedPaths: []string{
				"/apis/networking.k8s.io/v1alpha1/",
			},
		},
		{
			name: "OpenAPI V3 for batch/v1 works",
			gv:   schema.GroupVersion{Group: "batch", Version: "v1"},
			expectedPaths: []string{
				"/apis/batch/v1/",
				"/apis/batch/v1/jobs",
				"/apis/batch/v1/cronjobs",
			},
		},
		{
			name: "OpenAPI V3 spec not found",
			gv:   schema.GroupVersion{Group: "not", Version: "found"},
			err:  &GroupVersionNotFoundError{gv: schema.GroupVersion{Group: "not", Version: "found"}},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			client := openapitest.NewFileClient(t)
			root := NewRoot(client)
			gvSpecAsMap, err := root.GVSpecAsMap(test.gv)
			if test.err != nil {
				assert.True(t, reflect.DeepEqual(test.err, err))
				return
			}
			require.NoError(t, err)
			for _, path := range test.expectedPaths {
				pathsMap := gvSpecAsMap["paths"]
				if _, found := pathsMap.(map[string]interface{})[path]; !found {
					assert.True(t, found, "expected path not found (%s)\n", path)
				}
			}
		})
	}
}

func TestOpenAPIV3Root_GroupVersionToPath(t *testing.T) {
	tests := []struct {
		name         string
		groupVersion schema.GroupVersion
		expectedPath string
	}{
		{
			name: "OpenAPI V3 Root: Path to GroupVersion apps group",
			groupVersion: schema.GroupVersion{
				Group:   "apps",
				Version: "v1",
			},
			expectedPath: "apis/apps/v1",
		},
		{
			name: "OpenAPI V3 Root: Path to GroupVersion batch group",
			groupVersion: schema.GroupVersion{
				Group:   "batch",
				Version: "v1beta1",
			},
			expectedPath: "apis/batch/v1beta1",
		},
		{
			name: "OpenAPI V3 Root: Path to GroupVersion core group",
			groupVersion: schema.GroupVersion{
				Version: "v1",
			},
			expectedPath: "api/v1",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			actualPath := gvToAPIPath(test.groupVersion)
			assert.Equal(t, test.expectedPath, actualPath, "expected API path (%s), got (%s)",
				test.expectedPath, actualPath)
		})
	}
}

func TestOpenAPIV3Root_PathToGroupVersion(t *testing.T) {
	tests := []struct {
		name        string
		path        string
		expectedGV  schema.GroupVersion
		expectedErr bool
	}{
		{
			name: "OpenAPI V3 Root: Path to GroupVersion apps/v1 group",
			path: "apis/apps/v1",
			expectedGV: schema.GroupVersion{
				Group:   "apps",
				Version: "v1",
			},
		},
		{
			name:        "Group without Version throws error",
			path:        "apis/apps",
			expectedErr: true,
		},
		{
			name: "OpenAPI V3 Root: Path to GroupVersion batch group",
			path: "apis/batch/v1beta1",
			expectedGV: schema.GroupVersion{
				Group:   "batch",
				Version: "v1beta1",
			},
		},
		{
			name: "OpenAPI V3 Root: Path to GroupVersion core group",
			path: "api/v1",
			expectedGV: schema.GroupVersion{
				Version: "v1",
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			actualGV, err := pathToGroupVersion(test.path)
			if test.expectedErr {
				require.Error(t, err, "should have received error for path: %s", test.path)
			} else {
				require.NoError(t, err, "expected no error, got (%v)", err)
				assert.Equal(t, test.expectedGV, actualGV, "expected GroupVersion (%s), got (%s)",
					test.expectedGV, actualGV)
			}
		})
	}
}
