/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package oci

import (
	"context"
	"os"
	"testing"

	"github.com/containerd/containerd/v2/core/containers"
	"github.com/containerd/containerd/v2/pkg/namespaces"

	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/opencontainers/runtime-spec/specs-go"
)

func TestWithCPUCount(t *testing.T) {
	var (
		ctx = namespaces.WithNamespace(context.Background(), "testing")
		c   = containers.Container{ID: t.Name()}
		cpu = uint64(8)
		o   = WithWindowsCPUCount(cpu)
	)
	// Test with all three supported scenarios
	platforms := []string{"", "linux/amd64", "windows/amd64"}
	for _, p := range platforms {
		var spec *Spec
		var err error
		if p == "" {
			t.Log("Testing GenerateSpec default platform")
			spec, err = GenerateSpec(ctx, nil, &c, o)
		} else {
			t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
			spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
		}
		if err != nil {
			t.Fatalf("failed to generate spec with: %v", err)
		}
		if *spec.Windows.Resources.CPU.Count != cpu {
			t.Fatalf("spec.Windows.Resources.CPU.Count expected: %v, got: %v", cpu, *spec.Windows.Resources.CPU.Count)
		}
		if spec.Linux != nil && spec.Linux.Resources != nil && spec.Linux.Resources.CPU != nil {
			t.Fatalf("spec.Linux.Resources.CPU section should not be set on GOOS=windows")
		}
	}
}

func TestWithWindowsIgnoreFlushesDuringBoot(t *testing.T) {
	var (
		ctx = namespaces.WithNamespace(context.Background(), "testing")
		c   = containers.Container{ID: t.Name()}
		o   = WithWindowsIgnoreFlushesDuringBoot()
	)
	// Test with all supported scenarios
	platforms := []string{"", "windows/amd64"}
	for _, p := range platforms {
		var spec *Spec
		var err error
		if p == "" {
			t.Log("Testing GenerateSpec default platform")
			spec, err = GenerateSpec(ctx, nil, &c, o)
		} else {
			t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
			spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
		}
		if err != nil {
			t.Fatalf("failed to generate spec with: %v", err)
		}
		if spec.Windows.IgnoreFlushesDuringBoot != true {
			t.Fatalf("spec.Windows.IgnoreFlushesDuringBoot expected: true")
		}
	}
}

func TestWithWindowNetworksAllowUnqualifiedDNSQuery(t *testing.T) {
	var (
		ctx = namespaces.WithNamespace(context.Background(), "testing")
		c   = containers.Container{ID: t.Name()}
		o   = WithWindowNetworksAllowUnqualifiedDNSQuery()
	)
	// Test with all supported scenarios
	platforms := []string{"", "windows/amd64"}
	for _, p := range platforms {
		var spec *Spec
		var err error
		if p == "" {
			t.Log("Testing GenerateSpec default platform")
			spec, err = GenerateSpec(ctx, nil, &c, o)
		} else {
			t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
			spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
		}
		if err != nil {
			t.Fatalf("failed to generate spec with: %v", err)
		}
		if spec.Windows.Network.AllowUnqualifiedDNSQuery != true {
			t.Fatalf("spec.Windows.Network.AllowUnqualifiedDNSQuery expected: true")
		}
	}
}

// TestWithProcessArgsOverwritesWithImage verifies that when calling
// WithImageConfig followed by WithProcessArgs when `ArgsEscaped==false` that
// the process args overwrite the image args.
func TestWithProcessArgsOverwritesWithImage(t *testing.T) {
	t.Parallel()

	img, err := newFakeImage(ocispec.Image{
		Config: ocispec.ImageConfig{
			Entrypoint:  []string{"powershell.exe", "-Command", "Write-Host Hello"},
			Cmd:         []string{"cmd.exe", "/S", "/C", "echo Hello"},
			ArgsEscaped: false,
		},
	})
	if err != nil {
		t.Fatal(err)
	}

	s := Spec{
		Version: specs.Version,
		Root:    &specs.Root{},
		Windows: &specs.Windows{},
	}

	args := []string{"cmd.exe", "echo", "should be set"}
	opts := []SpecOpts{
		WithImageConfig(img),
		WithProcessArgs(args...),
	}

	for _, opt := range opts {
		if err := opt(nil, nil, nil, &s); err != nil {
			t.Fatal(err)
		}
	}

	if err := assertEqualsStringArrays(args, s.Process.Args); err != nil {
		t.Fatal(err)
	}
	if s.Process.CommandLine != "" {
		t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
	}
}

