/*
 * Minio Cloud Storage, (C) 2017 Minio, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cmd

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"testing"
	"time"
)

func TestDownloadURL(t *testing.T) {
	minioVersion1 := UTCNow()
	durl := getDownloadURL(minioVersion1)
	if runtime.GOOS == "windows" {
		if durl != minioReleaseURL+"minio.exe" {
			t.Errorf("Expected %s, got %s", minioReleaseURL+"minio.exe", durl)
		}
	} else {
		if durl != minioReleaseURL+"minio" {
			t.Errorf("Expected %s, got %s", minioReleaseURL+"minio", durl)
		}
	}

	os.Setenv("KUBERNETES_SERVICE_HOST", "10.11.148.5")
	durl = getDownloadURL(minioVersion1)
	if durl != kubernetesDeploymentDoc {
		t.Errorf("Expected %s, got %s", kubernetesDeploymentDoc, durl)
	}
	os.Unsetenv("KUBERNETES_SERVICE_HOST")

	os.Setenv("MESOS_CONTAINER_NAME", "mesos-1111")
	durl = getDownloadURL(minioVersion1)
	if durl != mesosDeploymentDoc {
		t.Errorf("Expected %s, got %s", mesosDeploymentDoc, durl)
	}
	os.Unsetenv("MESOS_CONTAINER_NAME")
}

func TestGetCurrentReleaseTime(t *testing.T) {
	minioVersion1 := UTCNow().Format(time.RFC3339)
	releaseTime1, _ := time.Parse(time.RFC3339, minioVersion1)

	minioVersion2 := "DEVELOPMENT.GOGET"
	tmpfile1, err := ioutil.TempFile("", "get-current-release-time-testcase-1")
	if err != nil {
		t.Fatalf("Unable to create temporary file. %s", err)
	}
	defer os.Remove(tmpfile1.Name())

	minioBinaryPath2 := tmpfile1.Name()
	fi, err := tmpfile1.Stat()
	if err != nil {
		t.Fatalf("Unable to get temporary file info. %s", err)
	}
	if err = tmpfile1.Close(); err != nil {
		t.Fatalf("Unable to create temporary file. %s", err)
	}
	releaseTime2 := fi.ModTime().UTC()

	minioBinaryPath3 := "go"
	if runtime.GOOS == globalWindowsOSName {
		minioBinaryPath3 = "go.exe"
	}
	goBinAbsPath, err := exec.LookPath(minioBinaryPath3)
	if err != nil {
		t.Fatal(err)
	}
	fi, err = osStat(goBinAbsPath)
	if err != nil {
		t.Fatal(err)
	}
	releaseTime3 := fi.ModTime().UTC()

	// Get a non-absolute binary path.
	minioBinaryPath4 := filepath.Base(tmpfile1.Name())

	// Get a non-existent absolute binary path
	minioBinaryPath5 := "/tmp/non-existent-file"
	if runtime.GOOS == globalWindowsOSName {
		minioBinaryPath5 = "C:\\tmp\\non-existent-file"
	}
	errorMessage1 := "exec: \"\": executable file not found in $PATH"
	if runtime.GOOS == globalWindowsOSName {
		errorMessage1 = "exec: \"\": executable file not found in %PATH%"
	}
	errorMessage2 := "exec: \"non-existent-file\": executable file not found in $PATH"
	if runtime.GOOS == globalWindowsOSName {
		errorMessage2 = "exec: \"non-existent-file\": executable file not found in %PATH%"
	}
	errorMessage3 := fmt.Sprintf("exec: \"%s\": executable file not found in $PATH", minioBinaryPath4)
	if runtime.GOOS == globalWindowsOSName {
		errorMessage3 = "exec: \"" + minioBinaryPath4 + "\": executable file not found in %PATH%"
	}
	errorMessage4 := "Unable to get ModTime of /tmp/non-existent-file. stat /tmp/non-existent-file: no such file or directory"
	if runtime.GOOS == "windows" {
		errorMessage4 = "Unable to get ModTime of C:\\tmp\\non-existent-file. CreateFile C:\\tmp\\non-existent-file: The system cannot find the path specified."
	}

	testCases := []struct {
		minioVersion    string
		minioBinaryPath string
		expectedResult  time.Time
		expectedErr     error
	}{
		{minioVersion1, "", releaseTime1, nil},
		{minioVersion1, minioBinaryPath2, releaseTime1, nil},
		{minioVersion2, minioBinaryPath2, releaseTime2, nil},
		{minioVersion2, minioBinaryPath3, releaseTime3, nil},
		{"junk", minioBinaryPath2, releaseTime2, nil},
		{"3.2.0", minioBinaryPath2, releaseTime2, nil},
		{minioVersion2, "", time.Time{}, errors.New(errorMessage1)},
		{"junk", "non-existent-file", time.Time{}, errors.New(errorMessage2)},
		{"3.2.0", "non-existent-file", time.Time{}, errors.New(errorMessage2)},
		{minioVersion2, minioBinaryPath4, time.Time{}, errors.New(errorMessage3)},
		{minioVersion2, minioBinaryPath5, time.Time{}, errors.New(errorMessage4)},
	}

	if runtime.GOOS == "linux" {
		testCases = append(testCases, struct {
			minioVersion    string
			minioBinaryPath string
			expectedResult  time.Time
			expectedErr     error
		}{"3.2a", "/proc/1/cwd", time.Time{}, errors.New("Unable to get ModTime of /proc/1/cwd. stat /proc/1/cwd: permission denied")})
	}

	for _, testCase := range testCases {
		result, err := getCurrentReleaseTime(testCase.minioVersion, testCase.minioBinaryPath)
		if testCase.expectedErr == nil {
			if err != nil {
				t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
			}
		} else if err == nil {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		} else if testCase.expectedErr.Error() != err.Error() {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		}

		if !testCase.expectedResult.Equal(result) {
			t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result)
		}
	}
}

// Tests user agent string.
func TestUserAgent(t *testing.T) {
	testCases := []struct {
		envName     string
		envValue    string
		mode        string
		expectedStr string
	}{
		{
			envName:     "",
			envValue:    "",
			mode:        globalMinioModeFS,
			expectedStr: fmt.Sprintf("Minio (%s; %s; %s; source) Minio/DEVELOPMENT.GOGET Minio/DEVELOPMENT.GOGET Minio/DEVELOPMENT.GOGET", runtime.GOOS, runtime.GOARCH, globalMinioModeFS),
		},
		{
			envName:     "MESOS_CONTAINER_NAME",
			envValue:    "mesos-11111",
			mode:        globalMinioModeXL,
			expectedStr: fmt.Sprintf("Minio (%s; %s; %s; %s; source) Minio/DEVELOPMENT.GOGET Minio/DEVELOPMENT.GOGET Minio/DEVELOPMENT.GOGET Minio/universe-%s", runtime.GOOS, runtime.GOARCH, globalMinioModeXL, "dcos", "mesos-1111"),
		},
		{
			envName:     "KUBERNETES_SERVICE_HOST",
			envValue:    "10.11.148.5",
			mode:        globalMinioModeXL,
			expectedStr: fmt.Sprintf("Minio (%s; %s; %s; %s; source) Minio/DEVELOPMENT.GOGET Minio/DEVELOPMENT.GOGET Minio/DEVELOPMENT.GOGET", runtime.GOOS, runtime.GOARCH, globalMinioModeXL, "kubernetes"),
		},
	}

	for i, testCase := range testCases {
		os.Setenv(testCase.envName, testCase.envValue)
		if testCase.envName == "MESOS_CONTAINER_NAME" {
			os.Setenv("MARATHON_APP_LABEL_DCOS_PACKAGE_VERSION", "mesos-1111")
		}
		str := getUserAgent(testCase.mode)
		if str != testCase.expectedStr {
			t.Errorf("Test %d: expected: %s, got: %s", i+1, testCase.expectedStr, str)
		}
		os.Unsetenv("MARATHON_APP_LABEL_DCOS_PACKAGE_VERSION")
		os.Unsetenv(testCase.envName)
	}
}

// Tests if the environment we are running is in DCOS.
func TestIsDCOS(t *testing.T) {
	os.Setenv("MESOS_CONTAINER_NAME", "mesos-1111")
	dcos := IsDCOS()
	if !dcos {
		t.Fatalf("Expected %t, got %t", true, dcos)
	}

	os.Unsetenv("MESOS_CONTAINER_NAME")
	dcos = IsDCOS()
	if dcos {
		t.Fatalf("Expected %t, got %t", false, dcos)
	}
}

// Tests if the environment we are running is in kubernetes.
func TestIsKubernetes(t *testing.T) {
	os.Setenv("KUBERNETES_SERVICE_HOST", "10.11.148.5")
	kubernetes := IsKubernetes()
	if !kubernetes {
		t.Fatalf("Expected %t, got %t", true, kubernetes)
	}
	os.Unsetenv("KUBERNETES_SERVICE_HOST")
	kubernetes = IsKubernetes()
	if kubernetes {
		t.Fatalf("Expected %t, got %t", false, kubernetes)
	}
}

// Tests if the environment we are running is in docker.
func TestIsDocker(t *testing.T) {
	createTempFile := func(content string) string {
		tmpfile, err := ioutil.TempFile("", "isdocker-testcase")
		if err != nil {
			t.Fatalf("Unable to create temporary file. %s", err)
		}
		if _, err = tmpfile.Write([]byte(content)); err != nil {
			t.Fatalf("Unable to create temporary file. %s", err)
		}
		if err = tmpfile.Close(); err != nil {
			t.Fatalf("Unable to create temporary file. %s", err)
		}
		return tmpfile.Name()
	}

	filename := createTempFile("")
	defer os.Remove(filename)

	testCases := []struct {
		filename       string
		expectedResult bool
		expectedErr    error
	}{
		{"", false, nil},
		{"/tmp/non-existing-file", false, nil},
		{filename, true, nil},
	}

	if runtime.GOOS == "linux" {
		testCases = append(testCases, struct {
			filename       string
			expectedResult bool
			expectedErr    error
		}{"/proc/1/cwd", false, errors.New("stat /proc/1/cwd: permission denied")})
	}

	for _, testCase := range testCases {
		result, err := isDocker(testCase.filename)
		if testCase.expectedErr == nil {
			if err != nil {
				t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
			}
		} else if err == nil {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		} else if testCase.expectedErr.Error() != err.Error() {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		}

		if testCase.expectedResult != result {
			t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result)
		}
	}
}

func TestIsSourceBuild(t *testing.T) {
	testCases := []struct {
		minioVersion   string
		expectedResult bool
	}{
		{UTCNow().Format(time.RFC3339), false},
		{"DEVELOPMENT.GOGET", true},
		{"junk", true},
		{"3.2.4", true},
	}

	for _, testCase := range testCases {
		result := isSourceBuild(testCase.minioVersion)
		if testCase.expectedResult != result {
			t.Fatalf("expected: %v, got: %v", testCase.expectedResult, result)
		}
	}
}

func TestDownloadReleaseData(t *testing.T) {
	httpServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
	defer httpServer1.Close()
	httpServer2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z")
	}))
	defer httpServer2.Close()
	httpServer3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "", http.StatusNotFound)
	}))
	defer httpServer3.Close()

	testCases := []struct {
		releaseChecksumURL string
		expectedResult     string
		expectedErr        error
	}{
		{httpServer1.URL, "", nil},
		{httpServer2.URL, "fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z\n", nil},
		{httpServer3.URL, "", fmt.Errorf("Error downloading URL " + httpServer3.URL + ". Response: 404 Not Found")},
	}

	for _, testCase := range testCases {
		result, err := downloadReleaseData(testCase.releaseChecksumURL, 1*time.Second, "")
		if testCase.expectedErr == nil {
			if err != nil {
				t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
			}
		} else if err == nil {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		} else if testCase.expectedErr.Error() != err.Error() {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		}

		if testCase.expectedResult != result {
			t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result)
		}
	}
}

func TestParseReleaseData(t *testing.T) {
	releaseTime, _ := time.Parse(minioReleaseTagTimeLayout, "2016-10-07T01-16-39Z")
	testCases := []struct {
		data           string
		expectedResult time.Time
		expectedErr    error
	}{
		{"more than two fields", time.Time{}, fmt.Errorf("Unknown release data `more than two fields`")},
		{"more than", time.Time{}, fmt.Errorf("Unknown release information `than`")},
		{"more than.two.fields", time.Time{}, fmt.Errorf("Unknown release 'than.two.fields'")},
		{"more minio.RELEASE.fields", time.Time{}, fmt.Errorf(`Unknown release time format. parsing time "fields" as "2006-01-02T15-04-05Z": cannot parse "fields" as "2006"`)},
		{"more minio.RELEASE.2016-10-07T01-16-39Z", releaseTime, nil},
		{"fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z\n", releaseTime, nil},
	}

	for _, testCase := range testCases {
		result, err := parseReleaseData(testCase.data)
		if testCase.expectedErr == nil {
			if err != nil {
				t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
			}
		} else if err == nil {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		} else if testCase.expectedErr.Error() != err.Error() {
			t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err)
		}

		if !testCase.expectedResult.Equal(result) {
			t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result)
		}
	}
}
