// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package scalingpolicies

import (
	"os"

	"github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/e2e/e2eutil"
	"github.com/hashicorp/nomad/e2e/framework"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/stretchr/testify/require"
)

type ScalingPolicyE2ETest struct {
	framework.TC
	namespaceIDs     []string
	namespacedJobIDs [][2]string
}

func init() {
	framework.AddSuites(&framework.TestSuite{
		Component:   "ScalingPolicies",
		CanRunLocal: true,
		Cases: []framework.TestCase{
			new(ScalingPolicyE2ETest),
		},
	})

}

func (tc *ScalingPolicyE2ETest) BeforeAll(f *framework.F) {
	e2eutil.WaitForLeader(f.T(), tc.Nomad())
	e2eutil.WaitForNodesReady(f.T(), tc.Nomad(), 1)
}

func (tc *ScalingPolicyE2ETest) AfterEach(f *framework.F) {
	if os.Getenv("NOMAD_TEST_SKIPCLEANUP") == "1" {
		return
	}

	for _, namespacedJob := range tc.namespacedJobIDs {
		err := e2eutil.StopJob(namespacedJob[1], "-purge", "-namespace",
			namespacedJob[0])
		f.Assert().NoError(err)
	}
	tc.namespacedJobIDs = [][2]string{}

	for _, ns := range tc.namespaceIDs {
		_, err := e2eutil.Command("nomad", "namespace", "delete", ns)
		f.Assert().NoError(err)
	}
	tc.namespaceIDs = []string{}

	_, err := e2eutil.Command("nomad", "system", "gc")
	f.Assert().NoError(err)
}