// TestWithProcessArgsOverwritesWithImageArgsEscaped verifies that when calling
// WithImageConfig followed by WithProcessArgs when `ArgsEscaped==true` that the
// process args overwrite the image args.
func TestWithProcessArgsOverwritesWithImageArgsEscaped(t *testing.T) {
	t.Parallel()

	img, err := newFakeImage(ocispec.Image{
		Config: ocispec.ImageConfig{
			Entrypoint:  []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`},
			Cmd:         []string{`cmd.exe /S /C "C:\test path\test.exe"`},
			ArgsEscaped: true,
		},
	})
	if err != nil {
		t.Fatal(err)
	}

	s := Spec{
		Version: specs.Version,
		Root:    &specs.Root{},
		Windows: &specs.Windows{},
	}

	args := []string{"cmd.exe", "echo", "should be set"}
	opts := []SpecOpts{
		WithImageConfig(img),
		WithProcessArgs(args...),
	}

	for _, opt := range opts {
		if err := opt(nil, nil, nil, &s); err != nil {
			t.Fatal(err)
		}
	}

	if err := assertEqualsStringArrays(args, s.Process.Args); err != nil {
		t.Fatal(err)
	}
	if s.Process.CommandLine != "" {
		t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
	}
}

// TestWithImageOverwritesWithProcessArgs verifies that when calling
// WithProcessArgs followed by WithImageConfig `ArgsEscaped==false` that the
// image args overwrites process args.
func TestWithImageOverwritesWithProcessArgs(t *testing.T) {
	t.Parallel()

	img, err := newFakeImage(ocispec.Image{
		Config: ocispec.ImageConfig{
			Entrypoint: []string{"powershell.exe", "-Command"},
			Cmd:        []string{"Write-Host", "echo Hello"},
		},
	})
	if err != nil {
		t.Fatal(err)
	}

	s := Spec{
		Version: specs.Version,
		Root:    &specs.Root{},
		Windows: &specs.Windows{},
	}

	opts := []SpecOpts{
		WithProcessArgs("cmd.exe", "echo", "should not be set"),
		WithImageConfig(img),
	}

	for _, opt := range opts {
		if err := opt(nil, nil, nil, &s); err != nil {
			t.Fatal(err)
		}
	}

	expectedArgs := []string{"powershell.exe", "-Command", "Write-Host", "echo Hello"}
	if err := assertEqualsStringArrays(expectedArgs, s.Process.Args); err != nil {
		t.Fatal(err)
	}
	if s.Process.CommandLine != "" {
		t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
	}
}

// TestWithImageOverwritesWithProcessArgs verifies that when calling
// WithProcessArgs followed by WithImageConfig `ArgsEscaped==true` that the
// image args overwrites process args.
func TestWithImageArgsEscapedOverwritesWithProcessArgs(t *testing.T) {
	t.Parallel()

	img, err := newFakeImage(ocispec.Image{
		Config: ocispec.ImageConfig{
			Entrypoint:  []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`},
			Cmd:         []string{`cmd.exe /S /C "C:\test path\test.exe"`},
			ArgsEscaped: true,
		},
	})
	if err != nil {
		t.Fatal(err)
	}

	s := Spec{
		Version: specs.Version,
		Root:    &specs.Root{},
		Windows: &specs.Windows{},
	}

	opts := []SpecOpts{
		WithProcessArgs("cmd.exe", "echo", "should not be set"),
		WithImageConfig(img),
	}

	expectedCommandLine := `powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2" "cmd.exe /S /C \"C:\test path\test.exe\""`

	for _, opt := range opts {
		if err := opt(nil, nil, nil, &s); err != nil {
			t.Fatal(err)
		}
	}

	if s.Process.Args != nil {
		t.Fatalf("Expected empty Process.Args, got: '%v'", s.Process.Args)
	}
	if expectedCommandLine != s.Process.CommandLine {
		t.Fatalf("Expected CommandLine '%s', got: '%s'", expectedCommandLine, s.Process.CommandLine)
	}
}

