// Licensed Materials - Property of IBM
// (c) Copyright IBM Corporation 2018, 2019. All Rights Reserved.
// Note to U.S. Government Users Restricted Rights:
// Use, duplication or disclosure restricted by GSA ADP Schedule
// Contract with IBM Corp.
package integration

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"

	seedctx "github.com/open-cluster-management/seed-sdk/pkg/context"
	"github.com/open-cluster-management/seed-sdk/pkg/controller"
	"github.com/open-cluster-management/seed-sdk/pkg/testing/framework"

	accesspolicyv1 "github.com/open-cluster-management/hcm-compliance/pkg/apis/accesspolicy/v1alpha1"
	compliancev1 "github.com/open-cluster-management/hcm-compliance/pkg/apis/compliance/v1alpha1"
	compliancev1alpha1 "github.com/open-cluster-management/hcm-compliance/pkg/apis/compliance/v1alpha1"
	policy "github.com/open-cluster-management/hcm-compliance/pkg/apis/policy/v1alpha1"
	"github.com/open-cluster-management/hcm-compliance/pkg/sync"
)

func TestCreateCRD(t *testing.T) {
	// @todo can eliminate this test case? Basically tests that we can create CRDs in k8s...
	tl := framework.NewIntegrationLogger(t)
	as := framework.SetUpKubernetesAPIServer(tl)
	defer as.TearDown(tl)

	config := as.NewConfig(tl)
	ctx, cancelFunc := context.WithCancel(context.Background())
	defer cancelFunc()

	controller.New().
		Install(compliancev1.CustomResourceDefinitions). // not registered automatically
		Watch("compliances.v1alpha1.compliance.mcm.ibm.com", "Compliance", &compliancev1.SchemeBuilder).
		RunLocalWithServerConfig(ctx, config)

	client, err := sync.NewComplianceClient(config)
	if err != nil {
		t.Fatal(err)
	}
	compliance := buildComplianceCR()
	if err = client.Create(compliance); err != nil {
		t.Fatal(err)
	}

	err = client.Get(compliance)
	assert.NoError(t, err)
	assert.NotEmpty(t, compliance.ObjectMeta.GetUID)
	compliance.Spec.RuntimeRules[0].Spec.ComplianceType = policy.MustNotHave

	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = client.Update(compliance)
		if err != nil {
			//fmt.Printf("Error %v\n", err)
			client.Get(compliance)
			compliance.Spec.RuntimeRules[0].Spec.ComplianceType = policy.MustNotHave
			return false, nil
		}
		return true, nil
	})
	assert.NoError(t, err)
	err = client.Get(compliance)
	assert.NoError(t, err)
	assert.Equal(t, string(policy.MustNotHave), string(compliance.Spec.RuntimeRules[0].Spec.ComplianceType))
}

// @todo consider moving all cluster, client, etc. setup into a shared function
func TestSyncSpec(t *testing.T) {
	tl := framework.NewIntegrationLogger(t)
	hubCluster := framework.SetUpKubernetesAPIServer(tl)
	defer hubCluster.TearDown(tl)
	managedCluster := framework.SetUpKubernetesAPIServer(tl)
	defer managedCluster.TearDown(tl)

	rCtx, rCancelFunc := seedctx.NewGlobal()
	defer rCancelFunc()
	lCtx, lCancelFunc := seedctx.NewGlobal()
	defer lCancelFunc()

	rCfg := hubCluster.NewConfig(tl)
	lCfg := managedCluster.NewConfig(tl)
	cs, _ := sync.NewComplianceSyncher(rCfg, lCfg)

	// run controllers to watch 'remote' (management) and 'local' (managed) API servers
	hc := controller.New().Install(compliancev1.CustomResourceDefinitions)
	mc := controller.New().Install(compliancev1.CustomResourceDefinitions)
	c := cs.WatchSpecEvents(hc)
	if err := c.RunLocalWithServerConfig(rCtx, rCfg); err != nil {
		t.Fatal(err)
	}

	// run controller for local (managed) API server
	c = cs.WatchStatusEvents(mc)
	if err := c.RunLocalWithServerConfig(lCtx, lCfg); err != nil {
		t.Fatal(err)
	}

	// create client for remote (management) API server
	remote, err := sync.NewComplianceClient(rCfg)
	if err != nil {
		t.Fatal(err)
	}

	// create client for local (managed) API server
	local, err := sync.NewComplianceClient(lCfg)
	if err != nil {
		t.Fatal(err)
	}

	// deploy compliance object on remote cluster
	rCompliance := buildComplianceCR()
	err = remote.Create(rCompliance)
	assert.NoError(t, err)

	// wait until object gets deployed on local cluster, timeout if not found
	// use deep copy for query otherwise we'll mix up with remote cluster obj
	lCompliance := rCompliance.DeepCopy()
	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = local.Get(lCompliance)
		if err != nil {
			return false, nil
		}
		return true, nil
	})
	err = local.Get(lCompliance) // confirm we exited with an object, not on timeout
	assert.NoError(t, err)

	// update object specs on remote cluster and verify it gets updated on local
	// first, get latest object from store
	err = remote.Get(rCompliance)
	assert.NoError(t, err)
	rCompliance.Spec.RuntimeRules[0].Spec.ComplianceType = policy.MustNotHave
	err = remote.Update(rCompliance)
	assert.NoError(t, err)

	// wait until object gets specs updated on local cluster, timeout if not updated
	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = local.Get(lCompliance)
		if err != nil || lCompliance.Spec.RuntimeRules[0].Spec.ComplianceType != policy.MustNotHave {
			return false, nil
		}
		return true, nil
	})
	err = local.Get(lCompliance)
	assert.NoError(t, err)
	assert.Equal(t, policy.MustNotHave, lCompliance.Spec.RuntimeRules[0].Spec.ComplianceType)
}

