package llb

import (
	"testing"

	"github.com/moby/buildkit/solver/pb"
	"github.com/stretchr/testify/require"
)

func TestTmpfsMountError(t *testing.T) {
	t.Parallel()

	st := Image("foo").Run(Shlex("args")).AddMount("/tmp", Scratch(), Tmpfs())
	_, err := st.Marshal(t.Context())

	require.Error(t, err)
	require.Contains(t, err.Error(), "can't be used as a parent")

	st = Image("foo").Run(Shlex("args"), AddMount("/tmp", Scratch(), Tmpfs())).Root()
	_, err = st.Marshal(t.Context())
	require.NoError(t, err)

	st = Image("foo").Run(Shlex("args"), AddMount("/tmp", Image("bar"), Tmpfs())).Root()
	_, err = st.Marshal(t.Context())
	require.Error(t, err)
	require.Contains(t, err.Error(), "must use scratch")
}

func TestInvalidSecurityModeMarshalError(t *testing.T) {
	t.Parallel()

	st := Image("busybox:latest").
		Run(Shlex("true"), Security(pb.SecurityMode(2))).Root()

	_, err := st.Marshal(t.Context())
	require.Error(t, err)
	require.Contains(t, err.Error(), "invalid security mode")
}

func TestValidGetMountIndex(t *testing.T) {
	// tests for https://github.com/moby/buildkit/issues/1520

	// tmpfs mount /c will sort later than target mount /b, /b will have output index==1
	st := Image("foo").Run(Shlex("args"), AddMount("/b", Scratch()), AddMount("/c", Scratch(), Tmpfs())).GetMount("/b")

	mountOutput, ok := st.Output().(*output)
	require.True(t, ok, "mount output is expected type")

	mountIndex, err := mountOutput.getIndex()
	require.NoError(t, err, "failed to getIndex")
	require.Equal(t, pb.OutputIndex(1), mountIndex, "unexpected mount index")

	// now swapping so the tmpfs mount /a will sort earlier than the target mount /b, /b should still have output index==1
	st = Image("foo").Run(Shlex("args"), AddMount("/b", Scratch()), AddMount("/a", Scratch(), Tmpfs())).GetMount("/b")

	mountOutput, ok = st.Output().(*output)
	require.True(t, ok, "mount output is expected type")

	mountIndex, err = mountOutput.getIndex()
	require.NoError(t, err, "failed to getIndex")
	require.Equal(t, pb.OutputIndex(1), mountIndex, "unexpected mount index")
}

func TestLinuxResourcesMarshal(t *testing.T) {
	t.Parallel()

	st := Image("busybox:latest").
		Run(
			Shlex("true"),
			MemoryLimit(64*1024*1024),
			CPUShares(512),
			CPUQuota(50000),
			CPUPeriod(100000),
			CpusetCpus("0-3"),
			CpusetMems("0"),
		).Root()

	def, err := st.Marshal(t.Context())
	require.NoError(t, err)

	// Resources should be in OpMetadata (not in the Op bytes / cache key)
	var found bool
	for _, md := range def.Metadata {
		if md.LinuxResources == nil {
			continue
		}
		found = true
		res := md.LinuxResources
		require.Equal(t, int64(64*1024*1024), res.Memory)
		require.Equal(t, uint64(512), res.CpuShares)
		require.Equal(t, int64(50000), res.CpuQuota)
		require.Equal(t, uint64(100000), res.CpuPeriod)
		require.Equal(t, "0-3", res.CpusetCpus)
		require.Equal(t, "0", res.CpusetMems)
	}
	require.True(t, found, "LinuxResources not found in OpMetadata")
}

func TestLinuxResourcesNotInCacheKey(t *testing.T) {
	t.Parallel()

	// Two ops with same command but different resource limits must produce the same digest
	st1 := Image("busybox:latest").
		Run(Shlex("echo hello"), MemoryLimit(64*1024*1024)).Root()

	st2 := Image("busybox:latest").
		Run(Shlex("echo hello"), MemoryLimit(128*1024*1024)).Root()

	st3 := Image("busybox:latest").
		Run(Shlex("echo hello")).Root()

	def1, err := st1.Marshal(t.Context())
	require.NoError(t, err)

	def2, err := st2.Marshal(t.Context())
	require.NoError(t, err)

	def3, err := st3.Marshal(t.Context())
	require.NoError(t, err)

	// All three should produce the same definition bytes (same cache key)
	require.Equal(t, def1.Def, def2.Def, "different resource limits should produce same digest")
	require.Equal(t, def1.Def, def3.Def, "resource limits vs no limits should produce same digest")
}

func TestLinuxResourcesMerge(t *testing.T) {
	t.Parallel()

	// Test that individual resource limit functions merge correctly
	st := Image("busybox:latest").
		Run(
			Shlex("true"),
			MemoryLimit(64*1024*1024),
			CPUShares(512),
		).Root()

	def, err := st.Marshal(t.Context())
	require.NoError(t, err)

	for _, md := range def.Metadata {
		if md.LinuxResources == nil {
			continue
		}
		res := md.LinuxResources
		require.Equal(t, int64(64*1024*1024), res.Memory)
		require.Equal(t, uint64(512), res.CpuShares)
		// Unset fields should be zero
		require.Equal(t, int64(0), res.CpuQuota)
		require.Equal(t, uint64(0), res.CpuPeriod)
		return
	}
	t.Fatal("LinuxResources not found in OpMetadata")
}

func TestExecOpMarshalConsistency(t *testing.T) {
	var prevDef [][]byte
	st := Image("busybox:latest").
		Run(
			Args([]string{"ls /a; ls /b"}),
			AddMount("/b", Scratch().File(Mkfile("file1", 0644, []byte("file1 contents")))),
		).AddMount("/a", Scratch().File(Mkfile("file2", 0644, []byte("file2 contents"))))

	for range 100 {
		def, err := st.Marshal(t.Context())
		require.NoError(t, err)

		if prevDef != nil {
			require.Equal(t, def.Def, prevDef)
		}

		prevDef = def.Def
	}
}