func TestWithImageConfigArgsWindows(t *testing.T) {
	testcases := []struct {
		name       string
		entrypoint []string
		cmd        []string
		args       []string

		expectError bool
		// When ArgsEscaped==false we always expect args and CommandLine==""
		expectedArgs []string
	}{
		{
			// This is not really a valid test case since Docker would have made
			// the default cmd to be the shell. So just verify it hits the error
			// case we expect.
			name:        "EmptyEntrypoint_EmptyCmd_EmptyArgs",
			entrypoint:  nil,
			cmd:         nil,
			args:        nil,
			expectError: true,
		},
		{
			name:         "EmptyEntrypoint_EmptyCmd_Args",
			entrypoint:   nil,
			cmd:          nil,
			args:         []string{"additional", "args"},
			expectedArgs: []string{"additional", "args"},
		},
		{
			name:         "EmptyEntrypoint_Cmd_EmptyArgs",
			entrypoint:   nil,
			cmd:          []string{"cmd", "args"},
			args:         nil,
			expectedArgs: []string{"cmd", "args"},
		},
		{
			name:         "EmptyEntrypoint_Cmd_Args",
			entrypoint:   nil,
			cmd:          []string{"cmd", "args"},
			args:         []string{"additional", "args"},
			expectedArgs: []string{"additional", "args"}, // Args overwrite Cmd
		},
		{
			name:         "Entrypoint_EmptyCmd_EmptyArgs",
			entrypoint:   []string{"entrypoint", "args"},
			cmd:          nil,
			args:         nil,
			expectedArgs: []string{"entrypoint", "args"},
		},
		{
			name:         "Entrypoint_EmptyCmd_Args",
			entrypoint:   []string{"entrypoint", "args"},
			cmd:          nil,
			args:         []string{"additional", "args"},
			expectedArgs: []string{"entrypoint", "args", "additional", "args"},
		},
		{
			name:         "Entrypoint_Cmd_EmptyArgs",
			entrypoint:   []string{"entrypoint", "args"},
			cmd:          []string{"cmd", "args"},
			args:         nil,
			expectedArgs: []string{"entrypoint", "args", "cmd", "args"},
		},
		{
			name:         "Entrypoint_Cmd_Args",
			entrypoint:   []string{"entrypoint", "args"},
			cmd:          []string{"cmd", "args"},
			args:         []string{"additional", "args"}, // Args overwrites Cmd
			expectedArgs: []string{"entrypoint", "args", "additional", "args"},
		},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			img, err := newFakeImage(ocispec.Image{
				Config: ocispec.ImageConfig{
					Entrypoint: tc.entrypoint,
					Cmd:        tc.cmd,
				},
			})
			if err != nil {
				t.Fatal(err)
			}

			s := Spec{
				Version: specs.Version,
				Root:    &specs.Root{},
				Windows: &specs.Windows{},
			}

			opts := []SpecOpts{
				WithImageConfigArgs(img, tc.args),
			}

			for _, opt := range opts {
				if err := opt(nil, nil, nil, &s); err != nil {
					if tc.expectError {
						continue
					}
					t.Fatal(err)
				}
			}

			if err := assertEqualsStringArrays(tc.expectedArgs, s.Process.Args); err != nil {
				t.Fatal(err)
			}
			if s.Process.CommandLine != "" {
				t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
			}
		})
	}
}