func TestSyncStatus(t *testing.T) {
	tl := framework.NewIntegrationLogger(t)
	hubCluster := framework.SetUpKubernetesAPIServer(tl)
	defer hubCluster.TearDown(tl)
	managedCluster := framework.SetUpKubernetesAPIServer(tl)
	defer managedCluster.TearDown(tl)

	rCtx, rCancelFunc := seedctx.NewGlobal()
	defer rCancelFunc()
	lCtx, lCancelFunc := seedctx.NewGlobal()
	defer lCancelFunc()

	rCfg := hubCluster.NewConfig(tl)
	lCfg := managedCluster.NewConfig(tl)
	cs, _ := sync.NewComplianceSyncher(rCfg, lCfg)

	// run controllers to watch 'remote' (management) and 'local' (managed) API servers
	hc := controller.New().Install(compliancev1.CustomResourceDefinitions)
	mc := controller.New().Install(compliancev1.CustomResourceDefinitions)
	c := cs.WatchSpecEvents(hc)
	if err := c.RunLocalWithServerConfig(rCtx, rCfg); err != nil {
		t.Fatal(err)
	}

	// run controller for local (managed) API server
	c = cs.WatchStatusEvents(mc)
	if err := c.RunLocalWithServerConfig(lCtx, lCfg); err != nil {
		t.Fatal(err)
	}

	// create client for remote (management) API server
	remote, err := sync.NewComplianceClient(rCfg)
	if err != nil {
		t.Fatal(err)
	}

	// create client for local (managed) API server
	local, err := sync.NewComplianceClient(lCfg)
	if err != nil {
		t.Fatal(err)
	}

	// deploy compliance object on remote cluster
	rCompliance := buildComplianceCR()
	err = remote.Create(rCompliance)
	assert.NoError(t, err)

	// wait until object gets deployed on local cluster, timeout if not found
	// use deep copy for query otherwise we'll mix up with remote cluster obj
	lCompliance := rCompliance.DeepCopy()
	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = local.Get(lCompliance)
		if err != nil {
			return false, nil
		}
		return true, nil
	})
	err = local.Get(lCompliance) // ensure we completed the wait.Poll with an object
	assert.NoError(t, err)

	// update object status on local cluster and verify it gets updated on remote
	clustername := "local-cluster"
	policyname := lCompliance.Spec.RuntimeRules[0].Name
	lCompliance.Status.Status = make(compliancev1.ComplianceMap)

	lCompliance.Status.Status[clustername] = &compliancev1alpha1.CompliancePerClusterStatus{ClusterName: clustername, AggregatePolicyStatus: map[string]*policy.PolicyStatus{}}
	lCompliance.Status.Status[clustername].AggregatePolicyStatus[policyname] = &policy.PolicyStatus{State: policy.ResourceStateCreated, Reason: "policy check pending"}

	//lCompliance.Status[clustername] = make(map[string]policy.PolicyStatus)
	//lCompliance.Status[clustername][policyname] = policy.PolicyStatus{
	//	State:  policy.ResourceStateCreated,
	//	Reason: "policy check pending",
	//}

	err = local.Update(lCompliance)
	assert.NoError(t, err)
	// wait until object gets status updated on remote cluster, timeout if not updated
	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = remote.Get(rCompliance)
		if err != nil || rCompliance.Status.Status[clustername].AggregatePolicyStatus[policyname].State != policy.ResourceStateCreated {
			return false, nil
		}
		return true, nil
	})
	err = remote.Get(rCompliance) // ensure wait.Poll didn't time out
	assert.NoError(t, err)
	assert.Equal(t, policy.ResourceStateCreated, rCompliance.Status.Status[clustername].AggregatePolicyStatus[policyname].State)
}

