package builder

import (
	"archive/tar"
	"bytes"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/docker/docker/pkg/archive"
)

const (
	dockerfileTestName = "Dockerfile-test"
	dockerfileContent  = "FROM busybox"
)

var prepareEmpty = func(t *testing.T) string {
	return ""
}

var prepareNoFiles = func(t *testing.T) string {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error when creating temporary directory: %s", err)
	}

	return contextDir
}

var prepareOneFile = func(t *testing.T) string {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error when creating temporary directory: %s", err)
	}

	dockerfileFilename := filepath.Join(contextDir, dockerfileTestName)
	err = ioutil.WriteFile(dockerfileFilename, []byte(dockerfileContent), 0777)

	if err != nil {
		t.Fatalf("Error with writing to file: %s", err)
	}

	return contextDir
}

func testValidateContextDirectory(t *testing.T, prepare func(t *testing.T) string, excludes []string) {
	contextDir := prepare(t)

	defer os.RemoveAll(contextDir)
	err := ValidateContextDirectory(contextDir, excludes)

	if err != nil {
		t.Fatalf("Error should be nil, got: %s", err)
	}
}

func TestGetContextFromLocalDirNoDockerfile(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	defer os.RemoveAll(contextDir)

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")

	if err == nil {
		t.Fatalf("Error should not be nil")
	}

	if absContextDir != "" {
		t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
	}

	if relDockerfile != "" {
		t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
	}
}

func TestGetContextFromLocalDirNotExistingDir(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	defer os.RemoveAll(contextDir)

	fakePath := filepath.Join(contextDir, "fake")

	absContextDir, relDockerfile, err := GetContextFromLocalDir(fakePath, "")

	if err == nil {
		t.Fatalf("Error should not be nil")
	}

	if absContextDir != "" {
		t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
	}

	if relDockerfile != "" {
		t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
	}
}

func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	defer os.RemoveAll(contextDir)

	fakePath := filepath.Join(contextDir, "fake")

	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, fakePath)

	if err == nil {
		t.Fatalf("Error should not be nil")
	}

	if absContextDir != "" {
		t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
	}

	if relDockerfile != "" {
		t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
	}
}

func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	defer os.RemoveAll(contextDir)

	dockerfileFilename := filepath.Join(contextDir, DefaultDockerfileName)
	err = ioutil.WriteFile(dockerfileFilename, []byte(dockerfileContent), 0777)

	if err != nil {
		t.Fatalf("Error when writing file (%s) contents: %s", dockerfileFilename, err)
	}

	workingDirectory, err := os.Getwd()

	if err != nil {
		t.Fatalf("Error when retrieving working directory: %s", err)
	}

	defer os.Chdir(workingDirectory)

	err = os.Chdir(contextDir)

	if err != nil {
		t.Fatalf("Error when changing directory to %s: %s", contextDir, err)
	}
	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")

	if err != nil {
		t.Fatalf("Error when getting context from local dir: %s", err)
	}

	if absContextDir != contextDir {
		t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir)
	}

	if relDockerfile != DefaultDockerfileName {
		t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile)
	}
}

func TestGetContextFromLocalDirWithDockerfile(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	defer os.RemoveAll(contextDir)

	dockerfileFilename := filepath.Join(contextDir, DefaultDockerfileName)
	err = ioutil.WriteFile(dockerfileFilename, []byte(dockerfileContent), 0777)

	if err != nil {
		t.Fatalf("Error when writing file (%s) contents: %s", dockerfileFilename, err)
	}

	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")

	if err != nil {
		t.Fatalf("Error when getting context from local dir: %s", err)
	}

	if absContextDir != contextDir {
		t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir)
	}

	if relDockerfile != DefaultDockerfileName {
		t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile)
	}
}

func TestGetContextFromLocalDirLocalFile(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	defer os.RemoveAll(contextDir)

	dockerfileFilename := filepath.Join(contextDir, DefaultDockerfileName)
	err = ioutil.WriteFile(dockerfileFilename, []byte(dockerfileContent), 0777)

	if err != nil {
		t.Fatalf("Error when writing file (%s) contents: %s", dockerfileFilename, err)
	}

	testFilename := filepath.Join(contextDir, "tmpTest")
	testContent := "test"
	err = ioutil.WriteFile(testFilename, []byte(testContent), 0777)

	if err != nil {
		t.Fatalf("Error when writing file (%s) contents: %s", testFilename, err)
	}

	absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "")

	if err == nil {
		t.Fatalf("Error should not be nil")
	}

	if absContextDir != "" {
		t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
	}

	if relDockerfile != "" {
		t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
	}
}