func TestWithImageConfigArgsEscapedWindows(t *testing.T) {
	testcases := []struct {
		name       string
		entrypoint []string
		cmd        []string
		args       []string

		expectError         bool
		expectedArgs        []string
		expectedCommandLine string
	}{
		{
			// This is not really a valid test case since Docker would have made
			// the default cmd to be the shell. So just verify it hits the error
			// case we expect.
			name:                "EmptyEntrypoint_EmptyCmd_EmptyArgs",
			entrypoint:          nil,
			cmd:                 nil,
			args:                nil,
			expectError:         true,
			expectedArgs:        nil,
			expectedCommandLine: "",
		},
		{
			// This case is special for ArgsEscaped, since there is no Image
			// Default Args should be passed as ProcessArgs not as Cmdline
			name:                "EmptyEntrypoint_EmptyCmd_Args",
			entrypoint:          nil,
			cmd:                 nil,
			args:                []string{"additional", "-args", "hello world"},
			expectedArgs:        []string{"additional", "-args", "hello world"},
			expectedCommandLine: "",
		},
		{
			name:                "EmptyEntrypoint_Cmd_EmptyArgs",
			entrypoint:          nil,
			cmd:                 []string{`cmd -args "hello world"`},
			args:                nil,
			expectedCommandLine: `cmd -args "hello world"`,
		},
		{
			// This case is a second special case for ArgsEscaped, since Args
			// overwrite Cmd the args are not from the image, so ArgsEscaped
			// should be ignored, and passed as Args not CommandLine.
			name:                "EmptyEntrypoint_Cmd_Args",
			entrypoint:          nil,
			cmd:                 []string{`cmd -args "hello world"`},
			args:                []string{"additional", "args"},
			expectedArgs:        []string{"additional", "args"}, // Args overwrite Cmd
			expectedCommandLine: "",
		},
		{
			name:                "Entrypoint_EmptyCmd_EmptyArgs",
			entrypoint:          []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
			cmd:                 nil,
			args:                nil,
			expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value"`,
		},
		{
			name:                "Entrypoint_EmptyCmd_Args",
			entrypoint:          []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
			cmd:                 nil,
			args:                []string{"additional", "args with spaces"},
			expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value" additional "args with spaces"`,
		},
		{
			// This case will not work in Docker today so adding the test to
			// confirm we fail in the same way. Although the appending of
			// Entrypoint + " " + Cmd here works, Cmd is double escaped and the
			// container would not launch. This is because when Docker built
			// such an image it escaped both Entrypoint and Cmd. However the
			// docs say that CMD should always be appened to entrypoint if not
			// overwritten so this results in an incorrect cmdline.
			name:                "Entrypoint_Cmd_EmptyArgs",
			entrypoint:          []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
			cmd:                 []string{`cmd -args "hello world"`},
			args:                nil,
			expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value" "cmd -args \"hello world\""`,
		},
		{
			name:                "Entrypoint_Cmd_Args",
			entrypoint:          []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
			cmd:                 []string{`cmd -args "hello world"`},
			args:                []string{"additional", "args with spaces"}, // Args overwrites Cmd
			expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value" additional "args with spaces"`,
		},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			img, err := newFakeImage(ocispec.Image{
				Config: ocispec.ImageConfig{
					Entrypoint:  tc.entrypoint,
					Cmd:         tc.cmd,
					ArgsEscaped: true,
				},
			})
			if err != nil {
				t.Fatal(err)
			}

			s := Spec{
				Version: specs.Version,
				Root:    &specs.Root{},
				Windows: &specs.Windows{},
			}

			opts := []SpecOpts{
				WithImageConfigArgs(img, tc.args),
			}

			for _, opt := range opts {
				if err := opt(nil, nil, nil, &s); err != nil {
					if tc.expectError {
						continue
					}
					t.Fatal(err)
				}
			}

			if err := assertEqualsStringArrays(tc.expectedArgs, s.Process.Args); err != nil {
				t.Fatal(err)
			}
			if tc.expectedCommandLine != s.Process.CommandLine {
				t.Fatalf("Expected CommandLine: '%s', got: '%s'", tc.expectedCommandLine, s.Process.CommandLine)
			}
		})
	}
}

func TestWindowsDefaultPathEnv(t *testing.T) {
	t.Parallel()
	s := Spec{}
	s.Process = &specs.Process{
		Env: []string{},
	}

	var (
		defaultUnixEnv = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
		ctx            = namespaces.WithNamespace(context.Background(), "test")
	)

	//check that the default PATH environment is not null
	if os.Getenv("PATH") == "" {
		t.Fatal("PATH environment variable is not set")
	}
	WithDefaultPathEnv(ctx, nil, nil, &s)
	//check that the path is not overwritten by the unix default path
	if Contains(s.Process.Env, defaultUnixEnv) {
		t.Fatal("default Windows Env overwritten by the default Unix Env")
	}
}
