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

package client

import (
	"testing"

	"github.com/hashicorp/nomad/acl"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/client/config"
	"github.com/hashicorp/nomad/helper/pointer"
	"github.com/hashicorp/nomad/nomad"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/testutil"
	"github.com/shoenig/test/must"
)

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

	s, _, cleanupS := nomad.TestACLServer(t, nil)
	defer cleanupS()
	testutil.WaitForLeader(t, s.RPC)

	c1, cleanup := TestClient(t, func(c *config.Config) {
		c.ACLEnabled = true
		c.Servers = []string{s.GetConfig().RPCAddr.String()}
	})
	defer cleanup()

	// Dynamic node metadata endpoints should fail without auth
	applyReq := &structs.NodeMetaApplyRequest{
		NodeID: c1.NodeID(),
		Meta: map[string]*string{
			"foo": pointer.Of("bar"),
		},
	}

	resp := structs.NodeMetaResponse{}
	err := c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.ErrorContains(t, err, structs.ErrPermissionDenied.Error())

	readReq := &structs.NodeSpecificRequest{
		NodeID: c1.NodeID(),
	}
	err = c1.ClientRPC("NodeMeta.Read", readReq, &resp)
	must.ErrorContains(t, err, structs.ErrPermissionDenied.Error())

	// Create a token to make it work
	policyGood := mock.NodePolicy(acl.PolicyWrite)
	tokenGood := mock.CreatePolicyAndToken(t, s.State(), 1009, "meta", policyGood)

	applyReq.AuthToken = tokenGood.SecretID
	err = c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.NoError(t, err)
	must.Eq(t, "bar", resp.Meta["foo"])

	readReq.AuthToken = tokenGood.SecretID
	err = c1.ClientRPC("NodeMeta.Read", readReq, &resp)
	must.NoError(t, err)
}

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

	s, cleanupS := nomad.TestServer(t, nil)
	defer cleanupS()
	testutil.WaitForLeader(t, s.RPC)

	c1, cleanup := TestClient(t, func(c *config.Config) {
		c.Servers = []string{s.GetConfig().RPCAddr.String()}
	})
	defer cleanup()

	applyReq := &structs.NodeMetaApplyRequest{
		NodeID: c1.NodeID(),
		Meta:   map[string]*string{},
	}

	resp := struct{}{}

	// An empty map is an error
	err := c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.ErrorContains(t, err, "missing required Meta")

	// empty keys are prohibited
	applyReq.Meta[""] = pointer.Of("bad")
	err = c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.ErrorContains(t, err, "empty")

	// * is prohibited in keys
	delete(applyReq.Meta, "")
	applyReq.Meta["*"] = pointer.Of("bad")
	err = c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.ErrorContains(t, err, "*")
}

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

	s, cleanupS := nomad.TestServer(t, nil)
	defer cleanupS()
	testutil.WaitForLeader(t, s.RPC)

	c1, cleanup := TestClient(t, func(c *config.Config) {
		c.Servers = []string{s.GetConfig().RPCAddr.String()}
		c.Node.Meta["static_meta"] = "true"
	})
	defer cleanup()

	// Set dynamic node metadata.
	applyReq := &structs.NodeMetaApplyRequest{
		NodeID: c1.NodeID(),
		Meta: map[string]*string{
			"dynamic_meta": pointer.Of("true"),
		},
	}
	var resp structs.NodeMetaResponse
	err := c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.NoError(t, err)

	// Check static_meta:
	//   1. must be present in Static.
	//   2. must be present in Meta
	must.Eq(t, resp.Static["static_meta"], "true")
	must.Eq(t, resp.Meta["static_meta"], "true")

	// Check dynamic_meta:
	//   1. must be present in Dynamic.
	//   2. must be present in Meta
	must.Eq(t, *resp.Dynamic["dynamic_meta"], "true")
	must.Eq(t, resp.Meta["dynamic_meta"], "true")

	// Unset static node metada.
	applyReq = &structs.NodeMetaApplyRequest{
		NodeID: c1.NodeID(),
		Meta: map[string]*string{
			"static_meta": nil,
		},
	}
	err = c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.NoError(t, err)

	// Check static_meta:
	//   1. must be present in Static.
	//   2. must not be present in Meta
	//   3. must be nil in Dynamic to persist its removal even on restarts.
	must.Eq(t, resp.Static["static_meta"], "true")
	must.MapNotContainsKey(t, resp.Meta, "static_meta")
	must.Nil(t, resp.Dynamic["static_meta"])

	// Check dynamic_meta:
	//   1. must be present in Dynamic.
	//   2. must be present in Meta
	must.Eq(t, *resp.Dynamic["dynamic_meta"], "true")
	must.Eq(t, resp.Meta["dynamic_meta"], "true")

	// Unset dynamic node metada.
	applyReq = &structs.NodeMetaApplyRequest{
		NodeID: c1.NodeID(),
		Meta: map[string]*string{
			"dynamic_meta": nil,
		},
	}
	err = c1.ClientRPC("NodeMeta.Apply", applyReq, &resp)
	must.NoError(t, err)

	// Check static_meta:
	//   1. must be present in Static.
	//   2. must not be present in Meta
	//   3. must be nil in Dynamic to persist its removal even on restarts.
	must.Eq(t, resp.Static["static_meta"], "true")
	must.MapNotContainsKey(t, resp.Meta, "static_meta")
	must.Nil(t, resp.Dynamic["static_meta"])

	// Check dynamic_meta:
	//   1. must not be present in Dynamic.
	//   2. must not be present in Meta
	must.MapNotContainsKey(t, resp.Dynamic, "dynamic_meta")
	must.MapNotContainsKey(t, resp.Meta, "dynamic_meta")
}
