// Copyright © 2019 The Knative 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 dynamic

import (
	"context"
	"strings"
	"testing"

	"gotest.tools/v3/assert"

	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"

	eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1"
	"knative.dev/eventing/pkg/apis/messaging"
	messagingv1 "knative.dev/eventing/pkg/apis/messaging/v1"
	sourcesv1 "knative.dev/eventing/pkg/apis/sources/v1"
	sourcesv1beta2 "knative.dev/eventing/pkg/apis/sources/v1beta2"
	dynamicclientfake "knative.dev/pkg/injection/clients/dynamicclient/fake"
	servingv1 "knative.dev/serving/pkg/apis/serving/v1"

	"knative.dev/client/pkg/util"
)

const testNamespace = "current"

func TestNamespace(t *testing.T) {
	client := createFakeKnDynamicClient(testNamespace, newSourceCRDObj("foo"))
	assert.Equal(t, client.Namespace(), testNamespace)
}

func TestListCRDs(t *testing.T) {
	client := createFakeKnDynamicClient(
		testNamespace,
		newSourceCRDObj("foo"),
		newSourceCRDObj("bar"),
	)
	assert.Check(t, client.RawClient() != nil)

	t.Run("List CRDs with match", func(t *testing.T) {
		options := metav1.ListOptions{}
		uList, err := client.ListCRDs(context.Background(), options)
		assert.NilError(t, err)
		assert.Equal(t, len(uList.Items), 2)
	})

	t.Run("List CRDs without match", func(t *testing.T) {
		options := metav1.ListOptions{}
		sourcesLabels := labels.Set{"duck.knative.dev/source": "true1"}
		options.LabelSelector = sourcesLabels.String()
		uList, err := client.ListCRDs(context.Background(), options)
		if err != nil {
			t.Fatal(err)
		}

		assert.Equal(t, len(uList.Items), 0)
	})
}

func TestListSourceTypes(t *testing.T) {
	client := createFakeKnDynamicClient(
		testNamespace,
		newSourceCRDObj("foo"),
		newSourceCRDObj("bar"),
	)

	t.Run("List source types", func(t *testing.T) {
		uList, err := client.ListSourcesTypes(context.Background())
		if err != nil {
			t.Fatal(err)
		}

		assert.Equal(t, len(uList.Items), 2)
		// List of objects is returned in sorted order according to the (ns first, then name)
		assert.Equal(t, uList.Items[0].GetName(), "bar")
		assert.Equal(t, uList.Items[1].GetName(), "foo")
	})
}

func TestListSources(t *testing.T) {
	t.Run("No GVRs set", func(t *testing.T) {
		obj := newSourceCRDObj("foo")
		client := createFakeKnDynamicClient(testNamespace, obj)
		assert.Check(t, client.RawClient() != nil)
		_, err := client.ListSources(context.Background())
		assert.Check(t, err != nil)
		assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "source", "kind", "CRD"))
	})

	t.Run("sources not installed", func(t *testing.T) {
		client := createFakeKnDynamicClient(testNamespace)
		_, err := client.ListSources(context.Background())
		assert.Check(t, err != nil)
		assert.Check(t, util.ContainsAll(err.Error(), "no sources", "found", "backend", "verify", "installation"))
	})

	t.Run("source list empty", func(t *testing.T) {
		client := createFakeKnDynamicClient(testNamespace,
			newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1beta2", "PingSource"),
		)
		sources, err := client.ListSources(context.Background())
		assert.NilError(t, err)
		assert.Equal(t, len(sources.Items), 0)
	})

	t.Run("source list non empty", func(t *testing.T) {
		client := createFakeKnDynamicClient(testNamespace,
			newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1beta2", "PingSource"),
			newSourceCRDObjWithSpec("apiserversources", "sources.knative.dev", "v1", "ApiServerSource"),
			newSourceUnstructuredObj("p1", "sources.knative.dev/v1beta2", "PingSource"),
			newSourceUnstructuredObj("a1", "sources.knative.dev/v1", "ApiServerSource"),
		)
		sources, err := client.ListSources(context.Background(), WithTypeFilter("pingsource"), WithTypeFilter("ApiServerSource"))
		assert.NilError(t, err)
		assert.Equal(t, len(sources.Items), 2)
		assert.DeepEqual(t, sources.GroupVersionKind(), schema.GroupVersionKind{Group: sourceListGroup, Version: sourceListVersion, Kind: sourceListKind})
	})
}

