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

package client_test

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/command/agent"
	"github.com/hashicorp/nomad/nomad"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/nomad/structs/config"
	"github.com/hashicorp/nomad/testutil"
	"github.com/stretchr/testify/require"
)

// TestPrevAlloc_StreamAllocDir_TLS asserts ephemeral disk migrations still
// work when TLS is enabled.
func TestPrevAlloc_StreamAllocDir_TLS(t *testing.T) {
	const (
		caFn         = "../helper/tlsutil/testdata/nomad-agent-ca.pem"
		serverCertFn = "../helper/tlsutil/testdata/global-server-nomad.pem"
		serverKeyFn  = "../helper/tlsutil/testdata/global-server-nomad-key.pem"
		clientCertFn = "../helper/tlsutil/testdata/global-client-nomad.pem"
		clientKeyFn  = "../helper/tlsutil/testdata/global-client-nomad-key.pem"
	)
	ci.Parallel(t)
	require := require.New(t)

	server, cleanupS := nomad.TestServer(t, func(c *nomad.Config) {
		c.TLSConfig = &config.TLSConfig{
			EnableHTTP:           true,
			EnableRPC:            true,
			VerifyServerHostname: true,
			CAFile:               caFn,
			CertFile:             serverCertFn,
			KeyFile:              serverKeyFn,
		}
	})
	defer cleanupS()
	testutil.WaitForLeader(t, server.RPC)

	t.Logf("[TEST] Leader started: %s", server.GetConfig().RPCAddr.String())

	agentConfFunc := func(c *agent.Config) {
		c.Region = "global"
		c.TLSConfig = &config.TLSConfig{
			EnableHTTP:           true,
			EnableRPC:            true,
			VerifyServerHostname: true,
			CAFile:               caFn,
			CertFile:             clientCertFn,
			KeyFile:              clientKeyFn,
		}
		c.Client.Enabled = true
		c.Client.Servers = []string{server.GetConfig().RPCAddr.String()}
	}
	client1 := agent.NewTestAgent(t, "client1", agentConfFunc)
	defer client1.Shutdown()

	client2 := agent.NewTestAgent(t, "client2", agentConfFunc)
	defer client2.Shutdown()

	job := mock.Job()
	job.Constraints = []*structs.Constraint{
		{
			LTarget: "${node.unique.name}",
			RTarget: "client1",
			Operand: "=",
		},
	}
	job.TaskGroups[0].Constraints = nil
	job.TaskGroups[0].Tasks[0].Services = nil
	job.TaskGroups[0].Count = 1
	job.TaskGroups[0].EphemeralDisk.Sticky = true
	job.TaskGroups[0].EphemeralDisk.Migrate = true
	job.TaskGroups[0].Tasks[0] = &structs.Task{
		Name:   "migrate_tls",
		Driver: "mock_driver",
		Config: map[string]interface{}{
			"run_for": "1m",
		},
		LogConfig: structs.DefaultLogConfig(),
		Resources: &structs.Resources{
			CPU:      50,
			MemoryMB: 25,
		},
	}
	testutil.WaitForRunning(t, server.RPC, job.Copy())

	allocArgs := &structs.JobSpecificRequest{}
	allocArgs.JobID = job.ID
	allocArgs.QueryOptions.Region = "global"
	var allocReply structs.JobAllocationsResponse
	require.NoError(server.RPC("Job.Allocations", allocArgs, &allocReply))
	require.Len(allocReply.Allocations, 1)
	origAlloc := allocReply.Allocations[0].ID

	// Save a file into alloc dir
	contents := []byte("123\n456")
	allocFn := filepath.Join(client1.DataDir, "alloc", origAlloc, "alloc", "data", "bar")
	require.NoError(os.WriteFile(allocFn, contents, 0666))
	t.Logf("[TEST] Wrote initial file: %s", allocFn)

	// Migrate alloc to other node
	job.Constraints[0].RTarget = "client2"

	// Only register job - don't wait for running - since previous completed allocs
	// will interfere
	testutil.RegisterJob(t, server.RPC, job.Copy())

	// Wait for new alloc to be running
	var newAlloc *structs.AllocListStub
	testutil.WaitForResult(func() (bool, error) {
		allocArgs := &structs.JobSpecificRequest{}
		allocArgs.JobID = job.ID
		allocArgs.QueryOptions.Region = "global"
		var allocReply structs.JobAllocationsResponse
		require.NoError(server.RPC("Job.Allocations", allocArgs, &allocReply))
		if n := len(allocReply.Allocations); n != 2 {
			return false, fmt.Errorf("expected 2 allocs found %d", n)
		}

		// Pick the one that didn't exist before
		if allocReply.Allocations[0].ID == origAlloc {
			newAlloc = allocReply.Allocations[1]
		} else {
			newAlloc = allocReply.Allocations[0]
		}

		return newAlloc.ClientStatus != structs.AllocClientStatusRunning,
			fmt.Errorf("client status: %v", newAlloc.ClientStatus)
	}, func(err error) {
		t.Fatalf("new alloc not running: %v", err)
	})

	// Wait for file to appear on other client
	allocFn2 := filepath.Join(client2.DataDir, "alloc", newAlloc.ID, "alloc", "data", "bar")
	t.Logf("[TEST] Comparing against file: %s", allocFn2)
	testutil.WaitForResult(func() (bool, error) {
		found, err := os.ReadFile(allocFn2)
		if err != nil {
			return false, err
		}
		return bytes.Equal(contents, found), fmt.Errorf("contents misatch. expected:\n%s\n\nfound:\n%s\n",
			contents, found)
	}, func(err error) {
		t.Fatalf("file didn't migrate: %v", err)
	})
}
