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

package command

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/testutil"
	"github.com/mitchellh/cli"
	"github.com/posener/complete"
	"github.com/shoenig/test/must"
)

// static check
var _ cli.Command = &AllocExecCommand{}

func TestAllocExecCommand_Fails(t *testing.T) {
	ci.Parallel(t)
	srv, client, url := testServer(t, true, nil)
	defer srv.Shutdown()

	cases := []struct {
		name          string
		args          []string
		expectedError string
	}{
		{
			"alloc id missing",
			[]string{},
			`An allocation ID is required`,
		},
		{
			"alloc id too short",
			[]string{"-address=" + url, "2", "/bin/bash"},
			`Alloc ID must contain at least two characters`,
		},
		{
			"alloc not found",
			[]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
			`No allocation(s) with prefix or id "26470238-5CF2-438F-8772-DC67CFB0705C"`,
		},
		{
			"alloc not found with odd-length prefix",
			[]string{"-address=" + url, "26470238-5CF", "/bin/bash"},
			`No allocation(s) with prefix or id "26470238-5CF"`,
		},
		{
			"job id missing",
			[]string{"-job"},
			`A job ID is required`,
		},
		{
			"job not found",
			[]string{"-address=" + url, "-job", "example", "/bin/bash"},
			`No job(s) with prefix or ID "example" found`,
		},
		{
			"command missing",
			[]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C"},
			`A command is required`,
		},
		{
			"connection failure",
			[]string{"-address=nope", "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
			`Error querying allocation`,
		},
		{
			"escape char too long",
			[]string{"-address=" + url, "-e", "es", "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
			`-e requires 'none' or a single character`,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			cmd := &AllocExecCommand{Meta: Meta{Ui: ui}}

			code := cmd.Run(c.args)
			must.One(t, code)

			out := ui.ErrorWriter.String()
			must.StrContains(t, out, c.expectedError)

			ui.ErrorWriter.Reset()
			ui.OutputWriter.Reset()
		})
	}

	// Wait for a node to be ready
	waitForNodes(t, client)

	t.Run("non existent task", func(t *testing.T) {
		ui := cli.NewMockUi()
		cmd := &AllocExecCommand{Meta: Meta{Ui: ui}}

		jobID := "job1_sfx"
		job1 := testJob(jobID)

		resp, _, err := client.Jobs().Register(job1, nil)
		must.NoError(t, err)

		code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
		must.Zero(t, code)

		// get an alloc id
		allocId1 := ""
		if allocs, _, err := client.Jobs().Allocations(jobID, false, nil); err == nil {
			if len(allocs) > 0 {
				allocId1 = allocs[0].ID
			}
		}
		must.NotEq(t, "", allocId1)

		// by alloc
		code = cmd.Run([]string{"-address=" + url, "-task=nonexistenttask1", allocId1, "/bin/bash"})
		must.One(t, code)

		out := ui.ErrorWriter.String()
		must.StrContains(t, out, "Could not find task named: nonexistenttask1")

		ui.ErrorWriter.Reset()

		// by jobID
		code = cmd.Run([]string{"-address=" + url, "-task=nonexistenttask2", "-job", jobID, "/bin/bash"})
		must.One(t, code)

		out = ui.ErrorWriter.String()
		must.StrContains(t, out, "Could not find task named: nonexistenttask2")

		ui.ErrorWriter.Reset()
	})

}

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

	srv, _, url := testServer(t, true, nil)
	defer srv.Shutdown()

	ui := cli.NewMockUi()
	cmd := &AllocExecCommand{Meta: Meta{Ui: ui, flagAddress: url}}

	// Create a fake alloc
	state := srv.Agent.Server().State()
	a := mock.Alloc()
	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{a}))

	prefix := a.ID[:5]
	args := complete.Args{Last: prefix}
	predictor := cmd.AutocompleteArgs()

	res := predictor.Predict(args)
	must.Len(t, 1, res)
	must.Eq(t, a.ID, res[0])
}

func TestAllocExecCommand_Run(t *testing.T) {
	ci.Parallel(t)
	srv, client, url := testServer(t, true, nil)
	defer srv.Shutdown()

	// Wait for a node to be ready
	waitForNodes(t, client)

	jobID := uuid.Generate()
	job := testJob(jobID)
	job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{
		"run_for": "10s",
		"exec_command": map[string]interface{}{
			"run_for":       "1ms",
			"exit_code":     21,
			"stdout_string": "sample stdout output\n",
			"stderr_string": "sample stderr output\n",
		},
	}
	resp, _, err := client.Jobs().Register(job, nil)
	must.NoError(t, err)

	evalUi := cli.NewMockUi()
	code := waitForSuccess(evalUi, client, fullId, t, resp.EvalID)
	must.Zero(t, code)

	allocId := ""

	testutil.WaitForResult(func() (bool, error) {
		allocs, _, err := client.Jobs().Allocations(jobID, false, nil)
		if err != nil {
			return false, fmt.Errorf("failed to get allocations: %v", err)
		}

		if len(allocs) < 0 {
			return false, fmt.Errorf("no allocations yet")
		}

		alloc := allocs[0]
		if alloc.ClientStatus != "running" {
			return false, fmt.Errorf("alloc is not running yet: %v", alloc.ClientStatus)
		}

		allocId = alloc.ID
		return true, nil
	}, func(err error) { must.NoError(t, err) })

	cases := []struct {
		name    string
		command string
		stdin   string

		stdout   string
		stderr   string
		exitCode int
	}{
		{
			name:     "basic stdout/err",
			command:  "simplecommand",
			stdin:    "",
			stdout:   "sample stdout output",
			stderr:   "sample stderr output",
			exitCode: 21,
		},
		{
			name:     "notty: streamining input",
			command:  "showinput",
			stdin:    "hello from stdin",
			stdout:   "TTY: false\nStdin:\nhello from stdin",
			exitCode: 0,
		},
	}

	for _, c := range cases {
		t.Run("by id: "+c.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			var stdout, stderr bufferCloser

			cmd := &AllocExecCommand{
				Meta:   Meta{Ui: ui},
				Stdin:  strings.NewReader(c.stdin),
				Stdout: &stdout,
				Stderr: &stderr,
			}

			code = cmd.Run([]string{"-address=" + url, allocId, c.command})
			must.Eq(t, c.exitCode, code)
			must.Eq(t, c.stdout, strings.TrimSpace(stdout.String()))
			must.Eq(t, c.stderr, strings.TrimSpace(stderr.String()))
		})
		t.Run("by job: "+c.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			var stdout, stderr bufferCloser

			cmd := &AllocExecCommand{
				Meta:   Meta{Ui: ui},
				Stdin:  strings.NewReader(c.stdin),
				Stdout: &stdout,
				Stderr: &stderr,
			}

			code = cmd.Run([]string{"-address=" + url, "-job", jobID, c.command})
			must.Eq(t, c.exitCode, code)
			must.Eq(t, c.stdout, strings.TrimSpace(stdout.String()))
			must.Eq(t, c.stderr, strings.TrimSpace(stderr.String()))
		})
	}
}

type bufferCloser struct {
	bytes.Buffer
}

func (b *bufferCloser) Close() error {
	return nil
}
