// Copyright 2025 The Inspektor Gadget authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tests

import (
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"syscall"
	"testing"
	"time"

	"github.com/cilium/ebpf"
	"github.com/stretchr/testify/require"

	gadgettesting "github.com/inspektor-gadget/inspektor-gadget/gadgets/testing"
	traceexec "github.com/inspektor-gadget/inspektor-gadget/gadgets/trace_exec/consts"
	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-service/api"
	"github.com/inspektor-gadget/inspektor-gadget/pkg/operators"
	"github.com/inspektor-gadget/inspektor-gadget/pkg/testing/gadgetrunner"
	"github.com/inspektor-gadget/inspektor-gadget/pkg/testing/utils"
)

type ExpectedTraceExecEvent struct {
	Proc   utils.Process `json:"proc"`
	Error  string        `json:"error"`
	Args   string        `json:"args"`
	Ctime  uint64        `json:"ctime"`
	Fctime uint64        `json:"fctime"`
	Pctime uint64        `json:"pctime"`
}

type testDef struct {
	runnerConfig   *utils.RunnerConfig
	mntnsFilterMap func(info *utils.RunnerInfo) *ebpf.Map
	argv           []string
	runFromThread  bool
	validate       func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string)
}

func TestTraceExecGadget(t *testing.T) {
	gadgettesting.InitUnitTest(t)
	testCases := map[string]testDef{
		"simple_executable": {
			runnerConfig: &utils.RunnerConfig{},
			argv:         []string{"/bin/echo", "hello", "world"},
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				require.Len(t, events, 1, "Expected 1 event but got %d", len(events))
				expectedArgs := strings.Join(inputArgs, traceexec.ArgsSeparator)
				require.Equal(t, expectedArgs, events[0].Args)
			},
		},
		"large_argument_list": {
			runnerConfig: &utils.RunnerConfig{},
			// should only capture TOTAL_ARGS_SIZE ~ 20 arguments
			argv: []string{"/bin/echo", "arg1", "arg2", "arg3", "arg4", "arg5", "arg6", "arg7", "arg8", "arg9", "arg10", "arg11", "arg12", "arg13", "arg14", "arg15", "arg16", "arg17", "arg18", "arg19", "arg20", "arg21"},
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				require.Len(t, events, 1, "Expected 1 event but got %d", len(events))
				expectedArgs := strings.Join(inputArgs[:20], traceexec.ArgsSeparator)
				require.Equal(t, expectedArgs, events[0].Args)
			},
		},
		"uid_gid": {
			runnerConfig: &utils.RunnerConfig{
				Uid:         1000,
				Gid:         1000,
				HostNetwork: false,
			},
			argv: []string{"/bin/ls", "-l", "/"},
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				require.Len(t, events, 1, "Expected 1 event but got %d", len(events))
				expectedArgs := strings.Join(inputArgs, traceexec.ArgsSeparator)
				require.Equal(t, expectedArgs, events[0].Args)
				require.Equal(t, uint32(info.Uid), events[0].Proc.Creds.Uid)
				require.Equal(t, uint32(info.Gid), events[0].Proc.Creds.Gid)
			},
		},
		"mount_namespace_filter_blocked": {
			runnerConfig: &utils.RunnerConfig{},
			mntnsFilterMap: func(info *utils.RunnerInfo) *ebpf.Map {
				return utils.CreateMntNsFilterMap(t, info.MountNsID+100)
			},
			argv: []string{"/bin/date"},
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				utils.ExpectNoEvent(t, info, "Expected 0 events", events)
			},
		},
		"error": {
			runnerConfig: &utils.RunnerConfig{},
			argv:         []string{"/bin/foobar", "hello"},
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				require.Len(t, events, 1, "Expected 1 event but got %d", len(events))
				expectedArgs := strings.Join(inputArgs, traceexec.ArgsSeparator)
				require.Equal(t, expectedArgs, events[0].Args)
				require.Equal(t, "ENOENT", events[0].Error)
			},
		},
		"ctime": {
			runnerConfig: &utils.RunnerConfig{},
			argv:         []string{"/bin/echo", "ctime-test"},
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				require.Len(t, events, 1, "Expected 1 event but got %d", len(events))

				// Get the actual ctime of /bin/echo via stat
				expectedCtime := getCtimeNs(t, "/bin/echo")

				// ctime and fctime should both match /bin/echo's ctime
				// (for a non-script binary, exe and file are the same)
				require.Equal(t, expectedCtime, events[0].Ctime, "ctime should match stat ctime of /bin/echo")
				require.Equal(t, expectedCtime, events[0].Fctime, "fctime should match stat ctime of /bin/echo")

				// pctime: parent exe ctime should be non-zero
				require.NotZero(t, events[0].Pctime, "pctime should be non-zero")
			},
		},
		"successful_exec_from_thread": {
			runnerConfig:  &utils.RunnerConfig{},
			argv:          []string{"/bin/echo", "hello", "world"},
			runFromThread: true,
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				require.Len(t, events, 2, "Expected 2 events but got %d", len(events))
				// We do not check the full path of the executable/symlink here, as it may vary depending on the environment.
				require.Contains(t, events[0].Args, "/bin/python3"+traceexec.ArgsSeparator+"-c")
				expectedArgs := strings.Join(inputArgs, traceexec.ArgsSeparator)
				require.Equal(t, expectedArgs, events[1].Args)
			},
		},
		"failed_exec_from_thread": {
			runnerConfig:  &utils.RunnerConfig{},
			argv:          []string{"/bin/meowmeow", "hello", "world"},
			runFromThread: true,
			validate: func(t *testing.T, info *utils.RunnerInfo, events []ExpectedTraceExecEvent, inputArgs []string) {
				require.Len(t, events, 2, "Expected 2 events but got %d", len(events))
				// We do not check the full path of the executable/symlink here, as it may vary depending on the environment.
				require.Contains(t, events[0].Args, "/bin/python3"+traceexec.ArgsSeparator+"-c")
				expectedArgs := strings.Join(inputArgs, traceexec.ArgsSeparator)
				require.Equal(t, expectedArgs, events[1].Args)
				require.Equal(t, "ENOENT", events[1].Error)
			},
		},
	}

	for name, testCase := range testCases {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			runner := utils.NewRunnerWithTest(t, testCase.runnerConfig)
			var mntnsFilterMap *ebpf.Map
			if testCase.mntnsFilterMap == nil {
				// by default we use the mount namespace of the runner
				mntnsFilterMap = utils.CreateMntNsFilterMap(t, runner.Info.MountNsID)
			} else {
				mntnsFilterMap = testCase.mntnsFilterMap(runner.Info)
			}
			onGadgetRun := func(gadgetCtx operators.GadgetContext) error {
				utils.RunWithRunner(t, runner, func() error {
					if testCase.runFromThread {
						generateEventFromThread(t, testCase.argv)
					} else {
						p, err := os.StartProcess(testCase.argv[0], testCase.argv, &os.ProcAttr{})
						if err == nil {
							defer p.Wait()
						}
					}
					return nil
				})
				return nil
			}
			opts := gadgetrunner.GadgetRunnerOpts[ExpectedTraceExecEvent]{
				Image:   "trace_exec",
				Timeout: 5 * time.Second,
				ParamValues: api.ParamValues{
					"operator.oci.ebpf.ignore-failed": "false",
				},
				MntnsFilterMap: mntnsFilterMap,
				OnGadgetRun:    onGadgetRun,
			}
			gadgetRunner := gadgetrunner.NewGadgetRunner(t, opts)

			gadgetRunner.RunGadget()
			testCase.validate(t, runner.Info, gadgetRunner.CapturedEvents, testCase.argv)
		})
	}
}

func generateEventFromThread(t *testing.T, argv []string) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	args := "["
	for i, arg := range argv {
		if i > 0 {
			args += ", "
		}
		args += `"` + arg + `"`
	}
	args += "]"

	script := fmt.Sprintf(`
import threading
import os

def exec():
    os.execve("%s", %v, {})

def main():
    thread = threading.Thread(target=exec)
    thread.start()
    thread.join()

if __name__ == "__main__":
    main()
`, argv[0], args)
	cmd := exec.Command("python3", "-c", script)
	err := cmd.Run()
	if err != nil {
		// python3 is not available
		t.Skip("Skipping test, python3 is needed to run the test")
	}
}

func getCtimeNs(t *testing.T, path string) uint64 {
	t.Helper()
	var stat syscall.Stat_t
	err := syscall.Stat(path, &stat)
	require.NoError(t, err, "stat %s", path)
	return uint64(stat.Ctim.Sec)*1_000_000_000 + uint64(stat.Ctim.Nsec)
}
