package docker

import (
	"flag"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"syscall"
	"testing"

	"github.com/goreleaser/goreleaser/internal/artifact"
	"github.com/goreleaser/goreleaser/internal/pipeline"
	"github.com/goreleaser/goreleaser/pkg/config"
	"github.com/goreleaser/goreleaser/pkg/context"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var it = flag.Bool("it", false, "push images to docker hub")
var registry = "localhost:5000/"

func TestMain(m *testing.M) {
	flag.Parse()
	if *it {
		registry = "docker.io/"
	}
	os.Exit(m.Run())
}

func start(t *testing.T) {
	if *it {
		return
	}
	if out, err := exec.Command(
		"docker", "run", "-d", "-p", "5000:5000", "--name", "registry", "registry:2",
	).CombinedOutput(); err != nil {
		t.Log("failed to start docker registry", string(out), err)
		t.FailNow()
	}
}

func killAndRm(t *testing.T) {
	if *it {
		return
	}
	t.Log("killing registry")
	_ = exec.Command("docker", "kill", "registry").Run()
	_ = exec.Command("docker", "rm", "registry").Run()
}

func TestRunPipe(t *testing.T) {
	type errChecker func(*testing.T, error)
	var shouldErr = func(msg string) errChecker {
		return func(t *testing.T, err error) {
			require.Error(t, err)
			require.Contains(t, err.Error(), msg)
		}
	}
	var shouldNotErr = func(t *testing.T, err error) {
		require.NoError(t, err)
	}

	var table = map[string]struct {
		dockers     []config.Docker
		publish     bool
		expect      []string
		assertError errChecker
	}{
		"valid": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/test_run_pipe",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					TagTemplates: []string{
						"{{.Tag}}-{{.Env.FOO}}",
						"v{{.Major}}",
						"v{{.Major}}.{{.Minor}}",
						"commint-{{.Commit}}",
						"le-{{.Os}}",
						"latest",
					},
					Files: []string{
						"testdata/extra_file.txt",
					},
				},
			},
			expect: []string{
				registry + "goreleaser/test_run_pipe:v1.0.0-123",
				registry + "goreleaser/test_run_pipe:v1",
				registry + "goreleaser/test_run_pipe:v1.0",
				registry + "goreleaser/test_run_pipe:le-linux",
				registry + "goreleaser/test_run_pipe:latest",
			},
			assertError: shouldNotErr,
		},
		"multiple images with same extra file": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/multiplefiles1",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					TagTemplates: []string{
						"latest",
					},
					Files: []string{
						"testdata/extra_file.txt",
					},
				},
				{
					Image:      registry + "goreleaser/multiplefiles2",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					TagTemplates: []string{
						"latest",
					},
					Files: []string{
						"testdata/extra_file.txt",
					},
				},
			},
			expect: []string{
				registry + "goreleaser/multiplefiles1:latest",
				registry + "goreleaser/multiplefiles2:latest",
			},
			assertError: shouldNotErr,
		},
		"multiple images with same dockerfile": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:        registry + "goreleaser/test_run_pipe",
					Goos:         "linux",
					Goarch:       "amd64",
					Dockerfile:   "testdata/Dockerfile",
					Binary:       "mybin",
					TagTemplates: []string{"latest"},
				},
				{
					Image:        registry + "goreleaser/test_run_pipe2",
					Goos:         "linux",
					Goarch:       "amd64",
					Dockerfile:   "testdata/Dockerfile",
					Binary:       "mybin",
					TagTemplates: []string{"latest"},
				},
			},
			expect: []string{
				registry + "goreleaser/test_run_pipe:latest",
				registry + "goreleaser/test_run_pipe2:latest",
			},
			assertError: shouldNotErr,
		},
		"valid_skip_push": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/test_run_pipe",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					SkipPush:   true,
					TagTemplates: []string{
						"{{.Tag}}-{{.Env.FOO}}",
						"v{{.Major}}",
						"v{{.Major}}.{{.Minor}}",
						"latest",
					},
					Files: []string{
						"testdata/extra_file.txt",
					},
				},
			},
			expect: []string{
				registry + "goreleaser/test_run_pipe:v1.0.0-123",
				registry + "goreleaser/test_run_pipe:v1",
				registry + "goreleaser/test_run_pipe:v1.0",
				registry + "goreleaser/test_run_pipe:latest",
			},
			assertError: shouldNotErr,
		},
		"valid_no_latest": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/test_run_pipe",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					TagTemplates: []string{
						"{{.Version}}",
					},
					Files: []string{
						"testdata/extra_file.txt",
					},
				},
			},
			expect: []string{
				registry + "goreleaser/test_run_pipe:1.0.0",
			},
			assertError: shouldNotErr,
		},
		"valid_dont_publish": {
			publish: false,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/test_run_pipe",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					TagTemplates: []string{
						"{{.Tag}}-{{.Env.FOO}}",
						"latest",
					},
					Files: []string{
						"testdata/extra_file.txt",
					},
				},
			},
			expect: []string{
				registry + "goreleaser/test_run_pipe:v1.0.0-123",
				registry + "goreleaser/test_run_pipe:latest",
			},
			assertError: shouldNotErr,
		},
		"bad_dockerfile": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/bad_dockerfile",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile.bad",
					Binary:     "mybin",
					TagTemplates: []string{
						"{{.Version}}",
					},
				},
			},
			assertError: shouldErr("pull access denied for nope, repository does not exist"),
		},
		"template_error": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/test_run_pipe",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					TagTemplates: []string{
						"{{.Tag}",
					},
				},
			},
			assertError: shouldErr(`template: tmpl:1: unexpected "}" in operand`),
		},
		"missing_env_on_template": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      registry + "goreleaser/test_run_pipe",
					Goos:       "linux",
					Goarch:     "amd64",
					Dockerfile: "testdata/Dockerfile",
					Binary:     "mybin",
					TagTemplates: []string{
						"{{.Env.NOPE}}",
					},
				},
			},
			assertError: shouldErr(`template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`),
		},
		"no_permissions": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      "docker.io/nope",
					Goos:       "linux",
					Goarch:     "amd64",
					Binary:     "mybin",
					Dockerfile: "testdata/Dockerfile",
					TagTemplates: []string{
						"{{.Tag}}",
						"latest",
					},
				},
			},
			expect: []string{
				"docker.io/nope:latest",
				"docker.io/nope:v1.0.0",
			},
			assertError: shouldErr(`requested access to the resource is denied`),
		},
		"dockerfile_doesnt_exist": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      "whatever",
					Goos:       "linux",
					Goarch:     "amd64",
					Binary:     "mybin",
					Dockerfile: "testdata/Dockerfilezzz",
					TagTemplates: []string{
						"{{.Tag}}",
					},
				},
			},
			assertError: shouldErr(`failed to link dockerfile`),
		},
		"extra_file_doesnt_exist": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:  "whatever",
					Goos:   "linux",
					Goarch: "amd64",
					Binary: "mybin",
					Files: []string{
						"testdata/nope.txt",
					},
					Dockerfile: "testdata/Dockerfile",
					TagTemplates: []string{
						"{{.Tag}}",
					},
				},
			},
			assertError: shouldErr(`failed to link extra file 'testdata/nope.txt'`),
		},
		"no_matching_binaries": {
			publish: true,
			dockers: []config.Docker{
				{
					Image:      "whatever",
					Goos:       "darwin",
					Goarch:     "amd64",
					Binary:     "mybinnnn",
					Dockerfile: "testdata/Dockerfile",
				},
			},
			assertError: shouldErr(`0 binaries match docker definition: mybinnnn: darwin_amd64_`),
		},
	}

	killAndRm(t)
	start(t)
	defer killAndRm(t)

	for name, docker := range table {
		t.Run(name, func(tt *testing.T) {
			folder, err := ioutil.TempDir("", "archivetest")
			require.NoError(tt, err)
			var dist = filepath.Join(folder, "dist")
			require.NoError(tt, os.Mkdir(dist, 0755))
			require.NoError(tt, os.Mkdir(filepath.Join(dist, "mybin"), 0755))
			var binPath = filepath.Join(dist, "mybin", "mybin")
			_, err = os.Create(binPath)
			require.NoError(tt, err)

			var ctx = context.New(config.Project{
				ProjectName: "mybin",
				Dist:        dist,
				Dockers:     docker.dockers,
			})
			ctx.SkipPublish = !docker.publish
			ctx.Env = map[string]string{
				"FOO": "123",
			}
			ctx.Version = "1.0.0"
			ctx.Git = context.GitInfo{
				CurrentTag: "v1.0.0",
			}
			for _, os := range []string{"linux", "darwin"} {
				for _, arch := range []string{"amd64", "386"} {
					ctx.Artifacts.Add(artifact.Artifact{
						Name:   "mybin",
						Path:   binPath,
						Goarch: arch,
						Goos:   os,
						Type:   artifact.Binary,
						Extra: map[string]string{
							"Binary": "mybin",
						},
					})
				}
			}

			// this might fail as the image doesnt exist yet, so lets ignore the error
			for _, img := range docker.expect {
				_ = exec.Command("docker", "rmi", img).Run()
			}

			docker.assertError(tt, Pipe{}.Run(ctx))

			// this might should not fail as the image should have been created when
			// the step ran
			for _, img := range docker.expect {
				tt.Log("removing docker image", img)
				require.NoError(tt, exec.Command("docker", "rmi", img).Run(), "could not delete image %s", img)
			}

		})
	}
}

