package integration

import (
	"bytes"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"github.com/opencontainers/runc/libcontainer"
	"golang.org/x/sys/unix"
)

func criuFeature(feature string) bool {
	return exec.Command("criu", "check", "--feature", feature).Run() == nil
}

func TestUsernsCheckpoint(t *testing.T) {
	testCheckpoint(t, true)
}

func TestCheckpoint(t *testing.T) {
	testCheckpoint(t, false)
}

func testCheckpoint(t *testing.T, userns bool) {
	if testing.Short() {
		return
	}

	if _, err := exec.LookPath("criu"); err != nil {
		t.Skipf("criu binary not found: %v", err)
	}

	// Workaround for https://github.com/opencontainers/runc/issues/3532.
	out, err := exec.Command("rpm", "-q", "criu").CombinedOutput()
	if err == nil && regexp.MustCompile(`^criu-3\.17-[123]\.el9`).Match(out) {
		t.Skip("Test requires criu >= 3.17-4 on CentOS Stream 9.")
	}

	if userns && !criuFeature("userns") {
		t.Skip("Test requires userns")
	}

	config := newTemplateConfig(t, &tParam{userns: userns})
	stateDir := t.TempDir()

	container, err := libcontainer.Create(stateDir, "test", config)
	ok(t, err)
	defer destroyContainer(container)

	stdinR, stdinW, err := os.Pipe()
	ok(t, err)

	var stdout bytes.Buffer

	pconfig := libcontainer.Process{
		Cwd:    "/",
		Args:   []string{"cat"},
		Env:    standardEnvironment,
		Stdin:  stdinR,
		Stdout: &stdout,
		Init:   true,
	}

	err = container.Run(&pconfig)
	_ = stdinR.Close()
	defer stdinW.Close() //nolint: errcheck
	ok(t, err)

	pid, err := pconfig.Pid()
	ok(t, err)

	process, err := os.FindProcess(pid)
	ok(t, err)

	tmp := t.TempDir()
	var parentImage string

	// Test pre-dump if mem_dirty_track is available.
	if criuFeature("mem_dirty_track") {
		parentImage = "../criu-parent"
		parentDir := filepath.Join(tmp, "criu-parent")
		preDumpOpts := &libcontainer.CriuOpts{
			ImagesDirectory: parentDir,
			WorkDirectory:   parentDir,
			PreDump:         true,
		}

		if err := container.Checkpoint(preDumpOpts); err != nil {
			t.Fatal(err)
		}

		state, err := container.Status()
		ok(t, err)

		if state != libcontainer.Running {
			t.Fatal("Unexpected preDump state: ", state)
		}
	}

	imagesDir := filepath.Join(tmp, "criu")

	checkpointOpts := &libcontainer.CriuOpts{
		ImagesDirectory: imagesDir,
		WorkDirectory:   imagesDir,
		ParentImage:     parentImage,
	}

	if err := container.Checkpoint(checkpointOpts); err != nil {
		t.Fatal(err)
	}

	state, err := container.Status()
	ok(t, err)

	if state != libcontainer.Stopped {
		t.Fatal("Unexpected state checkpoint: ", state)
	}

	_ = stdinW.Close()
	_, err = process.Wait()
	ok(t, err)

	// reload the container
	container, err = libcontainer.Load(stateDir, "test")
	ok(t, err)

	restoreStdinR, restoreStdinW, err := os.Pipe()
	ok(t, err)

	var restoreStdout bytes.Buffer
	restoreProcessConfig := &libcontainer.Process{
		Cwd:    "/",
		Stdin:  restoreStdinR,
		Stdout: &restoreStdout,
		Init:   true,
	}

	err = container.Restore(restoreProcessConfig, checkpointOpts)
	_ = restoreStdinR.Close()
	defer restoreStdinW.Close() //nolint: errcheck
	if err != nil {
		t.Fatal(err)
	}

	state, err = container.Status()
	ok(t, err)
	if state != libcontainer.Running {
		t.Fatal("Unexpected restore state: ", state)
	}

	pid, err = restoreProcessConfig.Pid()
	ok(t, err)

	err = unix.Kill(pid, 0)
	ok(t, err)

	_, err = restoreStdinW.WriteString("Hello!")
	ok(t, err)

	_ = restoreStdinW.Close()
	waitProcess(restoreProcessConfig, t)

	output := restoreStdout.String()
	if !strings.Contains(output, "Hello!") {
		t.Fatal("Did not restore the pipe correctly:", output)
	}
}