package docker

import (
	"encoding/base64"
	"encoding/json"
	//"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"github.com/containers/image/types"
	"github.com/containers/storage/pkg/homedir"
	"github.com/stretchr/testify/assert"
)

func TestDockerCertDir(t *testing.T) {
	const nondefaultFullPath = "/this/is/not/the/default/full/path"
	const nondefaultPerHostDir = "/this/is/not/the/default/certs.d"
	const variableReference = "$HOME"
	const rootPrefix = "/root/prefix"
	const registryHostPort = "localhost:5000"

	systemPerHostResult := filepath.Join(systemPerHostCertDirPath, registryHostPort)
	for _, c := range []struct {
		ctx      *types.SystemContext
		expected string
	}{
		// The common case
		{nil, systemPerHostResult},
		// There is a context, but it does not override the path.
		{&types.SystemContext{}, systemPerHostResult},
		// Full path overridden
		{&types.SystemContext{DockerCertPath: nondefaultFullPath}, nondefaultFullPath},
		// Per-host path overridden
		{
			&types.SystemContext{DockerPerHostCertDirPath: nondefaultPerHostDir},
			filepath.Join(nondefaultPerHostDir, registryHostPort),
		},
		// Both overridden
		{
			&types.SystemContext{
				DockerCertPath:           nondefaultFullPath,
				DockerPerHostCertDirPath: nondefaultPerHostDir,
			},
			nondefaultFullPath,
		},
		// Root overridden
		{
			&types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix},
			filepath.Join(rootPrefix, systemPerHostResult),
		},
		// Root and path overrides present simultaneously,
		{
			&types.SystemContext{
				DockerCertPath:               nondefaultFullPath,
				RootForImplicitAbsolutePaths: rootPrefix,
			},
			nondefaultFullPath,
		},
		{
			&types.SystemContext{
				DockerPerHostCertDirPath:     nondefaultPerHostDir,
				RootForImplicitAbsolutePaths: rootPrefix,
			},
			filepath.Join(nondefaultPerHostDir, registryHostPort),
		},
		// … and everything at once
		{
			&types.SystemContext{
				DockerCertPath:               nondefaultFullPath,
				DockerPerHostCertDirPath:     nondefaultPerHostDir,
				RootForImplicitAbsolutePaths: rootPrefix,
			},
			nondefaultFullPath,
		},
		// No environment expansion happens in the overridden paths
		{&types.SystemContext{DockerCertPath: variableReference}, variableReference},
		{
			&types.SystemContext{DockerPerHostCertDirPath: variableReference},
			filepath.Join(variableReference, registryHostPort),
		},
	} {
		path := dockerCertDir(c.ctx, registryHostPort)
		assert.Equal(t, c.expected, path)
	}
}