func TestListSourcesUsingGVKs(t *testing.T) {
	t.Run("No GVKs given", func(t *testing.T) {
		client := createFakeKnDynamicClient(testNamespace)
		assert.Check(t, client.RawClient() != nil)
		s, err := client.ListSourcesUsingGVKs(context.Background(), nil)
		assert.NilError(t, err)
		assert.Check(t, s == nil)
	})

	t.Run("source list with given GVKs", func(t *testing.T) {
		client := createFakeKnDynamicClient(testNamespace,
			newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1beta2", "PingSource"),
			newSourceCRDObjWithSpec("apiserversources", "sources.knative.dev", "v1", "ApiServerSource"),
			newSourceUnstructuredObj("p1", "sources.knative.dev/v1beta2", "PingSource"),
			newSourceUnstructuredObj("a1", "sources.knative.dev/v1", "ApiServerSource"),
		)
		assert.Check(t, client.RawClient() != nil)
		gvks := []schema.GroupVersionKind{
			{Group: "sources.knative.dev", Version: "v1beta2", Kind: "PingSource"},
			{Group: "sources.knative.dev", Version: "v1", Kind: "ApiServerSource"},
		}

		s, err := client.ListSourcesUsingGVKs(context.Background(), &gvks)
		assert.NilError(t, err)
		if s == nil {
			t.Fatal("s = nil, want not nil")
		}
		assert.Equal(t, len(s.Items), 2)
		assert.DeepEqual(t, s.GroupVersionKind(), schema.GroupVersionKind{Group: sourceListGroup, Version: sourceListVersion, Kind: sourceListKind})

		// withType
		s, err = client.ListSourcesUsingGVKs(context.Background(), &gvks, WithTypeFilter("PingSource"))
		assert.NilError(t, err)
		if s == nil {
			t.Fatal("s = nil, want not nil")
		}
		assert.Equal(t, len(s.Items), 1)
		assert.DeepEqual(t, s.GroupVersionKind(), schema.GroupVersionKind{Group: sourceListGroup, Version: sourceListVersion, Kind: sourceListKind})
	})

}

// createFakeKnDynamicClient gives you a dynamic client for testing containing the given objects.
// See also the one in the fake package. Duplicated here to avoid a dependency loop.
func createFakeKnDynamicClient(testNamespace string, objects ...runtime.Object) KnDynamicClient {
	scheme := runtime.NewScheme()
	servingv1.AddToScheme(scheme)
	eventingv1.AddToScheme(scheme)
	messagingv1.AddToScheme(scheme)
	sourcesv1.AddToScheme(scheme)
	sourcesv1beta2.AddToScheme(scheme)
	apiextensionsv1.AddToScheme(scheme)
	_, dynamicClient := dynamicclientfake.With(context.TODO(), scheme, objects...)
	return NewKnDynamicClient(dynamicClient, testNamespace)
}

func newSourceCRDObj(name string) *unstructured.Unstructured {
	obj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": crdGroup + "/" + crdVersion,
			"kind":       crdKind,
			"metadata": map[string]interface{}{
				"namespace": testNamespace,
				"name":      name,
			},
		},
	}
	obj.SetLabels(labels.Set{sourcesLabelKey: sourcesLabelValue})
	return obj
}

func newSourceCRDObjWithSpec(name, group, version, kind string) *unstructured.Unstructured {
	obj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": crdGroup + "/" + crdVersion,
			"kind":       crdKind,
			"metadata": map[string]interface{}{
				"namespace": testNamespace,
				"name":      name,
			},
		},
	}

	obj.Object["spec"] = map[string]interface{}{
		"group":   group,
		"version": version,
		"names": map[string]interface{}{
			"kind":   kind,
			"plural": strings.ToLower(kind) + "s",
		},
	}
	obj.SetLabels(labels.Set{sourcesLabelKey: sourcesLabelValue})
	return obj
}

func newSourceUnstructuredObj(name, apiVersion, kind string) *unstructured.Unstructured {
	return &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": apiVersion,
			"kind":       kind,
			"metadata": map[string]interface{}{
				"namespace": "current",
				"name":      name,
			},
			"spec": map[string]interface{}{
				"sink": map[string]interface{}{
					"ref": map[string]interface{}{
						"kind": "Service",
						"name": "foo",
					},
				},
			},
		},
	}
}