func TestDescription(t *testing.T) {
	assert.NotEmpty(t, Pipe{}.String())
}

func TestNoDockers(t *testing.T) {
	assert.True(t, pipeline.IsSkip(Pipe{}.Run(context.New(config.Project{}))))
}

func TestNoDockerWithoutImageName(t *testing.T) {
	assert.True(t, pipeline.IsSkip(Pipe{}.Run(context.New(config.Project{
		Dockers: []config.Docker{
			{
				Goos: "linux",
			},
		},
	}))))
}

func TestDockerNotInPath(t *testing.T) {
	var path = os.Getenv("PATH")
	defer func() {
		assert.NoError(t, os.Setenv("PATH", path))
	}()
	assert.NoError(t, os.Setenv("PATH", ""))
	var ctx = &context.Context{
		Version: "1.0.0",
		Config: config.Project{
			Dockers: []config.Docker{
				{
					Image: "a/b",
				},
			},
		},
	}
	assert.EqualError(t, Pipe{}.Run(ctx), ErrNoDocker.Error())
}

func TestDefault(t *testing.T) {
	var ctx = &context.Context{
		Config: config.Project{
			Builds: []config.Build{
				{
					Binary: "foo",
				},
			},
			Dockers: []config.Docker{
				{},
			},
		},
	}
	assert.NoError(t, Pipe{}.Default(ctx))
	assert.Len(t, ctx.Config.Dockers, 1)
	var docker = ctx.Config.Dockers[0]
	assert.Equal(t, "linux", docker.Goos)
	assert.Equal(t, "amd64", docker.Goarch)
	assert.Equal(t, ctx.Config.Builds[0].Binary, docker.Binary)
	assert.Equal(t, "Dockerfile", docker.Dockerfile)
	assert.Equal(t, []string{"{{ .Version }}"}, docker.TagTemplates)

}