func TestGetAuth(t *testing.T) {
	origHomeDir := homedir.Get()
	tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("using temporary home directory: %q", tmpDir)
	// override homedir
	os.Setenv(homedir.Key(), tmpDir)
	defer func() {
		err := os.RemoveAll(tmpDir)
		if err != nil {
			t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
		}
		os.Setenv(homedir.Key(), origHomeDir)
	}()

	configDir := filepath.Join(tmpDir, ".docker")
	if err := os.Mkdir(configDir, 0750); err != nil {
		t.Fatal(err)
	}
	configPath := filepath.Join(configDir, "config.json")

	for _, tc := range []struct {
		name             string
		hostname         string
		authConfig       testAuthConfig
		expectedUsername string
		expectedPassword string
		expectedError    error
		ctx              *types.SystemContext
	}{
		{
			name:       "empty hostname",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{"localhost:5000": testAuthConfigData{"bob", "password"}}),
		},
		{
			name:     "no auth config",
			hostname: "index.docker.io",
		},
		{
			name:             "match one",
			hostname:         "example.org",
			authConfig:       makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
			expectedUsername: "joe",
			expectedPassword: "mypass",
		},
		{
			name:       "match none",
			hostname:   "registry.example.org",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
		},
		{
			name:     "match docker.io",
			hostname: "docker.io",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"example.org":     testAuthConfigData{"example", "org"},
				"index.docker.io": testAuthConfigData{"index", "docker.io"},
				"docker.io":       testAuthConfigData{"docker", "io"},
			}),
			expectedUsername: "docker",
			expectedPassword: "io",
		},
		{
			name:     "match docker.io normalized",
			hostname: "docker.io",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"example.org":                testAuthConfigData{"bob", "pw"},
				"https://index.docker.io/v1": testAuthConfigData{"alice", "wp"},
			}),
			expectedUsername: "alice",
			expectedPassword: "wp",
		},
		{
			name:     "normalize registry",
			hostname: "https://docker.io/v1",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"docker.io":      testAuthConfigData{"user", "pw"},
				"localhost:5000": testAuthConfigData{"joe", "pass"},
			}),
			expectedUsername: "user",
			expectedPassword: "pw",
		},
		{
			name:     "match localhost",
			hostname: "http://localhost",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"docker.io":   testAuthConfigData{"user", "pw"},
				"localhost":   testAuthConfigData{"joe", "pass"},
				"example.com": testAuthConfigData{"alice", "pwd"},
			}),
			expectedUsername: "joe",
			expectedPassword: "pass",
		},
		{
			name:     "match ip",
			hostname: "10.10.3.56:5000",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"10.10.30.45":     testAuthConfigData{"user", "pw"},
				"localhost":       testAuthConfigData{"joe", "pass"},
				"10.10.3.56":      testAuthConfigData{"alice", "pwd"},
				"10.10.3.56:5000": testAuthConfigData{"me", "mine"},
			}),
			expectedUsername: "me",
			expectedPassword: "mine",
		},
		{
			name:     "match port",
			hostname: "https://localhost:5000",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"https://127.0.0.1:5000": testAuthConfigData{"user", "pw"},
				"http://localhost":       testAuthConfigData{"joe", "pass"},
				"https://localhost:5001": testAuthConfigData{"alice", "pwd"},
				"localhost:5000":         testAuthConfigData{"me", "mine"},
			}),
			expectedUsername: "me",
			expectedPassword: "mine",
		},
		{
			name:     "use system context",
			hostname: "example.org",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"example.org": testAuthConfigData{"user", "pw"},
			}),
			expectedUsername: "foo",
			expectedPassword: "bar",
			ctx: &types.SystemContext{
				DockerAuthConfig: &types.DockerAuthConfig{
					Username: "foo",
					Password: "bar",
				},
			},
		},
	} {
		contents, err := json.MarshalIndent(&tc.authConfig, "", "  ")
		if err != nil {
			t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err)
			continue
		}
		if err := ioutil.WriteFile(configPath, contents, 0640); err != nil {
			t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err)
			continue
		}

		var ctx *types.SystemContext
		if tc.ctx != nil {
			ctx = tc.ctx
		}
		username, password, err := getAuth(ctx, tc.hostname)
		if err == nil && tc.expectedError != nil {
			t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password)
			continue
		}
		if err != nil && tc.expectedError == nil {
			t.Errorf("[%s] got unexpected error: %#+v", tc.name, err)
			continue
		}
		if !reflect.DeepEqual(err, tc.expectedError) {
			t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
			continue
		}

		if username != tc.expectedUsername {
			t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername)
		}
		if password != tc.expectedPassword {
			t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword)
		}
	}
}

func TestGetAuthFromLegacyFile(t *testing.T) {
	origHomeDir := homedir.Get()
	tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("using temporary home directory: %q", tmpDir)
	// override homedir
	os.Setenv(homedir.Key(), tmpDir)
	defer func() {
		err := os.RemoveAll(tmpDir)
		if err != nil {
			t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
		}
		os.Setenv(homedir.Key(), origHomeDir)
	}()

	configPath := filepath.Join(tmpDir, ".dockercfg")

	for _, tc := range []struct {
		name             string
		hostname         string
		authConfig       testAuthConfig
		expectedUsername string
		expectedPassword string
		expectedError    error
	}{
		{
			name:     "normalize registry",
			hostname: "https://docker.io/v1",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"docker.io":      testAuthConfigData{"user", "pw"},
				"localhost:5000": testAuthConfigData{"joe", "pass"},
			}),
			expectedUsername: "user",
			expectedPassword: "pw",
		},
		{
			name:     "ignore schema and path",
			hostname: "http://index.docker.io/v1",
			authConfig: makeTestAuthConfig(testAuthConfigDataMap{
				"docker.io/v2":         testAuthConfigData{"user", "pw"},
				"https://localhost/v1": testAuthConfigData{"joe", "pwd"},
			}),
			expectedUsername: "user",
			expectedPassword: "pw",
		},
	} {
		contents, err := json.MarshalIndent(&tc.authConfig.Auths, "", "  ")
		if err != nil {
			t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err)
			continue
		}
		if err := ioutil.WriteFile(configPath, contents, 0640); err != nil {
			t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err)
			continue
		}

		username, password, err := getAuth(nil, tc.hostname)
		if err == nil && tc.expectedError != nil {
			t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password)
			continue
		}
		if err != nil && tc.expectedError == nil {
			t.Errorf("[%s] got unexpected error: %#+v", tc.name, err)
			continue
		}
		if !reflect.DeepEqual(err, tc.expectedError) {
			t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
			continue
		}

		if username != tc.expectedUsername {
			t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername)
		}
		if password != tc.expectedPassword {
			t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword)
		}
	}
}