func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	defer os.RemoveAll(contextDir)

	workingDirectory, err := os.Getwd()

	if err != nil {
		t.Fatalf("Error when retrieving working directory: %s", err)
	}

	defer os.Chdir(workingDirectory)

	err = os.Chdir(contextDir)

	if err != nil {
		t.Fatalf("Error when changing directory to %s: %s", contextDir, err)
	}

	dockerfileFilename := filepath.Join(contextDir, dockerfileTestName)
	err = ioutil.WriteFile(dockerfileFilename, []byte(dockerfileContent), 0777)

	if err != nil {
		t.Fatalf("Error when writing file (%s) contents: %s", dockerfileFilename, err)
	}

	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, dockerfileTestName)

	if err != nil {
		t.Fatalf("Error when getting context from local dir: %s", err)
	}

	if absContextDir != contextDir {
		t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir)
	}

	if relDockerfile != dockerfileTestName {
		t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", dockerfileTestName, relDockerfile)
	}

}

func TestGetContextFromReaderString(t *testing.T) {
	tarArchive, relDockerfile, err := GetContextFromReader(ioutil.NopCloser(strings.NewReader(dockerfileContent)), "")

	if err != nil {
		t.Fatalf("Error when executing GetContextFromReader: %s", err)
	}

	tarReader := tar.NewReader(tarArchive)

	_, err = tarReader.Next()

	if err != nil {
		t.Fatalf("Error when reading tar archive: %s", err)
	}

	buff := new(bytes.Buffer)
	buff.ReadFrom(tarReader)
	contents := buff.String()

	_, err = tarReader.Next()

	if err != io.EOF {
		t.Fatalf("Tar stream too long: %s", err)
	}

	if err = tarArchive.Close(); err != nil {
		t.Fatalf("Error when closing tar stream: %s", err)
	}

	if dockerfileContent != contents {
		t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContent, contents)
	}

	if relDockerfile != DefaultDockerfileName {
		t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile)
	}
}

func TestGetContextFromReaderTar(t *testing.T) {
	contextDir, err := ioutil.TempDir("", "builder-context-test")

	if err != nil {
		t.Fatalf("Error with creating temporary directory: %s", err)
	}

	defer os.RemoveAll(contextDir)

	dockerfileFilename := filepath.Join(contextDir, dockerfileTestName)
	err = ioutil.WriteFile(dockerfileFilename, []byte(dockerfileContent), 0777)

	if err != nil {
		t.Fatalf("Error when writing file (%s) contents: %s", dockerfileFilename, err)
	}

	tarStream, err := archive.Tar(contextDir, archive.Uncompressed)

	if err != nil {
		t.Fatalf("Error when creating tar: %s", err)
	}

	tarArchive, relDockerfile, err := GetContextFromReader(tarStream, dockerfileTestName)

	if err != nil {
		t.Fatalf("Error when executing GetContextFromReader: %s", err)
	}

	tarReader := tar.NewReader(tarArchive)

	header, err := tarReader.Next()

	if err != nil {
		t.Fatalf("Error when reading tar archive: %s", err)
	}

	if header.Name != dockerfileTestName {
		t.Fatalf("Dockerfile name should be: %s, got: %s", dockerfileTestName, header.Name)
	}

	buff := new(bytes.Buffer)
	buff.ReadFrom(tarReader)
	contents := buff.String()

	_, err = tarReader.Next()

	if err != io.EOF {
		t.Fatalf("Tar stream too long: %s", err)
	}

	if err = tarArchive.Close(); err != nil {
		t.Fatalf("Error when closing tar stream: %s", err)
	}

	if dockerfileContent != contents {
		t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContent, contents)
	}

	if relDockerfile != dockerfileTestName {
		t.Fatalf("Relative path not equals %s, got: %s", dockerfileTestName, relDockerfile)
	}
}

func TestValidateContextDirectoryEmptyContext(t *testing.T) {
	// This isn't a valid test on Windows. See https://play.golang.org/p/RR6z6jxR81.
	// The test will ultimately end up calling filepath.Abs(""). On Windows,
	// golang will error. On Linux, golang will return /. Due to there being
	// drive letters on Windows, this is probably the correct behaviour for
	// Windows.
	if runtime.GOOS == "windows" {
		t.Skip("Invalid test on Windows")
	}
	testValidateContextDirectory(t, prepareEmpty, []string{})
}

func TestValidateContextDirectoryContextWithNoFiles(t *testing.T) {
	testValidateContextDirectory(t, prepareNoFiles, []string{})
}

func TestValidateContextDirectoryWithOneFile(t *testing.T) {
	testValidateContextDirectory(t, prepareOneFile, []string{})
}

func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) {
	testValidateContextDirectory(t, prepareOneFile, []string{dockerfileTestName})
}