// test the case where object is deleted on remote cluster
func TestRemoteDelete(t *testing.T) {
	tl := framework.NewIntegrationLogger(t)
	hubCluster := framework.SetUpKubernetesAPIServer(tl)
	defer hubCluster.TearDown(tl)
	managedCluster := framework.SetUpKubernetesAPIServer(tl)
	defer managedCluster.TearDown(tl)

	rCtx, rCancelFunc := seedctx.NewGlobal()
	defer rCancelFunc()
	lCtx, lCancelFunc := seedctx.NewGlobal()
	defer lCancelFunc()

	rCfg := hubCluster.NewConfig(tl)
	lCfg := managedCluster.NewConfig(tl)
	cs, _ := sync.NewComplianceSyncher(rCfg, lCfg)

	// run controllers to watch 'remote' (management) and 'local' (managed) API servers
	hc := controller.New().Install(compliancev1.CustomResourceDefinitions)
	mc := controller.New().Install(compliancev1.CustomResourceDefinitions)
	c := cs.WatchSpecEvents(hc)
	if err := c.RunLocalWithServerConfig(rCtx, rCfg); err != nil {
		t.Fatal(err)
	}

	// run controller for local (managed) API server
	c = cs.WatchStatusEvents(mc)
	if err := c.RunLocalWithServerConfig(lCtx, lCfg); err != nil {
		t.Fatal(err)
	}

	// create client for remote (management) API server
	remote, err := sync.NewComplianceClient(rCfg)
	if err != nil {
		t.Fatal(err)
	}

	// create client for local (managed) API server
	local, err := sync.NewComplianceClient(lCfg)
	if err != nil {
		t.Fatal(err)
	}

	// deploy policy object on remote cluster
	rCompliance := buildComplianceCR()
	err = remote.Create(rCompliance)
	assert.NoError(t, err)

	// wait until object gets deployed on local cluster, timeout if not found
	// use deep copy for query otherwise we'll mix up with remote cluster obj
	lCompliance := rCompliance.DeepCopy()

	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = local.Get(lCompliance)
		if err != nil {
			return false, nil
		}
		return true, nil
	})
	err = local.Get(lCompliance) // ensure we did not exit on timeout
	assert.NoError(t, err)

	// delete object on remote cluster and wait for it to disappear
	err = remote.Delete(rCompliance, &metav1.DeleteOptions{})
	assert.NoError(t, err)

	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = remote.Get(rCompliance)
		if err != nil {
			return true, nil
		}
		return false, nil
	})
	err = remote.Get(rCompliance) // ensure not exited on timeout
	assert.True(t, errors.IsNotFound(err))

	// now check object gets deleted on local cluster
	err = wait.Poll(500*time.Millisecond, 10*time.Second, func() (bool, error) {
		err = local.Get(lCompliance)
		if err != nil {
			if errors.IsNotFound(err) { // expected
				return true, nil
			}
			return true, err
		}
		return false, nil
	})
	assert.NoError(t, err)
}

