package agentproc_test

import (
	"runtime"
	"syscall"
	"testing"

	"github.com/spf13/afero"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/agent/agentproc"
	"github.com/coder/coder/v2/agent/agentproc/agentproctest"
)

func TestList(t *testing.T) {
	t.Parallel()

	if runtime.GOOS != "linux" {
		t.Skipf("skipping non-linux environment")
	}

	t.Run("OK", func(t *testing.T) {
		t.Parallel()

		var (
			fs            = afero.NewMemMapFs()
			sc            = agentproctest.NewMockSyscaller(gomock.NewController(t))
			expectedProcs = make(map[int32]agentproc.Process)
		)

		for i := 0; i < 4; i++ {
			proc := agentproctest.GenerateProcess(t, fs)
			expectedProcs[proc.PID] = proc

			sc.EXPECT().
				Kill(proc.PID, syscall.Signal(0)).
				Return(nil)
		}

		actualProcs, err := agentproc.List(fs, sc)
		require.NoError(t, err)
		require.Len(t, actualProcs, len(expectedProcs))
		for _, proc := range actualProcs {
			expected, ok := expectedProcs[proc.PID]
			require.True(t, ok)
			require.Equal(t, expected.PID, proc.PID)
			require.Equal(t, expected.CmdLine, proc.CmdLine)
			require.Equal(t, expected.Dir, proc.Dir)
		}
	})

	t.Run("FinishedProcess", func(t *testing.T) {
		t.Parallel()

		var (
			fs            = afero.NewMemMapFs()
			sc            = agentproctest.NewMockSyscaller(gomock.NewController(t))
			expectedProcs = make(map[int32]agentproc.Process)
		)

		for i := 0; i < 3; i++ {
			proc := agentproctest.GenerateProcess(t, fs)
			expectedProcs[proc.PID] = proc

			sc.EXPECT().
				Kill(proc.PID, syscall.Signal(0)).
				Return(nil)
		}

		// Create a process that's already finished. We're not adding
		// it to the map because it should be skipped over.
		proc := agentproctest.GenerateProcess(t, fs)
		sc.EXPECT().
			Kill(proc.PID, syscall.Signal(0)).
			Return(xerrors.New("os: process already finished"))

		actualProcs, err := agentproc.List(fs, sc)
		require.NoError(t, err)
		require.Len(t, actualProcs, len(expectedProcs))
		for _, proc := range actualProcs {
			expected, ok := expectedProcs[proc.PID]
			require.True(t, ok)
			require.Equal(t, expected.PID, proc.PID)
			require.Equal(t, expected.CmdLine, proc.CmdLine)
			require.Equal(t, expected.Dir, proc.Dir)
		}
	})

	t.Run("NoSuchProcess", func(t *testing.T) {
		t.Parallel()

		var (
			fs            = afero.NewMemMapFs()
			sc            = agentproctest.NewMockSyscaller(gomock.NewController(t))
			expectedProcs = make(map[int32]agentproc.Process)
		)

		for i := 0; i < 3; i++ {
			proc := agentproctest.GenerateProcess(t, fs)
			expectedProcs[proc.PID] = proc

			sc.EXPECT().
				Kill(proc.PID, syscall.Signal(0)).
				Return(nil)
		}

		// Create a process that doesn't exist. We're not adding
		// it to the map because it should be skipped over.
		proc := agentproctest.GenerateProcess(t, fs)
		sc.EXPECT().
			Kill(proc.PID, syscall.Signal(0)).
			Return(syscall.ESRCH)

		actualProcs, err := agentproc.List(fs, sc)
		require.NoError(t, err)
		require.Len(t, actualProcs, len(expectedProcs))
		for _, proc := range actualProcs {
			expected, ok := expectedProcs[proc.PID]
			require.True(t, ok)
			require.Equal(t, expected.PID, proc.PID)
			require.Equal(t, expected.CmdLine, proc.CmdLine)
			require.Equal(t, expected.Dir, proc.Dir)
		}
	})
}

// These tests are not very interesting but they provide some modicum of
// confidence.
func TestProcess(t *testing.T) {
	t.Parallel()

	if runtime.GOOS != "linux" {
		t.Skipf("skipping non-linux environment")
	}

	t.Run("SetNiceness", func(t *testing.T) {
		t.Parallel()

		var (
			sc   = agentproctest.NewMockSyscaller(gomock.NewController(t))
			proc = &agentproc.Process{
				PID: 32,
			}
			score = 20
		)

		sc.EXPECT().SetPriority(proc.PID, score).Return(nil)
		err := proc.SetNiceness(sc, score)
		require.NoError(t, err)
	})

	t.Run("Cmd", func(t *testing.T) {
		t.Parallel()

		var (
			proc = &agentproc.Process{
				CmdLine: "helloworld\x00--arg1\x00--arg2",
			}
			expectedName = "helloworld --arg1 --arg2"
		)

		require.Equal(t, expectedName, proc.Cmd())
	})
}
