//go:build linux

package reaper_test

import (
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"syscall"
	"testing"
	"time"

	"github.com/hashicorp/go-reap"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/agent/reaper"
	"github.com/coder/coder/v2/testutil"
)

// TestReap checks that's the reaper is successfully reaping
// exited processes and passing the PIDs through the shared
// channel.
//
//nolint:paralleltest
func TestReap(t *testing.T) {
	// Don't run the reaper test in CI. It does weird
	// things like forkexecing which may have unintended
	// consequences in CI.
	if testutil.InCI() {
		t.Skip("Detected CI, skipping reaper tests")
	}

	pids := make(reap.PidCh, 1)
	err := reaper.ForkReap(
		reaper.WithPIDCallback(pids),
		// Provide some argument that immediately exits.
		reaper.WithExecArgs("/bin/sh", "-c", "exit 0"),
	)
	require.NoError(t, err)

	cmd := exec.Command("tail", "-f", "/dev/null")
	err = cmd.Start()
	require.NoError(t, err)

	cmd2 := exec.Command("tail", "-f", "/dev/null")
	err = cmd2.Start()
	require.NoError(t, err)

	err = cmd.Process.Kill()
	require.NoError(t, err)

	err = cmd2.Process.Kill()
	require.NoError(t, err)

	expectedPIDs := []int{cmd.Process.Pid, cmd2.Process.Pid}

	for i := 0; i < len(expectedPIDs); i++ {
		select {
		case <-time.After(testutil.WaitShort):
			t.Fatalf("Timed out waiting for process")
		case pid := <-pids:
			require.Contains(t, expectedPIDs, pid)
		}
	}
}

//nolint:paralleltest // Signal handling.
func TestReapInterrupt(t *testing.T) {
	// Don't run the reaper test in CI. It does weird
	// things like forkexecing which may have unintended
	// consequences in CI.
	if testutil.InCI() {
		t.Skip("Detected CI, skipping reaper tests")
	}

	errC := make(chan error, 1)
	pids := make(reap.PidCh, 1)

	// Use signals to notify when the child process is ready for the
	//  next step of our test.
	usrSig := make(chan os.Signal, 1)
	signal.Notify(usrSig, syscall.SIGUSR1, syscall.SIGUSR2)
	defer signal.Stop(usrSig)

	go func() {
		errC <- reaper.ForkReap(
			reaper.WithPIDCallback(pids),
			reaper.WithCatchSignals(os.Interrupt),
			// Signal propagation does not extend to children of children, so
			// we create a little bash script to ensure sleep is interrupted.
			reaper.WithExecArgs("/bin/sh", "-c", fmt.Sprintf("pid=0; trap 'kill -USR2 %d; kill -TERM $pid' INT; sleep 10 &\npid=$!; kill -USR1 %d; wait", os.Getpid(), os.Getpid())),
		)
	}()

	require.Equal(t, <-usrSig, syscall.SIGUSR1)
	err := syscall.Kill(os.Getpid(), syscall.SIGINT)
	require.NoError(t, err)
	require.Equal(t, <-usrSig, syscall.SIGUSR2)

	require.NoError(t, <-errC)
}
