//go:build !windows
// +build !windows

package daemon

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/containerd/containerd/plugin"
	v2runcoptions "github.com/containerd/containerd/runtime/v2/runc/options"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/daemon/config"
	"github.com/docker/docker/errdefs"
)

func TestInitRuntimes_InvalidConfigs(t *testing.T) {
	cases := []struct {
		name      string
		runtime   types.Runtime
		expectErr string
	}{
		{
			name:      "Empty",
			expectErr: "either a runtimeType or a path must be configured",
		},
		{
			name:      "ArgsOnly",
			runtime:   types.Runtime{Args: []string{"foo", "bar"}},
			expectErr: "either a runtimeType or a path must be configured",
		},
		{
			name:      "OptionsOnly",
			runtime:   types.Runtime{Options: map[string]interface{}{"hello": "world"}},
			expectErr: "either a runtimeType or a path must be configured",
		},
		{
			name:      "PathAndType",
			runtime:   types.Runtime{Path: "/bin/true", Type: "io.containerd.runsc.v1"},
			expectErr: "cannot configure both",
		},
		{
			name:      "PathAndOptions",
			runtime:   types.Runtime{Path: "/bin/true", Options: map[string]interface{}{"a": "b"}},
			expectErr: "options cannot be used with a path runtime",
		},
		{
			name:      "TypeAndArgs",
			runtime:   types.Runtime{Type: "io.containerd.runsc.v1", Args: []string{"--version"}},
			expectErr: "args cannot be used with a runtimeType runtime",
		},
		{
			name: "PathArgsOptions",
			runtime: types.Runtime{
				Path:    "/bin/true",
				Args:    []string{"--version"},
				Options: map[string]interface{}{"hmm": 3},
			},
			expectErr: "options cannot be used with a path runtime",
		},
		{
			name: "TypeOptionsArgs",
			runtime: types.Runtime{
				Type:    "io.containerd.kata.v2",
				Options: map[string]interface{}{"a": "b"},
				Args:    []string{"--help"},
			},
			expectErr: "args cannot be used with a runtimeType runtime",
		},
		{
			name: "PathArgsTypeOptions",
			runtime: types.Runtime{
				Path:    "/bin/true",
				Args:    []string{"foo"},
				Type:    "io.containerd.runsc.v1",
				Options: map[string]interface{}{"a": "b"},
			},
			expectErr: "cannot configure both",
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			cfg, err := config.New()
			assert.NilError(t, err)
			d := &Daemon{configStore: cfg}
			d.configStore.Root = t.TempDir()
			assert.Assert(t, os.Mkdir(filepath.Join(d.configStore.Root, "runtimes"), 0700))

			err = d.initRuntimes(map[string]types.Runtime{"myruntime": tt.runtime})
			assert.Check(t, is.ErrorContains(err, tt.expectErr))
		})
	}
}

func TestGetRuntime(t *testing.T) {
	// Configured runtimes can have any arbitrary name, including names
	// which would not be allowed as implicit runtime names. Explicit takes
	// precedence over implicit.
	const configuredRtName = "my/custom.runtime.v1"
	configuredRuntime := types.Runtime{Path: "/bin/true"}

	const rtWithArgsName = "withargs"
	rtWithArgs := types.Runtime{
		Path: "/bin/false",
		Args: []string{"--version"},
	}

	const shimWithOptsName = "shimwithopts"
	shimWithOpts := types.Runtime{
		Type:    plugin.RuntimeRuncV2,
		Options: map[string]interface{}{"IoUid": 42},
	}

	const shimAliasName = "wasmedge"
	shimAlias := types.Runtime{Type: "io.containerd.wasmedge.v1"}

	const configuredShimByPathName = "shimwithpath"
	configuredShimByPath := types.Runtime{Type: "/path/to/my/shim"}

	cfg, err := config.New()
	assert.NilError(t, err)

	d := &Daemon{configStore: cfg}
	d.configStore.Root = t.TempDir()
	assert.Assert(t, os.Mkdir(filepath.Join(d.configStore.Root, "runtimes"), 0700))
	d.configStore.Runtimes = map[string]types.Runtime{
		configuredRtName:         configuredRuntime,
		rtWithArgsName:           rtWithArgs,
		shimWithOptsName:         shimWithOpts,
		shimAliasName:            shimAlias,
		configuredShimByPathName: configuredShimByPath,
	}
	configureRuntimes(d.configStore)
	assert.Assert(t, d.loadRuntimes())

	stockRuntime, ok := d.configStore.Runtimes[config.StockRuntimeName]
	assert.Assert(t, ok, "stock runtime could not be found (test needs to be updated)")

	configdOpts := *stockRuntime.ShimConfig.Opts.(*v2runcoptions.Options)
	configdOpts.BinaryName = configuredRuntime.Path

	for _, tt := range []struct {
		name, runtime string
		wantShim      string
		wantOpts      interface{}
	}{
		{
			name:     "StockRuntime",
			runtime:  config.StockRuntimeName,
			wantShim: stockRuntime.ShimConfig.Binary,
			wantOpts: stockRuntime.ShimConfig.Opts,
		},
		{
			name:     "ShimName",
			runtime:  "io.containerd.my-shim.v42",
			wantShim: "io.containerd.my-shim.v42",
		},
		{
			// containerd is pretty loose about the format of runtime names. Perhaps too
			// loose. The only requirements are that the name contain a dot and (depending
			// on the containerd version) not start with a dot. It does not enforce any
			// particular format of the dot-delimited components of the name.
			name:     "VersionlessShimName",
			runtime:  "io.containerd.my-shim",
			wantShim: "io.containerd.my-shim",
		},
		{
			name:    "IllformedShimName",
			runtime: "myshim",
		},
		{
			name:    "EmptyString",
			runtime: "",
		},
		{
			name:    "PathToShim",
			runtime: "/path/to/runc",
		},
		{
			name:    "PathToShimName",
			runtime: "/path/to/io.containerd.runc.v2",
		},
		{
			name:    "RelPathToShim",
			runtime: "my/io.containerd.runc.v2",
		},
		{
			name:     "ConfiguredRuntime",
			runtime:  configuredRtName,
			wantShim: stockRuntime.ShimConfig.Binary,
			wantOpts: &configdOpts,
		},
		{
			name:     "RuntimeWithArgs",
			runtime:  rtWithArgsName,
			wantShim: stockRuntime.ShimConfig.Binary,
			wantOpts: defaultV2ShimConfig(
				d.configStore,
				d.rewriteRuntimePath(
					rtWithArgsName,
					rtWithArgs.Path,
					rtWithArgs.Args)).Opts,
		},
		{
			name:     "ShimWithOpts",
			runtime:  shimWithOptsName,
			wantShim: shimWithOpts.Type,
			wantOpts: &v2runcoptions.Options{IoUid: 42},
		},
		{
			name:     "ShimAlias",
			runtime:  shimAliasName,
			wantShim: shimAlias.Type,
		},
		{
			name:     "ConfiguredShimByPath",
			runtime:  configuredShimByPathName,
			wantShim: configuredShimByPath.Type,
		},
	} {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			gotShim, gotOpts, err := d.getRuntime(tt.runtime)
			assert.Check(t, is.Equal(gotShim, tt.wantShim))
			assert.Check(t, is.DeepEqual(gotOpts, tt.wantOpts))
			if tt.wantShim != "" {
				assert.Check(t, err)
			} else {
				assert.Check(t, errdefs.IsInvalidParameter(err))
			}
		})
	}
}