func TestDefaultNoDockers(t *testing.T) {
	var ctx = &context.Context{
		Config: config.Project{
			Dockers: []config.Docker{},
		},
	}
	assert.NoError(t, Pipe{}.Default(ctx))
	assert.Empty(t, ctx.Config.Dockers)
}

func TestDefaultSet(t *testing.T) {
	var ctx = &context.Context{
		Config: config.Project{
			Dockers: []config.Docker{
				{
					Goos:       "windows",
					Goarch:     "i386",
					Binary:     "bar",
					Dockerfile: "Dockerfile.foo",
				},
			},
		},
	}
	assert.NoError(t, Pipe{}.Default(ctx))
	assert.Len(t, ctx.Config.Dockers, 1)
	var docker = ctx.Config.Dockers[0]
	assert.Equal(t, "windows", docker.Goos)
	assert.Equal(t, "i386", docker.Goarch)
	assert.Equal(t, "bar", docker.Binary)
	assert.Equal(t, []string{"{{ .Version }}"}, docker.TagTemplates)
	assert.Equal(t, "Dockerfile.foo", docker.Dockerfile)
}
func TestLinkFile(t *testing.T) {
	const srcFile = "/tmp/test"
	const dstFile = "/tmp/linked"
	err := ioutil.WriteFile(srcFile, []byte("foo"), 0644)
	if err != nil {
		t.Log("Cannot setup test file")
		t.Fail()
	}
	err = link(srcFile, dstFile)
	if err != nil {
		t.Log("Failed to link: ", err)
		t.Fail()
	}
	if inode(srcFile) != inode(dstFile) {
		t.Log("Inodes do not match, destination file is not a link")
		t.Fail()
	}
	// cleanup
	os.Remove(srcFile)
	os.Remove(dstFile)
}

