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

package command

import (
	"fmt"
	"os"
	"regexp"
	"testing"
	"time"

	"github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/command/agent"
	"github.com/hashicorp/nomad/helper/pointer"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/testutil"
	"github.com/shoenig/test/must"
)

var nonAlphaNum = regexp.MustCompile(`[^a-zA-Z0-9]+`)

func testServer(t *testing.T, runClient bool, cb func(*agent.Config)) (*agent.TestAgent, *api.Client, string) {
	// Make a new test server
	a := agent.NewTestAgent(t, t.Name(), func(config *agent.Config) {
		config.Client.Enabled = runClient

		if cb != nil {
			cb(config)
		}
	})
	t.Cleanup(a.Shutdown)

	c := a.Client()
	return a, c, a.HTTPAddr()
}

// testClient starts a new test client, blocks until it joins, and performs
// cleanup after the test is complete.
func testClient(t *testing.T, name string, cb func(*agent.Config)) (*agent.TestAgent, *api.Client, string) {
	t.Logf("Starting client agent %s", name)
	a := agent.NewTestAgent(t, name, func(config *agent.Config) {
		if cb != nil {
			cb(config)
		}
	})
	t.Cleanup(a.Shutdown)

	c := a.Client()
	t.Logf("Waiting for client %s to join server(s) %s", name, a.GetConfig().Client.Servers)
	testutil.WaitForClient(t, a.Agent.RPC, a.Agent.Client().NodeID(), a.Agent.Client().Region())

	return a, c, a.HTTPAddr()
}

func testJob(jobID string) *api.Job {
	task := api.NewTask("task1", "mock_driver").
		SetConfig("kill_after", "1s").
		SetConfig("run_for", "5s").
		SetConfig("exit_code", 0).
		Require(&api.Resources{
			MemoryMB: pointer.Of(256),
			CPU:      pointer.Of(100),
		}).
		SetLogConfig(&api.LogConfig{
			MaxFiles:      pointer.Of(1),
			MaxFileSizeMB: pointer.Of(2),
		})

	group := api.NewTaskGroup("group1", 1).
		AddTask(task).
		RequireDisk(&api.EphemeralDisk{
			SizeMB: pointer.Of(20),
		})

	job := api.NewBatchJob(jobID, jobID, "global", 1).
		AddDatacenter("dc1").
		AddTaskGroup(group)

	return job
}

func testNomadServiceJob(jobID string) *api.Job {
	j := testJob(jobID)
	j.TaskGroups[0].Services = []*api.Service{{
		Name:        "service1",
		PortLabel:   "1000",
		AddressMode: "",
		Address:     "127.0.0.1",
		Checks: []api.ServiceCheck{{
			Name:     "check1",
			Type:     "http",
			Path:     "/",
			Interval: 1 * time.Second,
			Timeout:  1 * time.Second,
		}},
		Provider: "nomad",
	}}
	return j
}

func testMultiRegionJob(jobID, region, datacenter string) *api.Job {
	task := api.NewTask("task1", "mock_driver").
		SetConfig("kill_after", "10s").
		SetConfig("run_for", "15s").
		SetConfig("exit_code", 0).
		Require(&api.Resources{
			MemoryMB: pointer.Of(256),
			CPU:      pointer.Of(100),
		}).
		SetLogConfig(&api.LogConfig{
			MaxFiles:      pointer.Of(1),
			MaxFileSizeMB: pointer.Of(2),
		})

	group := api.NewTaskGroup("group1", 1).
		AddTask(task).
		RequireDisk(&api.EphemeralDisk{
			SizeMB: pointer.Of(20),
		})

	job := api.NewServiceJob(jobID, jobID, region, 1).AddDatacenter(datacenter).AddTaskGroup(group)
	job.Region = nil
	job.Multiregion = &api.Multiregion{
		Regions: []*api.MultiregionRegion{
			{
				Name:        "east",
				Datacenters: []string{"east-1"},
			},
			{
				Name:        "west",
				Datacenters: []string{"west-1"},
			},
		},
	}

	return job
}

func waitForNodes(t *testing.T, client *api.Client) {
	testutil.WaitForResult(func() (bool, error) {
		nodes, _, err := client.Nodes().List(nil)
		if err != nil {
			return false, err
		}
		for _, node := range nodes {
			if _, ok := node.Drivers["mock_driver"]; ok &&
				node.Status == structs.NodeStatusReady {
				return true, nil
			}
		}
		return false, fmt.Errorf("no ready nodes")
	}, func(err error) {
		must.NoError(t, err)
	})
}

func waitForJobAllocsStatus(t *testing.T, client *api.Client, jobID string, status string, token string) {
	testutil.WaitForResult(func() (bool, error) {
		q := &api.QueryOptions{AuthToken: token}

		allocs, _, err := client.Jobs().Allocations(jobID, true, q)
		if err != nil {
			return false, fmt.Errorf("failed to query job allocs: %v", err)
		}
		if len(allocs) == 0 {
			return false, fmt.Errorf("no allocs")
		}

		for _, alloc := range allocs {
			if alloc.ClientStatus != status {
				return false, fmt.Errorf("alloc status is %q not %q", alloc.ClientStatus, status)
			}
		}
		return true, nil
	}, func(err error) {
		must.NoError(t, err)
	})
}

func waitForAllocStatus(t *testing.T, client *api.Client, allocID string, status string) {
	testutil.WaitForResult(func() (bool, error) {
		alloc, _, err := client.Allocations().Info(allocID, nil)
		if err != nil {
			return false, err
		}
		if alloc.ClientStatus == status {
			return true, nil
		}
		return false, fmt.Errorf("alloc status is %q not %q", alloc.ClientStatus, status)
	}, func(err error) {
		must.NoError(t, err)
	})
}

func waitForAllocRunning(t *testing.T, client *api.Client, allocID string) {
	waitForAllocStatus(t, client, allocID, api.AllocClientStatusRunning)
}

func waitForCheckStatus(t *testing.T, client *api.Client, allocID, status string) {
	testutil.WaitForResult(func() (bool, error) {
		results, err := client.Allocations().Checks(allocID, nil)
		if err != nil {
			return false, err
		}

		// pick a check, any check will do
		for _, check := range results {
			if check.Status == status {
				return true, nil
			}
		}

		return false, fmt.Errorf("no check with status: %s", status)
	}, func(err error) {
		t.Fatalf("timed out waiting for alloc to be running: %v", err)
	})
}

func getAllocFromJob(t *testing.T, client *api.Client, jobID string) string {
	var allocID string
	if allocations, _, err := client.Jobs().Allocations(jobID, false, nil); err == nil {
		if len(allocations) > 0 {
			allocID = allocations[0].ID
		}
	}
	must.NotEq(t, "", allocID, must.Sprint("expected to find an evaluation after running job", jobID))
	return allocID
}

func getTempFile(t *testing.T, name string) (string, func()) {
	f, err := os.CreateTemp("", name)
	must.NoError(t, err)
	must.NoError(t, f.Close())
	return f.Name(), func() {
		_ = os.Remove(f.Name())
	}
}
