package systemd

import (
	"os"
	"os/exec"
	"strings"
	"testing"

	"golang.org/x/sys/unix"

	"github.com/opencontainers/runc/libcontainer/cgroups"
	"github.com/opencontainers/runc/libcontainer/configs"
)

func TestFreezeBeforeSet(t *testing.T) {
	requireV1(t)

	testCases := []struct {
		desc string
		// Test input.
		cg        *configs.Cgroup
		preFreeze bool
		// Expected values.
		// Before unit creation (Apply).
		freeze0, thaw0 bool
		// After unit creation.
		freeze1, thaw1 bool
	}{
		{
			// A slice with SkipDevices.
			desc: "slice,skip-devices",
			cg: &configs.Cgroup{
				Name:   "system-runc_test_freeze_1.slice",
				Parent: "system.slice",
				Resources: &configs.Resources{
					SkipDevices: true,
				},
			},
			// Expected.
			freeze0: false,
			thaw0:   false,
			freeze1: false,
			thaw1:   false,
		},
		{
			// A scope with SkipDevices. Not a realistic scenario with runc
			// (as container can't have SkipDevices == true), but possible
			// for a standalone cgroup manager.
			desc: "scope,skip-devices",
			cg: &configs.Cgroup{
				ScopePrefix: "test",
				Name:        "testFreeze2",
				Parent:      "system.slice",
				Resources: &configs.Resources{
					SkipDevices: true,
				},
			},
			// Expected.
			freeze0: false,
			thaw0:   false,
			freeze1: false,
			thaw1:   false,
		},
		{
			// A slice that is about to be frozen in Set.
			desc: "slice,will-freeze",
			cg: &configs.Cgroup{
				Name:   "system-runc_test_freeze_3.slice",
				Parent: "system.slice",
				Resources: &configs.Resources{
					Freezer: configs.Frozen,
				},
			},
			// Expected.
			freeze0: true,
			thaw0:   false,
			freeze1: true,
			thaw1:   false,
		},
		{
			// A pre-frozen slice that should stay frozen.
			desc: "slice,pre-frozen,will-freeze",
			cg: &configs.Cgroup{
				Name:   "system-runc_test_freeze_4.slice",
				Parent: "system.slice",
				Resources: &configs.Resources{
					Freezer: configs.Frozen,
				},
			},
			preFreeze: true,
			// Expected.
			freeze0: true, // not actually frozen yet.
			thaw0:   false,
			freeze1: false,
			thaw1:   false,
		},
		{
			// A pre-frozen scope with skip devices set.
			desc: "scope,pre-frozen,skip-devices",
			cg: &configs.Cgroup{
				ScopePrefix: "test",
				Name:        "testFreeze5",
				Parent:      "system.slice",
				Resources: &configs.Resources{
					SkipDevices: true,
				},
			},
			preFreeze: true,
			// Expected.
			freeze0: false,
			thaw0:   false,
			freeze1: false,
			thaw1:   false,
		},
		{
			// A pre-frozen scope which will be thawed.
			desc: "scope,pre-frozen",
			cg: &configs.Cgroup{
				ScopePrefix: "test",
				Name:        "testFreeze6",
				Parent:      "system.slice",
				Resources:   &configs.Resources{},
			},
			preFreeze: true,
			// Expected.
			freeze0: true, // not actually frozen yet.
			thaw0:   true,
			freeze1: false,
			thaw1:   false,
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.desc, func(t *testing.T) {
			m := NewLegacyManager(tc.cg, nil)
			defer m.Destroy() //nolint:errcheck
			lm := m.(*legacyManager)

			// Checks for a non-existent unit.
			freeze, thaw, err := lm.freezeBeforeSet(getUnitName(tc.cg), tc.cg.Resources)
			if err != nil {
				t.Fatal(err)
			}
			if freeze != tc.freeze0 || thaw != tc.thaw0 {
				t.Errorf("before Apply (non-existent unit): expected freeze: %v, thaw: %v, got freeze: %v, thaw: %v",
					tc.freeze0, tc.thaw0, freeze, thaw)
			}

			// Create systemd unit.
			pid := -1
			if strings.HasSuffix(getUnitName(tc.cg), ".scope") {
				// Scopes require a process inside.
				cmd := exec.Command("bash", "-c", "sleep 1m")
				if err := cmd.Start(); err != nil {
					t.Fatal(err)
				}
				pid = cmd.Process.Pid
				// Make sure to not leave a zombie.
				defer func() {
					// These may fail, we don't care.
					_ = cmd.Process.Kill()
					_ = cmd.Wait()
				}()
			}
			if err := m.Apply(pid); err != nil {
				t.Fatal(err)
			}
			if tc.preFreeze {
				if err := m.Freeze(configs.Frozen); err != nil {
					t.Error(err)
					return // no more checks
				}
			}
			freeze, thaw, err = lm.freezeBeforeSet(getUnitName(tc.cg), tc.cg.Resources)
			if err != nil {
				t.Error(err)
				return // no more checks
			}
			if freeze != tc.freeze1 || thaw != tc.thaw1 {
				t.Errorf("expected freeze: %v, thaw: %v, got freeze: %v, thaw: %v",
					tc.freeze1, tc.thaw1, freeze, thaw)
			}
			// Destroy() timeouts on a frozen container, so we need to thaw it.
			if tc.preFreeze {
				if err := m.Freeze(configs.Thawed); err != nil {
					t.Error(err)
				}
			}
			// Destroy() does not kill processes in cgroup, so we should.
			if pid != -1 {
				if err = unix.Kill(pid, unix.SIGKILL); err != nil {
					t.Errorf("unable to kill pid %d: %s", pid, err)
				}
			}
			// Not really needed, but may help catch some bugs.
			if err := m.Destroy(); err != nil {
				t.Errorf("destroy: %s", err)
			}
		})
	}
}

// requireV1 skips the test unless a set of requirements (cgroup v1,
// systemd, root) is met.
func requireV1(t *testing.T) {
	t.Helper()
	if cgroups.IsCgroup2UnifiedMode() {
		t.Skip("Test requires cgroup v1.")
	}
	if !IsRunningSystemd() {
		t.Skip("Test requires systemd.")
	}
	if os.Geteuid() != 0 {
		t.Skip("Test requires root.")
	}
}
