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

package e2eutil

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	api "github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/e2e/framework"
	"github.com/hashicorp/nomad/helper/discover"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/testutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type e2eJob struct {
	framework.TC
	jobfile string
	jobID   string
}

func (j *e2eJob) Name() string {
	return filepath.Base(j.jobfile)
}

// Ensure cluster has leader and at least 1 client node
// in a ready state before running tests
func (j *e2eJob) BeforeAll(f *framework.F) {
	WaitForLeader(f.T(), j.Nomad())
	WaitForNodesReady(f.T(), j.Nomad(), 1)
	j.jobID = "e2eutil-" + uuid.Generate()[0:8]
}

func (j *e2eJob) TestJob(f *framework.F) {
	file, err := os.Open(j.jobfile)
	t := f.T()
	require.NoError(t, err)

	scanner := bufio.NewScanner(file)
	var e2eJobLine string
	for scanner.Scan() {
		if strings.HasPrefix(scanner.Text(), "//e2e:") {
			e2eJobLine = scanner.Text()[6:]
		}
		require.NoError(t, scanner.Err())
	}

	switch {
	case strings.HasPrefix(e2eJobLine, "batch"):
		parseBatchJobLine(t, j, e2eJobLine).Run(f)
	case strings.HasPrefix(e2eJobLine, "service"):
		parseServiceJobLine(t, j, e2eJobLine).Run(f)
	default:
		require.Fail(t, "could not parse e2e job line: %q", e2eJobLine)
	}
}

type e2eBatchJob struct {
	*e2eJob

	shouldFail bool
}

func (j *e2eBatchJob) Run(f *framework.F) {
	t := f.T()
	require := require.New(t)
	nomadClient := j.Nomad()

	allocs := RegisterAndWaitForAllocs(f.T(), nomadClient, j.jobfile, j.jobID, "")
	require.Equal(1, len(allocs))
	allocID := allocs[0].ID

	// wait for the job to stop
	WaitForAllocStopped(t, nomadClient, allocID)
	alloc, _, err := nomadClient.Allocations().Info(allocID, nil)
	require.NoError(err)
	if j.shouldFail {
		require.NotEqual(structs.AllocClientStatusComplete, alloc.ClientStatus)
	} else {
		require.Equal(structs.AllocClientStatusComplete, alloc.ClientStatus)
	}
}

type e2eServiceJob struct {
	*e2eJob

	script          string
	runningDuration time.Duration
}

func (j *e2eServiceJob) Run(f *framework.F) {
	t := f.T()
	nomadClient := j.Nomad()

	allocs := RegisterAndWaitForAllocs(f.T(), nomadClient, j.jobfile, j.jobID, "")
	require.Equal(t, 1, len(allocs))
	allocID := allocs[0].ID

	var alloc *api.Allocation
	WaitForAllocRunning(t, nomadClient, allocID)
	testutil.AssertUntil(j.runningDuration, func() (bool, error) {
		var err error
		alloc, _, err = nomadClient.Allocations().Info(allocID, nil)
		if err != nil {
			return false, err
		}

		return alloc.ClientStatus == structs.AllocClientStatusRunning, fmt.Errorf("expected status running, but was: %s", alloc.ClientStatus)
	}, func(err error) {
		require.NoError(t, err, "failed to keep alloc running")
	})

	scriptPath := filepath.Join(filepath.Dir(j.jobfile), j.script)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	cmd := exec.CommandContext(ctx, scriptPath)
	nmdBin, err := discover.NomadExecutable()
	assert.NoError(t, err)
	cmd.Env = append(os.Environ(),
		"NOMAD_BIN="+nmdBin,
		"NOMAD_ALLOC_ID="+allocID,
		"NOMAD_ADDR="+nomadClient.Address(),
	)

	assert.NoError(t, cmd.Start())
	waitCh := make(chan error)
	go func() {
		select {
		case waitCh <- cmd.Wait():
		case <-ctx.Done():
		}
	}()

	select {
	case <-ctx.Done():
	case err := <-waitCh:
		assert.NoError(t, err)
		assert.Zero(t, cmd.ProcessState.ExitCode())
	}

	// stop the job
	_, _, err = nomadClient.Jobs().Deregister(j.jobID, false, nil)
	require.NoError(t, err)
	WaitForAllocStopped(t, nomadClient, allocID)
}

//e2e:batch fail=false
//e2e:service running=5s check=script.sh

func NewE2EJob(jobfile string) framework.TestCase {
	return &e2eJob{
		jobfile: jobfile,
	}

}

func parseServiceJobLine(t *testing.T, j *e2eJob, line string) *e2eServiceJob {
	job := &e2eServiceJob{
		e2eJob:          j,
		runningDuration: time.Second * 5,
	}
	for _, options := range strings.Split(line, " ")[1:] {
		o := strings.SplitN(options, "=", 2)
		switch o[0] {
		case "script":
			job.script = o[1]
		case "running":
			dur, err := time.ParseDuration(o[1])
			if err != nil {
				t.Logf("could not parse running duration %q for e2e job spec: %v", o[1], err)
			} else {
				job.runningDuration = dur
			}
		}
	}

	return job
}

func parseBatchJobLine(t *testing.T, j *e2eJob, line string) *e2eBatchJob {
	job := &e2eBatchJob{
		e2eJob: j,
	}
	for _, options := range strings.Split(line, " ")[1:] {
		o := strings.SplitN(options, "=", 2)
		switch o[0] {
		case "shouldFail":
			job.shouldFail, _ = strconv.ParseBool(o[1])
		}
	}

	return job
}
