package configs_test

import (
	"encoding/json"
	"fmt"
	"os"
	"reflect"
	"testing"
	"time"

	"github.com/opencontainers/runc/libcontainer/configs"
	"github.com/opencontainers/runtime-spec/specs-go"
)

func TestUnmarshalHooks(t *testing.T) {
	timeout := time.Second

	hookCmd := configs.NewCommandHook(&configs.Command{
		Path:    "/var/vcap/hooks/hook",
		Args:    []string{"--pid=123"},
		Env:     []string{"FOO=BAR"},
		Dir:     "/var/vcap",
		Timeout: &timeout,
	})

	hookJson, err := json.Marshal(hookCmd)
	if err != nil {
		t.Fatal(err)
	}

	for _, hookName := range configs.HookNameList {
		hooks := configs.Hooks{}
		err = hooks.UnmarshalJSON(fmt.Appendf(nil, `{"%s" :[%s]}`, hookName, hookJson))
		if err != nil {
			t.Fatal(err)
		}

		if !reflect.DeepEqual(hooks[hookName], configs.HookList{hookCmd}) {
			t.Errorf("Expected %s to equal %+v but it was %+v", hookName, hookCmd, hooks[hookName])
		}
	}
}

func TestUnmarshalHooksWithInvalidData(t *testing.T) {
	hook := configs.Hooks{}
	err := hook.UnmarshalJSON([]byte(`{invalid-json}`))
	if err == nil {
		t.Error("Expected error to occur but it was nil")
	}
}

func TestMarshalHooks(t *testing.T) {
	timeout := time.Second

	hookCmd := configs.NewCommandHook(&configs.Command{
		Path:    "/var/vcap/hooks/hook",
		Args:    []string{"--pid=123"},
		Env:     []string{"FOO=BAR"},
		Dir:     "/var/vcap",
		Timeout: &timeout,
	})

	hook := configs.Hooks{
		configs.Prestart:        configs.HookList{hookCmd},
		configs.CreateRuntime:   configs.HookList{hookCmd},
		configs.CreateContainer: configs.HookList{hookCmd},
		configs.StartContainer:  configs.HookList{hookCmd},
		configs.Poststart:       configs.HookList{hookCmd},
		configs.Poststop:        configs.HookList{hookCmd},
	}
	hooks, err := hook.MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}

	// Note Marshal seems to output fields in alphabetical order
	hookCmdJson := `[{"path":"/var/vcap/hooks/hook","args":["--pid=123"],"env":["FOO=BAR"],"dir":"/var/vcap","timeout":1000000000}]`
	h := fmt.Sprintf(`{"createContainer":%[1]s,"createRuntime":%[1]s,"poststart":%[1]s,"poststop":%[1]s,"prestart":%[1]s,"startContainer":%[1]s}`, hookCmdJson)
	if string(hooks) != h {
		t.Errorf("Expected hooks %s to equal %s", string(hooks), h)
	}
}

func TestMarshalUnmarshalHooks(t *testing.T) {
	timeout := time.Second

	hookCmd := configs.NewCommandHook(&configs.Command{
		Path:    "/var/vcap/hooks/hook",
		Args:    []string{"--pid=123"},
		Env:     []string{"FOO=BAR"},
		Dir:     "/var/vcap",
		Timeout: &timeout,
	})

	hook := configs.Hooks{
		configs.Prestart:        configs.HookList{hookCmd},
		configs.CreateRuntime:   configs.HookList{hookCmd},
		configs.CreateContainer: configs.HookList{hookCmd},
		configs.StartContainer:  configs.HookList{hookCmd},
		configs.Poststart:       configs.HookList{hookCmd},
		configs.Poststop:        configs.HookList{hookCmd},
	}
	hooks, err := hook.MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}

	umMhook := configs.Hooks{}
	err = umMhook.UnmarshalJSON(hooks)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(umMhook, hook) {
		t.Errorf("Expected hooks to be equal after mashaling -> unmarshaling them: %+v, %+v", umMhook, hook)
	}
}

func TestMarshalHooksWithUnexpectedType(t *testing.T) {
	fHook := configs.NewFunctionHook(func(*specs.State) error {
		return nil
	})
	hook := configs.Hooks{
		configs.CreateRuntime: configs.HookList{fHook},
	}
	hooks, err := hook.MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}

	h := `{"createContainer":null,"createRuntime":null,"poststart":null,"poststop":null,"prestart":null,"startContainer":null}`
	if string(hooks) != h {
		t.Errorf("Expected hooks %s to equal %s", string(hooks), h)
	}
}

func TestFuncHookRun(t *testing.T) {
	state := &specs.State{
		Version: "1",
		ID:      "1",
		Status:  "created",
		Pid:     1,
		Bundle:  "/bundle",
	}

	fHook := configs.NewFunctionHook(func(s *specs.State) error {
		if !reflect.DeepEqual(state, s) {
			return fmt.Errorf("expected state %+v to equal %+v", state, s)
		}
		return nil
	})

	err := fHook.Run(state)
	if err != nil {
		t.Fatal(err)
	}
}

func TestCommandHookRun(t *testing.T) {
	state := &specs.State{
		Version: "1",
		ID:      "1",
		Status:  "created",
		Pid:     1,
		Bundle:  "/bundle",
	}

	stateJson, err := json.Marshal(state)
	if err != nil {
		t.Fatal(err)
	}

	verifyCommandTemplate := `#!/bin/sh
if [ "$1" != "testarg" ]; then
	echo "Bad value for $1. Expected 'testarg', found '$1'"
	exit 1
fi
if [ -z "$FOO" ] || [ "$FOO" != BAR ]; then
	echo "Bad value for FOO. Expected 'BAR', found '$FOO'"
	exit 1
fi
expectedJson=%q
read JSON
if [ "$JSON" != "$expectedJson" ]; then
	echo "Bad JSON received. Expected '$expectedJson', found '$JSON'"
	exit 1
fi
exit 0
	`
	verifyCommand := fmt.Sprintf(verifyCommandTemplate, stateJson)
	filename := "/tmp/runc-hooktest.sh"
	os.Remove(filename)
	if err := os.WriteFile(filename, []byte(verifyCommand), 0o700); err != nil {
		t.Fatalf("Failed to create tmp file: %v", err)
	}
	defer os.Remove(filename)

	cmdHook := configs.NewCommandHook(&configs.Command{
		Path: filename,
		Args: []string{filename, "testarg"},
		Env:  []string{"FOO=BAR"},
		Dir:  "/",
	})

	if err := cmdHook.Run(state); err != nil {
		t.Errorf("Want no error, got: %+v", err)
	}
}

func TestCommandHookRunTimeout(t *testing.T) {
	state := &specs.State{
		Version: "1",
		ID:      "1",
		Status:  "created",
		Pid:     1,
		Bundle:  "/bundle",
	}
	timeout := 100 * time.Millisecond

	cmdHook := configs.NewCommandHook(&configs.Command{
		Path:    "/bin/sleep",
		Args:    []string{"/bin/sleep", "1"},
		Timeout: &timeout,
	})

	if err := cmdHook.Run(state); err == nil {
		t.Error("Expected error to occur but it was nil")
	}
}
