package environment

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/api/types/system"
	"github.com/docker/docker/client"
	"github.com/docker/docker/testutil/fixtures/load"
	"github.com/pkg/errors"
	"gotest.tools/v3/assert"
)

// Execution contains information about the current test execution and daemon
// under test
type Execution struct {
	client            client.APIClient
	DaemonInfo        system.Info
	DaemonVersion     types.Version
	PlatformDefaults  PlatformDefaults
	protectedElements protectedElements
}

// PlatformDefaults are defaults values for the platform of the daemon under test
type PlatformDefaults struct {
	BaseImage            string
	VolumesConfigPath    string
	ContainerStoragePath string
}

// New creates a new Execution struct
// This is configured using the env client (see client.FromEnv)
func New(ctx context.Context) (*Execution, error) {
	c, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create client")
	}
	return FromClient(ctx, c)
}

// FromClient creates a new Execution environment from the passed in client
func FromClient(ctx context.Context, c *client.Client) (*Execution, error) {
	info, err := c.Info(ctx)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to get info from daemon")
	}
	v, err := c.ServerVersion(context.Background())
	if err != nil {
		return nil, errors.Wrapf(err, "failed to get version info from daemon")
	}

	return &Execution{
		client:            c,
		DaemonInfo:        info,
		DaemonVersion:     v,
		PlatformDefaults:  getPlatformDefaults(info),
		protectedElements: newProtectedElements(),
	}, nil
}

func getPlatformDefaults(info system.Info) PlatformDefaults {
	volumesPath := filepath.Join(info.DockerRootDir, "volumes")
	containersPath := filepath.Join(info.DockerRootDir, "containers")

	switch info.OSType {
	case "linux":
		return PlatformDefaults{
			BaseImage:            "scratch",
			VolumesConfigPath:    toSlash(volumesPath),
			ContainerStoragePath: toSlash(containersPath),
		}
	case "windows":
		baseImage := "mcr.microsoft.com/windows/servercore:ltsc2022"
		if overrideBaseImage := os.Getenv("WINDOWS_BASE_IMAGE"); overrideBaseImage != "" {
			baseImage = overrideBaseImage
			if overrideBaseImageTag := os.Getenv("WINDOWS_BASE_IMAGE_TAG"); overrideBaseImageTag != "" {
				baseImage = baseImage + ":" + overrideBaseImageTag
			}
		}
		fmt.Println("INFO: Windows Base image is ", baseImage)
		return PlatformDefaults{
			BaseImage:            baseImage,
			VolumesConfigPath:    filepath.FromSlash(volumesPath),
			ContainerStoragePath: filepath.FromSlash(containersPath),
		}
	default:
		panic(fmt.Sprintf("unknown OSType for daemon: %s", info.OSType))
	}
}

