//
// Copyright (c) 2019-2023 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//
// Contributors:
//   Red Hat, Inc. - initial API and implementation
//

package devworkspace

import (
	"context"
	"os"
	"testing"
	"time"

	defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults"

	dwo "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
	"github.com/devfile/devworkspace-operator/pkg/infrastructure"
	chev2 "github.com/eclipse-che/che-operator/api/v2"
	devworkspacedefaults "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults"
	"github.com/eclipse-che/che-operator/controllers/devworkspace/sync"

	routev1 "github.com/openshift/api/route/v1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	"k8s.io/api/node/v1alpha1"
	rbac "k8s.io/api/rbac/v1"

	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func createTestScheme() *runtime.Scheme {
	infrastructure.InitializeForTesting(infrastructure.Kubernetes)

	scheme := runtime.NewScheme()
	utilruntime.Must(v1alpha1.AddToScheme(scheme))
	utilruntime.Must(networkingv1.AddToScheme(scheme))
	utilruntime.Must(corev1.AddToScheme(scheme))
	utilruntime.Must(appsv1.AddToScheme(scheme))
	utilruntime.Must(rbac.AddToScheme(scheme))
	utilruntime.Must(routev1.AddToScheme(scheme))
	utilruntime.Must(chev2.AddToScheme(scheme))
	utilruntime.Must(dwo.AddToScheme(scheme))

	return scheme
}

func TestNoCustomResourceSharedWhenReconcilingNonExistent(t *testing.T) {
	infrastructure.InitializeForTesting(infrastructure.Kubernetes)

	// clear the map before the test
	for k := range currentCheInstances {
		delete(currentCheInstances, k)
	}

	managerName := "che"
	ns := "default"
	scheme := createTestScheme()
	cl := fake.NewFakeClientWithScheme(scheme)

	ctx := context.TODO()

	reconciler := CheClusterReconciler{client: cl, scheme: scheme, syncer: sync.New(cl, scheme)}

	_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	// there is nothing in our context, so the map should still be empty
	managers := GetCurrentCheClusterInstances()
	if len(managers) != 0 {
		t.Fatalf("There should have been no managers after a reconcile of a non-existent manager.")
	}

	// now add some manager and reconcile a non-existent one
	cl.Create(ctx, &chev2.CheCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:       managerName + "-not-me",
			Namespace:  ns,
			Finalizers: []string{FinalizerName},
		},
		Spec: chev2.CheClusterSpec{
			Networking: chev2.CheClusterSpecNetworking{
				Hostname: "over.the.rainbow",
			},
		},
		Status: chev2.CheClusterStatus{
			WorkspaceBaseDomain: "down.on.earth",
		},
	})

	_, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	managers = GetCurrentCheClusterInstances()
	if len(managers) != 0 {
		t.Fatalf("There should have been no managers after a reconcile of a non-existent manager.")
	}
}

func TestAddsCustomResourceToSharedMapOnCreate(t *testing.T) {
	infrastructure.InitializeForTesting(infrastructure.Kubernetes)

	// clear the map before the test
	for k := range currentCheInstances {
		delete(currentCheInstances, k)
	}

	managerName := "che"
	ns := "default"
	scheme := createTestScheme()
	cl := fake.NewFakeClientWithScheme(scheme, &chev2.CheCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:       managerName,
			Namespace:  ns,
			Finalizers: []string{FinalizerName},
		},
		Spec: chev2.CheClusterSpec{
			Networking: chev2.CheClusterSpecNetworking{
				Hostname: "over.the.rainbow",
				Domain:   "down.on.earth",
			},
		},
	})

	reconciler := CheClusterReconciler{client: cl, scheme: scheme, syncer: sync.New(cl, scheme)}

	_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	managers := GetCurrentCheClusterInstances()
	if len(managers) != 1 {
		t.Fatalf("There should have been exactly 1 manager after a reconcile but there is %d.", len(managers))
	}

	mgr, ok := managers[types.NamespacedName{Name: managerName, Namespace: ns}]
	if !ok {
		t.Fatalf("The map of the current managers doesn't contain the expected one.")
	}

	if mgr.Name != managerName {
		t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName)
	}
}

