// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package runtime_test

import (
	"fmt"
	"internal/cgrouptest"
	"runtime"
	"strings"
	"syscall"
	"testing"
	"unsafe"
)

func mustHaveFourCPUs(t *testing.T) {
	// If NumCPU is lower than the cgroup limit, GOMAXPROCS will use
	// NumCPU.
	//
	// cgroup GOMAXPROCS also have a minimum of 2. We need some room above
	// that to test interesting properties.
	if runtime.NumCPU() < 4 {
		t.Helper()
		t.Skip("skipping test: fewer than 4 CPUs")
	}
}

func TestCgroupGOMAXPROCS(t *testing.T) {
	mustHaveFourCPUs(t)

	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		godebug int
		want    int
	}{
		// With containermaxprocs=1, GOMAXPROCS should use the cgroup
		// limit.
		{
			godebug: 1,
			want:    3,
		},
		// With containermaxprocs=0, it should be ignored.
		{
			godebug: 0,
			want:    runtime.NumCPU(),
		},
	}
	for _, tc := range tests {
		t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) {
			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
				if err := c.SetCPUMax(300000, 100000); err != nil {
					t.Fatalf("unable to set CPU limit: %v", err)
				}

				got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug))
				want := fmt.Sprintf("%d\n", tc.want)
				if got != want {
					t.Fatalf("output got %q want %q", got, want)
				}
			})
		})
	}
}

// Without a cgroup limit, GOMAXPROCS uses NumCPU.
func TestCgroupGOMAXPROCSNoLimit(t *testing.T) {
	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
		if err := c.SetCPUMax(-1, 100000); err != nil {
			t.Fatalf("unable to set CPU limit: %v", err)
		}

		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
		want := fmt.Sprintf("%d\n", runtime.NumCPU())
		if got != want {
			t.Fatalf("output got %q want %q", got, want)
		}
	})
}

// If the cgroup limit is higher than NumCPU, GOMAXPROCS uses NumCPU.
func TestCgroupGOMAXPROCSHigherThanNumCPU(t *testing.T) {
	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
		if err := c.SetCPUMax(2*int64(runtime.NumCPU())*100000, 100000); err != nil {
			t.Fatalf("unable to set CPU limit: %v", err)
		}

		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
		want := fmt.Sprintf("%d\n", runtime.NumCPU())
		if got != want {
			t.Fatalf("output got %q want %q", got, want)
		}
	})
}

func TestCgroupGOMAXPROCSRound(t *testing.T) {
	mustHaveFourCPUs(t)

	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		quota int64
		want  int
	}{
		// We always round the fractional component up.
		{
			quota: 200001,
			want:  3,
		},
		{
			quota: 250000,
			want:  3,
		},
		{
			quota: 299999,
			want:  3,
		},
		// Anything less than two rounds up to a minimum of 2.
		{
			quota: 50000, // 0.5
			want:  2,
		},
		{
			quota: 100000,
			want:  2,
		},
		{
			quota: 150000,
			want:  2,
		},
	}
	for _, tc := range tests {
		t.Run(fmt.Sprintf("%d", tc.quota), func(t *testing.T) {
			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
				if err := c.SetCPUMax(tc.quota, 100000); err != nil {
					t.Fatalf("unable to set CPU limit: %v", err)
				}

				got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
				want := fmt.Sprintf("%d\n", tc.want)
				if got != want {
					t.Fatalf("output got %q want %q", got, want)
				}
			})
		})
	}
}

// Environment variable takes precedence over defaults.
func TestCgroupGOMAXPROCSEnvironment(t *testing.T) {
	mustHaveFourCPUs(t)

	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
		if err := c.SetCPUMax(200000, 100000); err != nil {
			t.Fatalf("unable to set CPU limit: %v", err)
		}

		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", "GOMAXPROCS=3")
		want := "3\n"
		if got != want {
			t.Fatalf("output got %q want %q", got, want)
		}
	})
}

// CPU affinity takes priority if lower than cgroup limit.
func TestCgroupGOMAXPROCSSchedAffinity(t *testing.T) {
	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
		if err := c.SetCPUMax(300000, 100000); err != nil {
			t.Fatalf("unable to set CPU limit: %v", err)
		}

		// CPU affinity is actually a per-thread attribute.
		runtime.LockOSThread()
		defer runtime.UnlockOSThread()

		const maxCPUs = 64 * 1024
		var orig [maxCPUs / 8]byte
		_, _, errno := syscall.Syscall6(syscall.SYS_SCHED_GETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0)
		if errno != 0 {
			t.Fatalf("unable to get CPU affinity: %v", errno)
		}

		// We're going to restrict to CPUs 0 and 1. Make sure those are already available.
		if orig[0]&0b11 != 0b11 {
			t.Skipf("skipping test: CPUs 0 and 1 not available")
		}

		var mask [maxCPUs / 8]byte
		mask[0] = 0b11
		_, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(mask), uintptr(unsafe.Pointer(&mask[0])), 0, 0, 0)
		if errno != 0 {
			t.Fatalf("unable to set CPU affinity: %v", errno)
		}
		defer func() {
			_, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0)
			if errno != 0 {
				t.Fatalf("unable to restore CPU affinity: %v", errno)
			}
		}()

		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
		want := "2\n"
		if got != want {
			t.Fatalf("output got %q want %q", got, want)
		}
	})
}

func TestCgroupGOMAXPROCSSetDefault(t *testing.T) {
	mustHaveFourCPUs(t)

	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		godebug int
		want    int
	}{
		// With containermaxprocs=1, SetDefaultGOMAXPROCS should observe
		// the cgroup limit.
		{
			godebug: 1,
			want:    3,
		},
		// With containermaxprocs=0, it should be ignored.
		{
			godebug: 0,
			want:    runtime.NumCPU(),
		},
	}
	for _, tc := range tests {
		t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) {
			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
				env := []string{
					fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()),
					"GO_TEST_CPU_MAX_QUOTA=300000",
					fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug),
				}
				got := runBuiltTestProg(t, exe, "SetLimitThenDefaultGOMAXPROCS", env...)
				want := fmt.Sprintf("%d\n", tc.want)
				if got != want {
					t.Fatalf("output got %q want %q", got, want)
				}
			})
		})
	}
}

func TestCgroupGOMAXPROCSUpdate(t *testing.T) {
	mustHaveFourCPUs(t)

	if testing.Short() {
		t.Skip("skipping test: long sleeps")
	}

	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
		got := runBuiltTestProg(t, exe, "UpdateGOMAXPROCS", fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()))
		if !strings.Contains(got, "OK") {
			t.Fatalf("output got %q want OK", got)
		}
	})
}

func TestCgroupGOMAXPROCSDontUpdate(t *testing.T) {
	mustHaveFourCPUs(t)

	if testing.Short() {
		t.Skip("skipping test: long sleeps")
	}

	exe, err := buildTestProg(t, "testprog")
	if err != nil {
		t.Fatal(err)
	}

	// Two ways to disable updates: explicit GOMAXPROCS or GODEBUG for
	// update feature.
	for _, v := range []string{"GOMAXPROCS=4", "GODEBUG=updatemaxprocs=0"} {
		t.Run(v, func(t *testing.T) {
			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
				got := runBuiltTestProg(t, exe, "DontUpdateGOMAXPROCS",
					fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()),
					v)
				if !strings.Contains(got, "OK") {
					t.Fatalf("output got %q want OK", got)
				}
			})
		})
	}
}