func TestGetAuthPreferNewConfig(t *testing.T) {
	origHomeDir := homedir.Get()
	tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("using temporary home directory: %q", tmpDir)
	// override homedir
	os.Setenv(homedir.Key(), tmpDir)
	defer func() {
		err := os.RemoveAll(tmpDir)
		if err != nil {
			t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
		}
		os.Setenv(homedir.Key(), origHomeDir)
	}()

	configDir := filepath.Join(tmpDir, ".docker")
	if err := os.Mkdir(configDir, 0750); err != nil {
		t.Fatal(err)
	}

	for _, data := range []struct {
		path string
		ac   interface{}
	}{
		{
			filepath.Join(configDir, "config.json"),
			makeTestAuthConfig(testAuthConfigDataMap{
				"https://index.docker.io/v1/": testAuthConfigData{"alice", "pass"},
			}),
		},
		{
			filepath.Join(tmpDir, ".dockercfg"),
			makeTestAuthConfig(testAuthConfigDataMap{
				"https://index.docker.io/v1/": testAuthConfigData{"bob", "pw"},
			}).Auths,
		},
	} {
		contents, err := json.MarshalIndent(&data.ac, "", "  ")
		if err != nil {
			t.Fatalf("failed to marshal authConfig: %v", err)
		}
		if err := ioutil.WriteFile(data.path, contents, 0640); err != nil {
			t.Fatalf("failed to write file %q: %v", data.path, err)
		}
	}

	username, password, err := getAuth(nil, "index.docker.io")
	if err != nil {
		t.Fatalf("got unexpected error: %#+v", err)
	}

	if username != "alice" {
		t.Fatalf("got unexpected user name: %q != %q", username, "alice")
	}
	if password != "pass" {
		t.Fatalf("got unexpected user name: %q != %q", password, "pass")
	}
}

func TestGetAuthFailsOnBadInput(t *testing.T) {
	origHomeDir := homedir.Get()
	tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("using temporary home directory: %q", tmpDir)
	// override homedir
	os.Setenv(homedir.Key(), tmpDir)
	defer func() {
		err := os.RemoveAll(tmpDir)
		if err != nil {
			t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
		}
		os.Setenv(homedir.Key(), origHomeDir)
	}()

	configDir := filepath.Join(tmpDir, ".docker")
	if err := os.Mkdir(configDir, 0750); err != nil {
		t.Fatal(err)
	}
	configPath := filepath.Join(configDir, "config.json")

	// no config file present
	username, password, err := getAuth(nil, "index.docker.io")
	if err != nil {
		t.Fatalf("got unexpected error: %#+v", err)
	}
	if len(username) > 0 || len(password) > 0 {
		t.Fatalf("got unexpected not empty username/password: %q/%q", username, password)
	}

	if err := ioutil.WriteFile(configPath, []byte("Json rocks! Unless it doesn't."), 0640); err != nil {
		t.Fatalf("failed to write file %q: %v", configPath, err)
	}
	username, password, err = getAuth(nil, "index.docker.io")
	if err == nil {
		t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password)
	}
	if _, ok := err.(*json.SyntaxError); !ok {
		t.Fatalf("expected os.PathError, not: %#+v", err)
	}

	// remove the invalid config file
	os.RemoveAll(configPath)
	// no config file present
	username, password, err = getAuth(nil, "index.docker.io")
	if err != nil {
		t.Fatalf("got unexpected error: %#+v", err)
	}
	if len(username) > 0 || len(password) > 0 {
		t.Fatalf("got unexpected not empty username/password: %q/%q", username, password)
	}

	configPath = filepath.Join(tmpDir, ".dockercfg")
	if err := ioutil.WriteFile(configPath, []byte("I'm certainly not a json string."), 0640); err != nil {
		t.Fatalf("failed to write file %q: %v", configPath, err)
	}
	username, password, err = getAuth(nil, "index.docker.io")
	if err == nil {
		t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password)
	}
	if _, ok := err.(*json.SyntaxError); !ok {
		t.Fatalf("expected os.PathError, not: %#+v", err)
	}
}

type testAuthConfigData struct {
	username string
	password string
}

type testAuthConfigDataMap map[string]testAuthConfigData

type testAuthConfigEntry struct {
	Auth string `json:"auth,omitempty"`
}

type testAuthConfig struct {
	Auths map[string]testAuthConfigEntry `json:"auths"`
}

// encodeAuth creates an auth value from given authConfig data to be stored in auth config file.
// Inspired by github.com/docker/docker/cliconfig/config.go v1.10.3.
func encodeAuth(authConfig *testAuthConfigData) string {
	authStr := authConfig.username + ":" + authConfig.password
	msg := []byte(authStr)
	encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
	base64.StdEncoding.Encode(encoded, msg)
	return string(encoded)
}

func makeTestAuthConfig(authConfigData map[string]testAuthConfigData) testAuthConfig {
	ac := testAuthConfig{
		Auths: make(map[string]testAuthConfigEntry),
	}
	for host, data := range authConfigData {
		ac.Auths[host] = testAuthConfigEntry{
			Auth: encodeAuth(&data),
		}
	}
	return ac
}