func TestUpdatesCustomResourceInSharedMapOnUpdate(t *testing.T) {
	infrastructure.InitializeForTesting(infrastructure.Kubernetes)

	// clear the map before the test
	for k := range currentCheInstances {
		delete(currentCheInstances, k)
	}

	managerName := "che"
	ns := "default"
	scheme := createTestScheme()

	cl := fake.NewFakeClientWithScheme(scheme, &chev2.CheCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:       managerName,
			Namespace:  ns,
			Finalizers: []string{FinalizerName},
		},
		Spec: chev2.CheClusterSpec{
			Networking: chev2.CheClusterSpecNetworking{
				Hostname: "over.the.rainbow",
				Domain:   "down.on.earth",
			},
		},
	})

	reconciler := CheClusterReconciler{client: cl, scheme: scheme, syncer: sync.New(cl, scheme)}

	_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	managers := GetCurrentCheClusterInstances()
	if len(managers) != 1 {
		t.Fatalf("There should have been exactly 1 manager after a reconcile but there is %d.", len(managers))
	}

	mgr, ok := managers[types.NamespacedName{Name: managerName, Namespace: ns}]
	if !ok {
		t.Fatalf("The map of the current managers doesn't contain the expected one.")
	}

	if mgr.Name != managerName {
		t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName)
	}

	if mgr.GetCheHost() != "over.the.rainbow" {
		t.Fatalf("Unexpected host value: expected: over.the.rainbow, actual: %s", mgr.GetCheHost())
	}

	// now update the manager and reconcile again. See that the map contains the updated value
	mgrInCluster := chev2.CheCluster{}
	cl.Get(context.TODO(), client.ObjectKey{Name: managerName, Namespace: ns}, &mgrInCluster)

	// to be able to update, we need to set the resource version
	mgr.SetResourceVersion(mgrInCluster.GetResourceVersion())

	mgr.Spec.Networking.Hostname = "over.the.shoulder"
	err = cl.Update(context.TODO(), &mgr)
	if err != nil {
		t.Fatalf("Failed to update. Wat? %s", err)
	}

	// before the reconcile, the map still should containe the old value
	managers = GetCurrentCheClusterInstances()
	mgr, ok = managers[types.NamespacedName{Name: managerName, Namespace: ns}]
	if !ok {
		t.Fatalf("The map of the current managers doesn't contain the expected one.")
	}

	if mgr.Name != managerName {
		t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName)
	}

	if mgr.Spec.Networking.Hostname != "over.the.rainbow" {
		t.Fatalf("Unexpected host value: expected: over.the.rainbow, actual: %s", mgr.Spec.Networking.Hostname)
	}

	// now reconcile and see that the value in the map is now updated

	_, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	managers = GetCurrentCheClusterInstances()
	mgr, ok = managers[types.NamespacedName{Name: managerName, Namespace: ns}]
	if !ok {
		t.Fatalf("The map of the current managers doesn't contain the expected one.")
	}

	if mgr.Name != managerName {
		t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName)
	}

	if mgr.Spec.Networking.Hostname != "over.the.shoulder" {
		t.Fatalf("Unexpected host value: expected: over.the.shoulder, actual: %s", mgr.Spec.Networking.Hostname)
	}
}

func TestRemovesCustomResourceFromSharedMapOnDelete(t *testing.T) {
	infrastructure.InitializeForTesting(infrastructure.Kubernetes)

	// clear the map before the test
	for k := range currentCheInstances {
		delete(currentCheInstances, k)
	}

	managerName := "che"
	ns := "default"
	scheme := createTestScheme()

	cl := fake.NewFakeClientWithScheme(scheme, &chev2.CheCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:       managerName,
			Namespace:  ns,
			Finalizers: []string{FinalizerName},
		},
		Spec: chev2.CheClusterSpec{
			Networking: chev2.CheClusterSpecNetworking{
				Hostname: "over.the.rainbow",
				Domain:   "down.on.earth",
			},
		},
	})

	reconciler := CheClusterReconciler{client: cl, scheme: scheme, syncer: sync.New(cl, scheme)}

	_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	managers := GetCurrentCheClusterInstances()
	if len(managers) != 1 {
		t.Fatalf("There should have been exactly 1 manager after a reconcile but there is %d.", len(managers))
	}

	mgr, ok := managers[types.NamespacedName{Name: managerName, Namespace: ns}]
	if !ok {
		t.Fatalf("The map of the current managers doesn't contain the expected one.")
	}

	if mgr.Name != managerName {
		t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName)
	}

	cl.Delete(context.TODO(), &mgr)

	// now reconcile and see that the value is no longer in the map

	_, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	managers = GetCurrentCheClusterInstances()
	_, ok = managers[types.NamespacedName{Name: managerName, Namespace: ns}]
	if ok {
		t.Fatalf("The map of the current managers should no longer contain the manager after it has been deleted.")
	}
}