// Make sure in context of daemon, not the local platform. Note we can't
// use filepath.ToSlash here as that is a no-op on Unix.
func toSlash(path string) string {
	return strings.ReplaceAll(path, `\`, `/`)
}

// IsLocalDaemon is true if the daemon under test is on the same
// host as the test process.
//
// Deterministically working out the environment in which CI is running
// to evaluate whether the daemon is local or remote is not possible through
// a build tag.
//
// For example Windows to Linux CI under Jenkins tests the 64-bit
// Windows binary build with the daemon build tag, but calls a remote
// Linux daemon.
//
// We can't just say if Windows then assume the daemon is local as at
// some point, we will be testing the Windows CLI against a Windows daemon.
//
// Similarly, it will be perfectly valid to also run CLI tests from
// a Linux CLI (built with the daemon tag) against a Windows daemon.
func (e *Execution) IsLocalDaemon() bool {
	return os.Getenv("DOCKER_REMOTE_DAEMON") == ""
}

// IsRemoteDaemon is true if the daemon under test is on different host
// as the test process.
func (e *Execution) IsRemoteDaemon() bool {
	return !e.IsLocalDaemon()
}

// DaemonAPIVersion returns the negotiated daemon api version
func (e *Execution) DaemonAPIVersion() string {
	version, err := e.APIClient().ServerVersion(context.TODO())
	if err != nil {
		return ""
	}
	return version.APIVersion
}

// Print the execution details to stdout
// TODO: print everything
func (e *Execution) Print() {
	if e.IsLocalDaemon() {
		fmt.Println("INFO: Testing against a local daemon")
	} else {
		fmt.Println("INFO: Testing against a remote daemon")
	}
}

// APIClient returns an APIClient connected to the daemon under test
func (e *Execution) APIClient() client.APIClient {
	return e.client
}

// IsUserNamespace returns whether the user namespace remapping is enabled
func (e *Execution) IsUserNamespace() bool {
	root := os.Getenv("DOCKER_REMAP_ROOT")
	return root != ""
}

// RuntimeIsWindowsContainerd returns whether containerd runtime is used on Windows
func (e *Execution) RuntimeIsWindowsContainerd() bool {
	return os.Getenv("DOCKER_WINDOWS_CONTAINERD_RUNTIME") == "1"
}

// IsRootless returns whether the rootless mode is enabled
func (e *Execution) IsRootless() bool {
	return os.Getenv("DOCKER_ROOTLESS") != ""
}

// IsUserNamespaceInKernel returns whether the kernel supports user namespaces
func (e *Execution) IsUserNamespaceInKernel() bool {
	if _, err := os.Stat("/proc/self/uid_map"); os.IsNotExist(err) {
		/*
		 * This kernel-provided file only exists if user namespaces are
		 * supported
		 */
		return false
	}

	// We need extra check on redhat based distributions
	if f, err := os.Open("/sys/module/user_namespace/parameters/enable"); err == nil {
		defer f.Close()
		b := make([]byte, 1)
		_, _ = f.Read(b)
		return string(b) != "N"
	}

	return true
}

// UsingSnapshotter returns whether containerd snapshotters are used for the
// tests by checking if the "TEST_INTEGRATION_USE_SNAPSHOTTER" is set to a
// non-empty value.
func (e *Execution) UsingSnapshotter() bool {
	return os.Getenv("TEST_INTEGRATION_USE_SNAPSHOTTER") != ""
}

// HasExistingImage checks whether there is an image with the given reference.
// Note that this is done by filtering and then checking whether there were any
// results -- so ambiguous references might result in false-positives.
func (e *Execution) HasExistingImage(t testing.TB, reference string) bool {
	imageList, err := e.APIClient().ImageList(context.Background(), image.ListOptions{
		All: true,
		Filters: filters.NewArgs(
			filters.Arg("dangling", "false"),
			filters.Arg("reference", reference),
		),
	})
	assert.NilError(t, err, "failed to list images")

	return len(imageList) > 0
}

// EnsureFrozenImagesLinux loads frozen test images into the daemon
// if they aren't already loaded
func EnsureFrozenImagesLinux(ctx context.Context, testEnv *Execution) error {
	if testEnv.DaemonInfo.OSType == "linux" {
		err := load.FrozenImagesLinux(ctx, testEnv.APIClient(), frozenImages...)
		if err != nil {
			return errors.Wrap(err, "error loading frozen images")
		}
	}
	return nil
}

// GitHubActions is true if test is executed on a GitHub Runner.
func (e *Execution) GitHubActions() bool {
	return os.Getenv("GITHUB_ACTIONS") != ""
}

// NotAmd64 returns true if the daemon's architecture is not amd64
func (e *Execution) NotAmd64() bool {
	return e.DaemonVersion.Arch != "amd64"
}

// FirewallBackendDriver returns the value of FirewallBackend.Driver from
// system Info if set, else the empty string.
func (e *Execution) FirewallBackendDriver() string {
	if e.DaemonInfo.FirewallBackend == nil {
		return ""
	}
	return e.DaemonInfo.FirewallBackend.Driver
}
