package container

import (
	"context"
	"io"
	"os"
	"runtime"
	"strings"
	"testing"

	"github.com/docker/cli/internal/test"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/pkg/archive"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/fs"
)

func TestRunCopyWithInvalidArguments(t *testing.T) {
	testcases := []struct {
		doc         string
		options     copyOptions
		expectedErr string
	}{
		{
			doc: "copy between container",
			options: copyOptions{
				source:      "first:/path",
				destination: "second:/path",
			},
			expectedErr: "copying between containers is not supported",
		},
		{
			doc: "copy without a container",
			options: copyOptions{
				source:      "./source",
				destination: "./dest",
			},
			expectedErr: "must specify at least one container source",
		},
	}
	for _, testcase := range testcases {
		t.Run(testcase.doc, func(t *testing.T) {
			err := runCopy(context.TODO(), test.NewFakeCli(nil), testcase.options)
			assert.Error(t, err, testcase.expectedErr)
		})
	}
}

func TestRunCopyFromContainerToStdout(t *testing.T) {
	tarContent := "the tar content"

	cli := test.NewFakeCli(&fakeClient{
		containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
			assert.Check(t, is.Equal("container", ctr))
			return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil
		},
	})
	err := runCopy(context.TODO(), cli, copyOptions{
		source:      "container:/path",
		destination: "-",
	})
	assert.NilError(t, err)
	assert.Check(t, is.Equal(tarContent, cli.OutBuffer().String()))
	assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
}

func TestRunCopyFromContainerToFilesystem(t *testing.T) {
	destDir := fs.NewDir(t, "cp-test",
		fs.WithFile("file1", "content\n"))
	defer destDir.Remove()

	cli := test.NewFakeCli(&fakeClient{
		containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
			assert.Check(t, is.Equal("container", ctr))
			readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
			return readCloser, container.PathStat{}, err
		},
	})
	err := runCopy(context.TODO(), cli, copyOptions{
		source:      "container:/path",
		destination: destDir.Path(),
		quiet:       true,
	})
	assert.NilError(t, err)
	assert.Check(t, is.Equal("", cli.OutBuffer().String()))
	assert.Check(t, is.Equal("", cli.ErrBuffer().String()))

	content, err := os.ReadFile(destDir.Join("file1"))
	assert.NilError(t, err)
	assert.Check(t, is.Equal("content\n", string(content)))
}

func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.T) {
	destDir := fs.NewDir(t, "cp-test",
		fs.WithFile("file1", "content\n"))
	defer destDir.Remove()

	cli := test.NewFakeCli(&fakeClient{
		containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
			assert.Check(t, is.Equal("container", ctr))
			readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
			return readCloser, container.PathStat{}, err
		},
	})
	err := runCopy(context.TODO(), cli, copyOptions{
		source:      "container:/path",
		destination: destDir.Join("missing", "foo"),
	})
	assert.ErrorContains(t, err, destDir.Join("missing"))
}

func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) {
	srcFile := fs.NewFile(t, t.Name())
	defer srcFile.Remove()

	cli := test.NewFakeCli(&fakeClient{})
	err := runCopy(context.TODO(), cli, copyOptions{
		source:      srcFile.Path() + string(os.PathSeparator),
		destination: "container:/path",
	})

	expectedError := "not a directory"
	if runtime.GOOS == "windows" {
		expectedError = "The filename, directory name, or volume label syntax is incorrect"
	}
	assert.ErrorContains(t, err, expectedError)
}

func TestRunCopyToContainerSourceDoesNotExist(t *testing.T) {
	cli := test.NewFakeCli(&fakeClient{})
	err := runCopy(context.TODO(), cli, copyOptions{
		source:      "/does/not/exist",
		destination: "container:/path",
	})
	expected := "no such file or directory"
	if runtime.GOOS == "windows" {
		expected = "cannot find the file specified"
	}
	assert.ErrorContains(t, err, expected)
}

func TestSplitCpArg(t *testing.T) {
	testcases := []struct {
		doc               string
		path              string
		os                string
		expectedContainer string
		expectedPath      string
	}{
		{
			doc:          "absolute path with colon",
			os:           "unix",
			path:         "/abs/path:withcolon",
			expectedPath: "/abs/path:withcolon",
		},
		{
			doc:          "relative path with colon",
			path:         "./relative:path",
			expectedPath: "./relative:path",
		},
		{
			doc:          "absolute path with drive",
			os:           "windows",
			path:         `d:\abs\path`,
			expectedPath: `d:\abs\path`,
		},
		{
			doc:          "no separator",
			path:         "relative/path",
			expectedPath: "relative/path",
		},
		{
			doc:               "with separator",
			path:              "container:/opt/foo",
			expectedPath:      "/opt/foo",
			expectedContainer: "container",
		},
	}
	for _, tc := range testcases {
		tc := tc
		t.Run(tc.doc, func(t *testing.T) {
			if tc.os == "windows" && runtime.GOOS != "windows" {
				t.Skip("skipping windows test on non-windows platform")
			}
			if tc.os == "unix" && runtime.GOOS == "windows" {
				t.Skip("skipping unix test on windows")
			}

			ctr, path := splitCpArg(tc.path)
			assert.Check(t, is.Equal(tc.expectedContainer, ctr))
			assert.Check(t, is.Equal(tc.expectedPath, path))
		})
	}
}

func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
	cli := test.NewFakeCli(nil)
	err := runCopy(context.TODO(), cli, copyOptions{
		source:      "container:/dev/null",
		destination: "/dev/random",
	})
	assert.Assert(t, err != nil)
	expected := `"/dev/random" must be a directory or a regular file`
	assert.ErrorContains(t, err, expected)
}