// TestScalingPolicies multi-namespace scaling policy test which performs reads
// and job manipulations to ensure Nomad behaves as expected.
func (tc *ScalingPolicyE2ETest) TestScalingPolicies(f *framework.F) {
	t := f.T()

	// Create our non-default namespace.
	_, err := e2eutil.Command("nomad", "namespace", "apply", "NamespaceA")
	f.NoError(err, "could not create namespace")
	tc.namespaceIDs = append(tc.namespaceIDs, "NamespaceA")

	// Register the jobs, capturing their IDs.
	jobDefault1 := tc.run(f, "scalingpolicies/input/namespace_default_1.nomad", "default", []string{"running"})
	jobDefault2 := tc.run(f, "scalingpolicies/input/namespace_default_1.nomad", "default", []string{"running"})
	jobA := tc.run(f, "scalingpolicies/input/namespace_a_1.nomad", "NamespaceA", []string{"running"})

	// Setup some reused query options.
	defaultQueryOpts := api.QueryOptions{Namespace: "default"}
	aQueryOpts := api.QueryOptions{Namespace: "NamespaceA"}

	// Perform initial listings to check each namespace has the correct number
	// of policies.
	defaultPolicyList, _, err := tc.Nomad().Scaling().ListPolicies(&defaultQueryOpts)
	require.NoError(t, err)
	require.Len(t, defaultPolicyList, 2)

	policyListA, _, err := tc.Nomad().Scaling().ListPolicies(&aQueryOpts)
	require.NoError(t, err)
	require.Len(t, policyListA, 1)

	// Deregister a job from the default namespace and then check all the
	// response objects.
	_, _, err = tc.Nomad().Jobs().Deregister(jobDefault1, true, &api.WriteOptions{Namespace: "default"})
	require.NoError(t, err)

	for i, namespacedJob := range tc.namespacedJobIDs {
		if namespacedJob[1] == jobDefault1 && namespacedJob[0] == "default" {
			tc.namespacedJobIDs = append(tc.namespacedJobIDs[:i], tc.namespacedJobIDs[i+1:]...)
			break
		}
	}

	defaultPolicyList, _, err = tc.Nomad().Scaling().ListPolicies(&defaultQueryOpts)
	require.NoError(t, err)
	require.Len(t, defaultPolicyList, 1)

	defaultPolicy := defaultPolicyList[0]
	require.True(t, defaultPolicy.Enabled)
	require.Equal(t, "horizontal", defaultPolicy.Type)
	require.Equal(t, defaultPolicy.Target["Namespace"], "default")
	require.Equal(t, defaultPolicy.Target["Job"], jobDefault2)
	require.Equal(t, defaultPolicy.Target["Group"], "horizontally_scalable")

	defaultPolicyInfo, _, err := tc.Nomad().Scaling().GetPolicy(defaultPolicy.ID, &defaultQueryOpts)
	require.NoError(t, err)
	require.Equal(t, *defaultPolicyInfo.Min, int64(1))
	require.Equal(t, *defaultPolicyInfo.Max, int64(10))
	require.Equal(t, defaultPolicyInfo.Policy["cooldown"], "13m")
	require.Equal(t, defaultPolicyInfo.Target["Namespace"], "default")
	require.Equal(t, defaultPolicyInfo.Target["Job"], jobDefault2)
	require.Equal(t, defaultPolicyInfo.Target["Group"], "horizontally_scalable")

	// Check response objects from the namespace with name "NamespaceA".
	aPolicyList, _, err := tc.Nomad().Scaling().ListPolicies(&aQueryOpts)
	require.NoError(t, err)
	require.Len(t, aPolicyList, 1)

	aPolicy := aPolicyList[0]
	require.True(t, aPolicy.Enabled)
	require.Equal(t, "horizontal", aPolicy.Type)
	require.Equal(t, aPolicy.Target["Namespace"], "NamespaceA")
	require.Equal(t, aPolicy.Target["Job"], jobA)
	require.Equal(t, aPolicy.Target["Group"], "horizontally_scalable")

	aPolicyInfo, _, err := tc.Nomad().Scaling().GetPolicy(aPolicy.ID, &aQueryOpts)
	require.NoError(t, err)
	require.Equal(t, *aPolicyInfo.Min, int64(1))
	require.Equal(t, *aPolicyInfo.Max, int64(10))
	require.Equal(t, aPolicyInfo.Policy["cooldown"], "13m")
	require.Equal(t, aPolicyInfo.Target["Namespace"], "NamespaceA")
	require.Equal(t, aPolicyInfo.Target["Job"], jobA)
	require.Equal(t, aPolicyInfo.Target["Group"], "horizontally_scalable")

	// List policies using the splat namespace operator.
	splatPolicyList, _, err := tc.Nomad().Scaling().ListPolicies(&api.QueryOptions{Namespace: "*"})
	require.NoError(t, err)
	require.Len(t, splatPolicyList, 2)

	// Deregister the job from the "NamespaceA" namespace and then check the
	// response objects.
	_, _, err = tc.Nomad().Jobs().Deregister(jobA, true, &api.WriteOptions{Namespace: "NamespaceA"})
	require.NoError(t, err)

	for i, namespacedJob := range tc.namespacedJobIDs {
		if namespacedJob[1] == jobA && namespacedJob[0] == "NamespaceA" {
			tc.namespacedJobIDs = append(tc.namespacedJobIDs[:i], tc.namespacedJobIDs[i+1:]...)
			break
		}
	}

	aPolicyList, _, err = tc.Nomad().Scaling().ListPolicies(&aQueryOpts)
	require.NoError(t, err)
	require.Len(t, aPolicyList, 0)

	// Update the running job scaling policy and ensure the changes are
	// reflected.
	err = e2eutil.Register(jobDefault2, "scalingpolicies/input/namespace_default_2.nomad")
	require.NoError(t, err)

	defaultPolicyList, _, err = tc.Nomad().Scaling().ListPolicies(&defaultQueryOpts)
	require.NoError(t, err)
	require.Len(t, defaultPolicyList, 1)

	defaultPolicyInfo, _, err = tc.Nomad().Scaling().GetPolicy(defaultPolicyList[0].ID, &defaultQueryOpts)
	require.NoError(t, err)
	require.Equal(t, *defaultPolicyInfo.Min, int64(1))
	require.Equal(t, *defaultPolicyInfo.Max, int64(11))
	require.Equal(t, defaultPolicyInfo.Policy["cooldown"], "14m")
	require.Equal(t, defaultPolicyInfo.Target["Namespace"], "default")
	require.Equal(t, defaultPolicyInfo.Target["Job"], jobDefault2)
	require.Equal(t, defaultPolicyInfo.Target["Group"], "horizontally_scalable")

}

// run is a helper which runs a job within a namespace, providing the caller
// with the generated jobID.
func (tc *ScalingPolicyE2ETest) run(f *framework.F, jobSpec, ns string, expected []string) string {
	jobID := "test-scaling-policy-" + uuid.Generate()[0:8]
	f.NoError(e2eutil.Register(jobID, jobSpec))
	tc.namespacedJobIDs = append(tc.namespacedJobIDs, [2]string{ns, jobID})
	f.NoError(e2eutil.WaitForAllocStatusExpected(jobID, ns, expected), "job should be running")
	return jobID
}