func newChannelCRDObj(name string) *unstructured.Unstructured {
	obj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": crdGroup + "/" + crdVersion,
			"kind":       crdKind,
			"metadata": map[string]interface{}{
				"namespace": testNamespace,
				"name":      name,
			},
		},
	}
	obj.SetLabels(labels.Set{messaging.SubscribableDuckVersionAnnotation: channelLabelValue})
	return obj
}

func newChannelCRDObjWithSpec(name, group, version, kind string) *unstructured.Unstructured {
	obj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": crdGroup + "/" + crdVersion,
			"kind":       crdKind,
			"metadata": map[string]interface{}{
				"namespace": testNamespace,
				"name":      name,
			},
		},
	}

	obj.Object["spec"] = map[string]interface{}{
		"group":   group,
		"version": version,
		"names": map[string]interface{}{
			"kind":   kind,
			"plural": strings.ToLower(kind) + "s",
		},
	}
	obj.SetLabels(labels.Set{messaging.SubscribableDuckVersionAnnotation: channelLabelValue})
	return obj
}

func newChannelUnstructuredObj(name, apiVersion, kind string) *unstructured.Unstructured {
	return &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": apiVersion,
			"kind":       kind,
			"metadata": map[string]interface{}{
				"namespace": "current",
				"name":      name,
			},
			"spec": map[string]interface{}{
				"sink": map[string]interface{}{
					"ref": map[string]interface{}{
						"name": "foo",
					},
				},
			},
		},
	}
}
func TestListChannelsTypes(t *testing.T) {
	t.Run("List channel types", func(t *testing.T) {
		client := createFakeKnDynamicClient(
			testNamespace,
			newChannelCRDObjWithSpec("Channel", "messaging.knative.dev", "v1", "Channel"),
			newChannelCRDObjWithSpec("InMemoryChannel", "messaging.knative.dev", "v1", "InMemoryChannel"),
		)

		uList, err := client.ListChannelsTypes(context.Background())
		if err != nil {
			t.Fatal(err)
		}
		assert.Equal(t, len(uList.Items), 1)
		assert.Equal(t, uList.Items[0].GetName(), "InMemoryChannel")
	})

	t.Run("List channel types error", func(t *testing.T) {
		client := createFakeKnDynamicClient(
			testNamespace,
			newChannelCRDObj("foo"),
		)
		uList, err := client.ListChannelsTypes(context.Background())
		assert.Check(t, err == nil)
		if err != nil {
			t.Fatal(err)
		}
		assert.Equal(t, len(uList.Items), 1)
		assert.Equal(t, uList.Items[0].GetName(), "foo")
	})
}

func TestListChannelsUsingGVKs(t *testing.T) {
	t.Run("No GVKs given", func(t *testing.T) {
		client := createFakeKnDynamicClient(testNamespace)
		assert.Check(t, client.RawClient() != nil)
		s, err := client.ListChannelsUsingGVKs(context.Background(), nil)
		assert.NilError(t, err)
		assert.Check(t, s == nil)
	})

	t.Run("channel list with given GVKs", func(t *testing.T) {
		client := createFakeKnDynamicClient(testNamespace,
			newChannelCRDObjWithSpec("InMemoryChannel", "messaging.knative.dev", "v1", "InMemoryChannel"),
			newChannelUnstructuredObj("i1", "messaging.knative.dev/v1", "InMemoryChannel"),
		)
		assert.Check(t, client.RawClient() != nil)
		gv := schema.GroupVersion{Group: "messaging.knative.dev", Version: "v1"}
		gvks := []schema.GroupVersionKind{gv.WithKind("InMemoryChannel")}

		s, err := client.ListChannelsUsingGVKs(context.Background(), &gvks)
		assert.NilError(t, err)
		if s == nil {
			t.Fatal("s = nil, want not nil")
		}
		assert.Equal(t, len(s.Items), 1)
		assert.DeepEqual(t, s.GroupVersionKind(), schema.GroupVersionKind{Group: messaging.GroupName, Version: channelListVersion, Kind: channelListKind})

		// withType
		s, err = client.ListChannelsUsingGVKs(context.Background(), &gvks, WithTypeFilter("InMemoryChannel"))
		assert.NilError(t, err)
		if s == nil {
			t.Fatal("s = nil, want not nil")
		}
		assert.Equal(t, len(s.Items), 1)
		assert.DeepEqual(t, s.GroupVersionKind(), schema.GroupVersionKind{Group: messaging.GroupName, Version: channelListVersion, Kind: channelListKind})
	})

}
