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

package nomad

import (
	"sync"
	"testing"
	"time"

	msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/testutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPlanEndpoint_Submit(t *testing.T) {
	ci.Parallel(t)

	s1, cleanupS1 := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS1()
	codec := rpcClient(t, s1)
	testutil.WaitForLeader(t, s1.RPC)

	// Create the register request
	eval1 := mock.Eval()
	s1.evalBroker.Enqueue(eval1)

	evalOut, token, err := s1.evalBroker.Dequeue([]string{eval1.Type}, time.Second)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if evalOut != eval1 {
		t.Fatalf("Bad eval")
	}

	// Submit a plan
	plan := mock.Plan()
	plan.EvalID = eval1.ID
	plan.EvalToken = token
	plan.Job = mock.Job()
	req := &structs.PlanRequest{
		Plan:         plan,
		WriteRequest: structs.WriteRequest{Region: "global"},
	}
	var resp structs.PlanResponse
	if err := msgpackrpc.CallWithCodec(codec, "Plan.Submit", req, &resp); err != nil {
		t.Fatalf("err: %v", err)
	}
	if resp.Result == nil {
		t.Fatalf("missing result")
	}
}

// TestPlanEndpoint_Submit_Bad asserts that the Plan.Submit endpoint rejects
// bad data with an error instead of panicking.
func TestPlanEndpoint_Submit_Bad(t *testing.T) {
	ci.Parallel(t)

	s1, cleanupS1 := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS1()
	codec := rpcClient(t, s1)
	testutil.WaitForLeader(t, s1.RPC)

	// Mock a valid eval being dequeued by a worker
	eval := mock.Eval()
	s1.evalBroker.Enqueue(eval)

	evalOut, _, err := s1.evalBroker.Dequeue([]string{eval.Type}, time.Second)
	require.NoError(t, err)
	require.Equal(t, eval, evalOut)

	cases := []struct {
		Name string
		Plan *structs.Plan
		Err  string
	}{
		{
			Name: "Nil",
			Plan: nil,
			Err:  "cannot submit nil plan",
		},
		{
			Name: "Empty",
			Plan: &structs.Plan{},
			Err:  "evaluation is not outstanding",
		},
		{
			Name: "BadEvalID",
			Plan: &structs.Plan{
				EvalID: "1234", // does not exist
			},
			Err: "evaluation is not outstanding",
		},
		{
			Name: "MissingToken",
			Plan: &structs.Plan{
				EvalID: eval.ID,
			},
			Err: "evaluation token does not match",
		},
		{
			Name: "InvalidToken",
			Plan: &structs.Plan{
				EvalID:    eval.ID,
				EvalToken: "1234", // invalid
			},
			Err: "evaluation token does not match",
		},
	}

	for i := range cases {
		tc := cases[i]
		t.Run(tc.Name, func(t *testing.T) {
			req := &structs.PlanRequest{
				Plan:         tc.Plan,
				WriteRequest: structs.WriteRequest{Region: "global"},
			}
			var resp structs.PlanResponse
			err := msgpackrpc.CallWithCodec(codec, "Plan.Submit", req, &resp)
			require.EqualError(t, err, tc.Err)
			require.Nil(t, resp.Result)
		})
	}

	// Ensure no plans were enqueued
	require.Zero(t, s1.planner.planQueue.Stats().Depth)
}

func TestPlanEndpoint_ApplyConcurrent(t *testing.T) {
	ci.Parallel(t)

	s1, cleanupS1 := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS1()
	testutil.WaitForLeader(t, s1.RPC)

	plans := []*structs.Plan{}

	for i := 0; i < 5; i++ {

		// Create a node to place on
		node := mock.Node()
		store := s1.fsm.State()
		require.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 100, node))

		// Create the eval
		eval1 := mock.Eval()
		s1.evalBroker.Enqueue(eval1)
		require.NoError(t, store.UpsertEvals(
			structs.MsgTypeTestSetup, 150, []*structs.Evaluation{eval1}))

		evalOut, token, err := s1.evalBroker.Dequeue([]string{eval1.Type}, time.Second)
		require.NoError(t, err)
		require.Equal(t, eval1, evalOut)

		// Submit a plan
		plan := mock.Plan()
		plan.EvalID = eval1.ID
		plan.EvalToken = token
		plan.Job = mock.Job()

		alloc := mock.Alloc()
		alloc.JobID = plan.Job.ID
		alloc.Job = plan.Job

		plan.NodeAllocation = map[string][]*structs.Allocation{
			node.ID: []*structs.Allocation{alloc}}

		plans = append(plans, plan)
	}

	var wg sync.WaitGroup

	for _, plan := range plans {
		plan := plan
		wg.Add(1)
		go func() {

			req := &structs.PlanRequest{
				Plan:         plan,
				WriteRequest: structs.WriteRequest{Region: "global"},
			}
			var resp structs.PlanResponse
			err := s1.RPC("Plan.Submit", req, &resp)
			assert.NoError(t, err)
			assert.NotNil(t, resp.Result, "missing result")
			wg.Done()
		}()
	}

	wg.Wait()
}
