package command

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/moby/moby/v2/daemon/config"
	"github.com/moby/sys/reexec"
	"golang.org/x/sys/unix"
	"gotest.tools/v3/assert"
)

const (
	testListenerNoAddrCmdPhase1 = "test-listener-no-addr1"
	testListenerNoAddrCmdPhase2 = "test-listener-no-addr2"
)

type listenerTestResponse struct {
	Err string
}

func initListenerTestPhase1() {
	os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid()))
	os.Setenv("LISTEN_FDS", "1")

	// NOTE: We cannot use O_CLOEXEC here because we need the fd to stay open for the child process.
	_, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	cmd := reexec.Command(testListenerNoAddrCmdPhase2)
	if err := unix.Exec(cmd.Path, cmd.Args, os.Environ()); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func initListenerTestPhase2() {
	cfg := &config.Config{
		CommonConfig: config.CommonConfig{
			Hosts: []string{"fd://"},
		},
	}
	_, _, err := loadListeners(cfg, nil)
	var resp listenerTestResponse
	if err != nil {
		resp.Err = err.Error()
	}

	if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

// Test to make sure that the listen specs without an address are handled
// It requires a 2-phase setup due to how socket activation works (which we are using to test).
// It requires LISTEN_FDS and LISTEN_PID to be set in the environment.
//
// LISTEN_PID is used by socket activation to determine if the process is the one that should be activated.
// LISTEN_FDS is used by socket activation to determine how many file descriptors are passed to the process.
//
// We can sort of fake this without using extra processes, but it ends up not
// being a true test because that's not how socket activation is expected to
// work and we'll end up with nil listeners since the test framework has other
// file descriptors open.
//
// This is not currently testing `tcp://` or `unix://` listen specs without an address because those can conflict with the machine running the test.
// This could be worked around by using linux namespaces, however that would require root privileges which unit tests don't typically have.
func TestLoadListenerNoAddr(t *testing.T) {
	cmd := reexec.Command(testListenerNoAddrCmdPhase1)
	stdout := bytes.NewBuffer(nil)
	cmd.Stdout = stdout
	stderr := bytes.NewBuffer(nil)
	cmd.Stderr = stderr

	assert.NilError(t, cmd.Run(), stderr.String())

	var resp listenerTestResponse
	assert.NilError(t, json.NewDecoder(stdout).Decode(&resp))
	assert.Equal(t, resp.Err, "")
}

func TestC8dSnapshotterWithUsernsRemap(t *testing.T) {
	testcases := []struct {
		name   string
		cfg    *config.Config
		expCfg *config.Config
		expErr string
	}{
		{
			name:   "no remap, no snapshotter",
			cfg:    &config.Config{},
			expCfg: &config.Config{},
		},
		{
			name: "userns remap, no explicit containerd-snapshotter feature",
			cfg:  &config.Config{RemappedRoot: "default"},
			expCfg: &config.Config{
				RemappedRoot: "dockremap:dockremap",
				CommonConfig: config.CommonConfig{
					ContainerdNamespace:       "-100000.100000",
					ContainerdPluginNamespace: "-100000.100000",
					Features:                  map[string]bool{"containerd-snapshotter": false},
				},
			},
		},
		{
			name: "userns remap, explicit containerd-snapshotter feature",
			cfg: &config.Config{
				RemappedRoot: "default",
				CommonConfig: config.CommonConfig{Features: map[string]bool{"containerd-snapshotter": true}},
			},
			expCfg: &config.Config{
				RemappedRoot: "dockremap:dockremap",
				CommonConfig: config.CommonConfig{
					ContainerdNamespace:       "-100000.100000",
					ContainerdPluginNamespace: "-100000.100000",
					Features:                  map[string]bool{"containerd-snapshotter": true},
				},
			},
			expErr: "containerd-snapshotter is explicitly enabled, but is not compatible with userns remapping. Please disable userns remapping or containerd-snapshotter",
		},
		{
			name: "no remap, explicit containerd-snapshotter feature",
			cfg: &config.Config{
				CommonConfig: config.CommonConfig{Features: map[string]bool{"containerd-snapshotter": true}},
			},
			expCfg: &config.Config{
				CommonConfig: config.CommonConfig{Features: map[string]bool{"containerd-snapshotter": true}},
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			err := setPlatformOptions(tc.cfg)
			assert.DeepEqual(t, tc.expCfg, tc.cfg, cmp.AllowUnexported(config.DefaultBridgeConfig{}))
			if tc.expErr != "" {
				assert.Equal(t, tc.expErr, err.Error())
			} else {
				assert.NilError(t, err)
			}
		})
	}
}