func TestMultipleKindSpecSync(t *testing.T) {
	tl := framework.NewIntegrationLogger(t)
	hubCluster := framework.SetUpKubernetesAPIServer(tl)
	defer hubCluster.TearDown(tl)
	managedCluster := framework.SetUpKubernetesAPIServer(tl)
	defer managedCluster.TearDown(tl)

	rCtx, rCancelFunc := seedctx.NewGlobal()
	defer rCancelFunc()
	lCtx, lCancelFunc := seedctx.NewGlobal()
	defer lCancelFunc()

	rCfg := hubCluster.NewConfig(tl)
	lCfg := managedCluster.NewConfig(tl)
	complianceSync, _ := sync.NewComplianceSyncher(rCfg, lCfg)
	accesspolicySync, _ := sync.NewAccessPolicySyncher(rCfg, lCfg)

	// run controllers to watch 'remote' (management) and 'local' (managed) API servers
	hc := controller.New().
		Install(compliancev1.CustomResourceDefinitions).
		Install(accesspolicyv1.CustomResourceDefinitions)
	mc := controller.New().
		Install(compliancev1.CustomResourceDefinitions).
		Install(accesspolicyv1.CustomResourceDefinitions)
	_ = complianceSync.WatchSpecEvents(hc)
	c := accesspolicySync.WatchSpecEvents(hc)
	if err := c.RunLocalWithServerConfig(rCtx, rCfg); err != nil {
		t.Fatal(err)
	}

	// run controller for local (managed) API server
	_ = complianceSync.WatchStatusEvents(mc)
	c = accesspolicySync.WatchStatusEvents(hc)
	if err := c.RunLocalWithServerConfig(lCtx, lCfg); err != nil {
		t.Fatal(err)
	}

	// create clients for remote (management) API server
	remoteComplianceClient, err := sync.NewComplianceClient(rCfg)
	if err != nil {
		t.Fatal(err)
	}
	remoteAccessPolicyClient, err := sync.NewAccessPolicyClient(rCfg)
	if err != nil {
		t.Fatal(err)
	}

	// create and deploy objects on remote cluster
	rCompliance := buildComplianceCR()
	err = remoteComplianceClient.Create(rCompliance)
	assert.NoError(t, err)
	rAccessPolicy := buildAccessPolicyCR()
	err = remoteAccessPolicyClient.Create(rAccessPolicy)
	assert.NoError(t, err)

	// create clients for local (managed) API server
	localComplianceClient, err := sync.NewComplianceClient(lCfg)
	if err != nil {
		t.Fatal(err)
	}
	// wait until object gets deployed on local cluster, timeout if not found
	// use deep copy for query otherwise we'll mix up with remote cluster obj
	lCompliance := rCompliance.DeepCopy()
	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = localComplianceClient.Get(lCompliance)
		if err != nil {
			return false, nil
		}
		return true, nil
	})
	err = localComplianceClient.Get(lCompliance) // confirm we exited with an object, not on timeout
	assert.NoError(t, err)

	localAccessPolicyClient, err := sync.NewAccessPolicyClient(lCfg)
	if err != nil {
		t.Fatal(err)
	}
	// wait until object gets deployed on local cluster, timeout if not found
	// use deep copy for query otherwise we'll mix up with remote cluster obj
	lAccessPolicy := rAccessPolicy.DeepCopy()
	err = wait.Poll(500*time.Millisecond, 5*time.Second, func() (bool, error) {
		err = localAccessPolicyClient.Get(lAccessPolicy)
		if err != nil {
			return false, nil
		}
		return true, nil
	})
	err = localAccessPolicyClient.Get(lAccessPolicy) // confirm we exited with an object, not on timeout
	assert.NoError(t, err)
}

func buildComplianceCR() *compliancev1.Compliance {
	// Create an instance of our custom resource
	return &compliancev1.Compliance{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Compliance",
			APIVersion: "compliance.mcm.ibm.com/v1alpha1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "compliance-1",
			Namespace: "default",
			//SelfLink:  "/apis/policy.mcm.ibm.com/v1alpha1/namespaces/default/policies/" + name,
		},
		Spec: compliancev1.ComplianceSpec{
			RuntimeRules: []policy.Policy{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "policy-Z",
					},
					Spec: policy.PolicySpec{
						ComplianceType:    "musthave",
						RemediationAction: "enforce",
						Namespaces: policy.Target{
							Include: []string{"*"},
						},
					},
				},
			},
		},
	}
}

func buildAccessPolicyCR() *accesspolicyv1.AccessPolicy {
	return &accesspolicyv1.AccessPolicy{
		TypeMeta: metav1.TypeMeta{
			Kind:       "AccessPolicy",
			APIVersion: "accesspolicy.mcm.ibm.com/v1alpha1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "access-policy-test",
			Namespace: "default",
		},
		Spec: accesspolicyv1.AccessPolicySpec{
			Namespaces: "some-ns",
			Source: accesspolicyv1.Microservice{
				Service: "source-svc",
			},
			Destination: accesspolicyv1.Microservice{
				Service: "destination-svc",
			},
			Rules: []accesspolicyv1.AccessPolicyRule{
				accesspolicyv1.AccessPolicyRule{
					Protocol:  "http",
					Actions:   []string{"get"},
					Resources: []string{"/*"},
				},
			},
		},
	}
}