func TestCustomResourceFinalization(t *testing.T) {
	infrastructure.InitializeForTesting(infrastructure.Kubernetes)

	managerName := "che"
	ns := "default"
	scheme := createTestScheme()
	ctx := context.TODO()
	cl := fake.NewFakeClientWithScheme(scheme,
		&chev2.CheCluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:       managerName,
				Namespace:  ns,
				Finalizers: []string{FinalizerName},
			},
			Spec: chev2.CheClusterSpec{
				Networking: chev2.CheClusterSpecNetworking{
					Hostname: "over.the.rainbow",
					Domain:   "down.on.earth",
				},
			},
		},
		&corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "ws1",
				Namespace: ns,
				Annotations: map[string]string{
					devworkspacedefaults.ConfigAnnotationCheManagerName:      managerName,
					devworkspacedefaults.ConfigAnnotationCheManagerNamespace: ns,
				},
				Labels: devworkspacedefaults.GetLabelsFromNames(managerName, "gateway-config"),
			},
		})

	reconciler := CheClusterReconciler{client: cl, scheme: scheme, syncer: sync.New(cl, scheme)}

	_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	// check that the reconcile loop added the finalizer
	manager := chev2.CheCluster{}
	err = cl.Get(ctx, client.ObjectKey{Name: managerName, Namespace: ns}, &manager)
	if err != nil {
		t.Fatalf("Failed to obtain the manager from the fake client: %s", err)
	}

	if len(manager.Finalizers) != 1 {
		t.Fatalf("Expected a single finalizer on the manager but found: %d", len(manager.Finalizers))
	}

	if manager.Finalizers[0] != FinalizerName {
		t.Fatalf("Expected a finalizer called %s but got %s", FinalizerName, manager.Finalizers[0])
	}

	// try to delete the manager and check that the configmap disallows that and that the status of the manager is updated
	manager.DeletionTimestamp = &metav1.Time{Time: time.Now()}
	err = cl.Update(ctx, &manager)
	if err != nil {
		t.Fatalf("Failed to update the manager in the fake client: %s", err)
	}
	_, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	manager = chev2.CheCluster{}
	err = cl.Get(ctx, client.ObjectKey{Name: managerName, Namespace: ns}, &manager)
	if err != nil {
		t.Fatalf("Failed to obtain the manager from the fake client: %s", err)
	}

	if len(manager.Finalizers) != 1 {
		t.Fatalf("There should have been a finalizer on the manager after a failed finalization attempt")
	}

	if manager.Status.ChePhase != chev2.ClusterPhasePendingDeletion {
		t.Fatalf("Expected the manager to be in the pending deletion phase but it is: %s", manager.Status.ChePhase)
	}
	if len(manager.Status.Message) == 0 {
		t.Fatalf("Expected an non-empty message about the failed finalization in the manager status")
	}

	// now remove the config map and check that the finalization proceeds
	err = cl.Delete(ctx, &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "ws1",
			Namespace: ns,
		},
	})
	if err != nil {
		t.Fatalf("Failed to delete the test configmap: %s", err)
	}

	_, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}})
	if err != nil {
		t.Fatalf("Failed to reconcile che manager with error: %s", err)
	}

	manager = chev2.CheCluster{}
	err = cl.Get(ctx, client.ObjectKey{Name: managerName, Namespace: ns}, &manager)
	if err == nil || !k8sErrors.IsNotFound(err) {
		t.Fatalf("Failed to obtain the manager from the fake client: %s", err)
	}
}

// This test should be removed if we are again in charge of gateway creation.
func TestExternalGatewayDetection(t *testing.T) {
	origFlavor := defaults.GetCheFlavor()
	t.Cleanup(func() {
		os.Setenv("CHE_FLAVOR", origFlavor)
	})

	os.Setenv("CHE_FLAVOR", "test-che")

	scheme := createTestScheme()

	clusterName := "eclipse-che"
	ns := "default"

	cluster := &chev2.CheCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      clusterName,
			Namespace: ns,
		},
		Status: chev2.CheClusterStatus{
			WorkspaceBaseDomain: "down.on.earth",
			CheURL:              "https://host",
		},
	}

	onKubernetes(func() {
		cl := fake.NewFakeClientWithScheme(scheme, cluster)

		reconciler := CheClusterReconciler{client: cl, scheme: scheme, syncer: sync.New(cl, scheme)}

		// first reconcile sets the finalizer, second reconcile actually finishes the process
		_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}})
		if err != nil {
			t.Fatalf("Failed to reconcile che manager with error: %s", err)
		}
		_, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}})
		if err != nil {
			t.Fatalf("Failed to reconcile che manager with error: %s", err)
		}

		persisted := chev2.CheCluster{}
		if err := cl.Get(context.TODO(), types.NamespacedName{Name: clusterName, Namespace: ns}, &persisted); err != nil {
			t.Fatal(err)
		}

		if persisted.GetCheHost() != "host" {
			t.Fatalf("Unexpected gateway host: %v", persisted.GetCheHost())
		}
	})

	onOpenShift(func() {
		cl := fake.NewFakeClientWithScheme(scheme, cluster)

		reconciler := CheClusterReconciler{client: cl, scheme: scheme, syncer: sync.New(cl, scheme)}

		// first reconcile sets the finalizer, second reconcile actually finishes the process
		_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}})
		if err != nil {
			t.Fatalf("Failed to reconcile che manager with error: %s", err)
		}
		_, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}})
		if err != nil {
			t.Fatalf("Failed to reconcile che manager with error: %s", err)
		}

		persisted := chev2.CheCluster{}
		if err := cl.Get(context.TODO(), types.NamespacedName{Name: clusterName, Namespace: ns}, &persisted); err != nil {
			t.Fatal(err)
		}

		if persisted.GetCheHost() != "host" {
			t.Fatalf("Unexpected gateway host: %v", persisted.GetCheHost())
		}
	})
}

func onKubernetes(f func()) {
	infrastructure.InitializeForTesting(infrastructure.Kubernetes)
	f()
}

func onOpenShift(f func()) {
	infrastructure.InitializeForTesting(infrastructure.OpenShiftv4)
	f()
}