func TestLinkDirectory(t *testing.T) {
	const srcDir = "/tmp/testdir"
	const testFile = "test"
	const dstDir = "/tmp/linkedDir"

	os.Mkdir(srcDir, 0755)
	err := ioutil.WriteFile(srcDir+"/"+testFile, []byte("foo"), 0644)
	if err != nil {
		t.Log("Cannot setup test file")
		t.Fail()
	}
	err = link(srcDir, dstDir)
	if err != nil {
		t.Log("Failed to link: ", err)
		t.Fail()
	}
	if inode(srcDir+"/"+testFile) != inode(dstDir+"/"+testFile) {
		t.Log("Inodes do not match, destination file is not a link")
		t.Fail()
	}

	// cleanup
	os.RemoveAll(srcDir)
	os.RemoveAll(dstDir)
}

func TestLinkTwoLevelDirectory(t *testing.T) {
	const srcDir = "/tmp/testdir"
	const srcLevel2 = srcDir + "/level2"
	const testFile = "test"
	const dstDir = "/tmp/linkedDir"

	os.Mkdir(srcDir, 0755)
	os.Mkdir(srcLevel2, 0755)
	err := ioutil.WriteFile(srcDir+"/"+testFile, []byte("foo"), 0644)
	if err != nil {
		t.Log("Cannot setup test file")
		t.Fail()
	}
	err = ioutil.WriteFile(srcLevel2+"/"+testFile, []byte("foo"), 0644)
	if err != nil {
		t.Log("Cannot setup test file")
		t.Fail()
	}
	err = link(srcDir, dstDir)
	if err != nil {
		t.Log("Failed to link: ", err)
		t.Fail()
	}
	if inode(srcDir+"/"+testFile) != inode(dstDir+"/"+testFile) {
		t.Log("Inodes do not match")
		t.Fail()
	}
	if inode(srcLevel2+"/"+testFile) != inode(dstDir+"/level2/"+testFile) {
		t.Log("Inodes do not match")
		t.Fail()
	}
	// cleanup
	os.RemoveAll(srcDir)
	os.RemoveAll(dstDir)
}

func inode(file string) uint64 {
	fileInfo, err := os.Stat(file)
	if err != nil {
		return 0
	}
	stat := fileInfo.Sys().(*syscall.Stat_t)
	return stat.Ino
}