package configfile

import (
	"bytes"
	"encoding/json"
	"errors"
	"os"
	"testing"

	"github.com/docker/cli/cli/config/credentials"
	"github.com/docker/cli/cli/config/types"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/fs"
	"gotest.tools/v3/golden"
)

func TestEncodeAuth(t *testing.T) {
	newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"}
	authStr := encodeAuth(newAuthConfig)

	expected := &types.AuthConfig{}
	var err error
	expected.Username, expected.Password, err = decodeAuth(authStr)
	assert.NilError(t, err)
	assert.Check(t, is.DeepEqual(expected, newAuthConfig))
}

func TestProxyConfig(t *testing.T) {
	var (
		httpProxy  = "http://proxy.mycorp.example.com:3128"
		httpsProxy = "https://user:password@proxy.mycorp.example.com:3129"
		ftpProxy   = "http://ftpproxy.mycorp.example.com:21"
		noProxy    = "*.intra.mycorp.example.com"
		allProxy   = "socks://example.com:1234"

		defaultProxyConfig = ProxyConfig{
			HTTPProxy:  httpProxy,
			HTTPSProxy: httpsProxy,
			FTPProxy:   ftpProxy,
			NoProxy:    noProxy,
			AllProxy:   allProxy,
		}
	)

	cfg := ConfigFile{
		Proxies: map[string]ProxyConfig{
			"default": defaultProxyConfig,
		},
	}

	proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", nil)
	expected := map[string]*string{
		"HTTP_PROXY":  &httpProxy,
		"http_proxy":  &httpProxy,
		"HTTPS_PROXY": &httpsProxy,
		"https_proxy": &httpsProxy,
		"FTP_PROXY":   &ftpProxy,
		"ftp_proxy":   &ftpProxy,
		"NO_PROXY":    &noProxy,
		"no_proxy":    &noProxy,
		"ALL_PROXY":   &allProxy,
		"all_proxy":   &allProxy,
	}
	assert.Check(t, is.DeepEqual(expected, proxyConfig))
}

func TestProxyConfigOverride(t *testing.T) {
	var (
		httpProxy         = "http://proxy.mycorp.example.com:3128"
		httpProxyOverride = "http://proxy.example.com:3128"
		httpsProxy        = "https://user:password@proxy.mycorp.example.com:3129"
		ftpProxy          = "http://ftpproxy.mycorp.example.com:21"
		noProxy           = "*.intra.mycorp.example.com"
		noProxyOverride   = ""

		defaultProxyConfig = ProxyConfig{
			HTTPProxy:  httpProxy,
			HTTPSProxy: httpsProxy,
			FTPProxy:   ftpProxy,
			NoProxy:    noProxy,
		}
	)

	cfg := ConfigFile{
		Proxies: map[string]ProxyConfig{
			"default": defaultProxyConfig,
		},
	}

	clone := func(s string) *string {
		s2 := s
		return &s2
	}

	ropts := map[string]*string{
		"HTTP_PROXY": clone(httpProxyOverride),
		"NO_PROXY":   clone(noProxyOverride),
	}
	proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", ropts)
	expected := map[string]*string{
		"HTTP_PROXY":  &httpProxyOverride,
		"http_proxy":  &httpProxy,
		"HTTPS_PROXY": &httpsProxy,
		"https_proxy": &httpsProxy,
		"FTP_PROXY":   &ftpProxy,
		"ftp_proxy":   &ftpProxy,
		"NO_PROXY":    &noProxyOverride,
		"no_proxy":    &noProxy,
	}
	assert.Check(t, is.DeepEqual(expected, proxyConfig))
}

func TestProxyConfigPerHost(t *testing.T) {
	var (
		httpProxy  = "http://proxy.mycorp.example.com:3128"
		httpsProxy = "https://user:password@proxy.mycorp.example.com:3129"
		ftpProxy   = "http://ftpproxy.mycorp.example.com:21"
		noProxy    = "*.intra.mycorp.example.com"

		extHTTPProxy  = "http://proxy.example.com:3128"
		extHTTPSProxy = "https://user:password@proxy.example.com:3129"
		extFTPProxy   = "http://ftpproxy.example.com:21"
		extNoProxy    = "*.intra.example.com"

		defaultProxyConfig = ProxyConfig{
			HTTPProxy:  httpProxy,
			HTTPSProxy: httpsProxy,
			FTPProxy:   ftpProxy,
			NoProxy:    noProxy,
		}

		externalProxyConfig = ProxyConfig{
			HTTPProxy:  extHTTPProxy,
			HTTPSProxy: extHTTPSProxy,
			FTPProxy:   extFTPProxy,
			NoProxy:    extNoProxy,
		}
	)

	cfg := ConfigFile{
		Proxies: map[string]ProxyConfig{
			"default":                       defaultProxyConfig,
			"tcp://example.docker.com:2376": externalProxyConfig,
		},
	}

	proxyConfig := cfg.ParseProxyConfig("tcp://example.docker.com:2376", nil)
	expected := map[string]*string{
		"HTTP_PROXY":  &extHTTPProxy,
		"http_proxy":  &extHTTPProxy,
		"HTTPS_PROXY": &extHTTPSProxy,
		"https_proxy": &extHTTPSProxy,
		"FTP_PROXY":   &extFTPProxy,
		"ftp_proxy":   &extFTPProxy,
		"NO_PROXY":    &extNoProxy,
		"no_proxy":    &extNoProxy,
	}
	assert.Check(t, is.DeepEqual(expected, proxyConfig))
}

func TestConfigFile(t *testing.T) {
	configFilename := "configFilename"
	configFile := New(configFilename)

	assert.Check(t, is.Equal(configFilename, configFile.Filename))
}

type mockNativeStore struct {
	GetAllCallCount  int
	authConfigs      map[string]types.AuthConfig
	authConfigErrors map[string]error
}

func (c *mockNativeStore) Erase(registryHostname string) error {
	delete(c.authConfigs, registryHostname)
	return nil
}

func (c *mockNativeStore) Get(registryHostname string) (types.AuthConfig, error) {
	return c.authConfigs[registryHostname], c.authConfigErrors[registryHostname]
}

func (c *mockNativeStore) GetAll() (map[string]types.AuthConfig, error) {
	c.GetAllCallCount++
	return c.authConfigs, nil
}

func (c *mockNativeStore) Store(_ types.AuthConfig) error {
	return nil
}

// make sure it satisfies the interface
var _ credentials.Store = (*mockNativeStore)(nil)

func NewMockNativeStore(authConfigs map[string]types.AuthConfig, authConfigErrors map[string]error) credentials.Store {
	return &mockNativeStore{authConfigs: authConfigs, authConfigErrors: authConfigErrors}
}

func TestGetAllCredentialsFileStoreOnly(t *testing.T) {
	configFile := New("filename")
	exampleAuth := types.AuthConfig{
		Username: "user",
		Password: "pass",
	}
	configFile.AuthConfigs["example.com/foo"] = exampleAuth

	authConfigs, err := configFile.GetAllCredentials()
	assert.NilError(t, err)

	expected := make(map[string]types.AuthConfig)
	expected["example.com/foo"] = exampleAuth
	assert.Check(t, is.DeepEqual(expected, authConfigs))
}

func TestGetAllCredentialsCredsStore(t *testing.T) {
	configFile := New("filename")
	configFile.CredentialsStore = "test_creds_store"
	testRegistryHostname := "example.com"
	expectedAuth := types.AuthConfig{
		Username: "user",
		Password: "pass",
	}

	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: expectedAuth}, nil)

	tmpNewNativeStore := newNativeStore
	defer func() { newNativeStore = tmpNewNativeStore }()
	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
		return testCredsStore
	}

	authConfigs, err := configFile.GetAllCredentials()
	assert.NilError(t, err)

	expected := make(map[string]types.AuthConfig)
	expected[testRegistryHostname] = expectedAuth
	assert.Check(t, is.DeepEqual(expected, authConfigs))
	assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount))
}

func TestGetAllCredentialsCredStoreErrorHandling(t *testing.T) {
	const (
		workingHelperRegistryHostname = "working-helper.example.com"
		brokenHelperRegistryHostname  = "broken-helper.example.com"
	)
	configFile := New("filename")
	configFile.CredentialHelpers = map[string]string{
		workingHelperRegistryHostname: "cred_helper",
		brokenHelperRegistryHostname:  "broken_cred_helper",
	}
	expectedAuth := types.AuthConfig{
		Username: "username",
		Password: "pass",
	}
	// configure the mock store to throw an error
	// when calling the helper for this registry
	authErrors := map[string]error{
		brokenHelperRegistryHostname: errors.New("an error"),
	}

	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{
		workingHelperRegistryHostname: expectedAuth,
		// configure an auth entry for the "broken" credential
		// helper that will throw an error
		brokenHelperRegistryHostname: {},
	}, authErrors)

	tmpNewNativeStore := newNativeStore
	defer func() { newNativeStore = tmpNewNativeStore }()
	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
		return testCredsStore
	}

	authConfigs, err := configFile.GetAllCredentials()

	// make sure we're still returning the expected credentials
	// and skipping the ones throwing an error
	assert.NilError(t, err)
	assert.Check(t, is.Equal(1, len(authConfigs)))
	assert.Check(t, is.DeepEqual(expectedAuth, authConfigs[workingHelperRegistryHostname]))
}

func TestGetAllCredentialsCredHelper(t *testing.T) {
	const (
		testCredHelperSuffix                = "test_cred_helper"
		testCredHelperRegistryHostname      = "credhelper.com"
		testExtraCredHelperRegistryHostname = "somethingweird.com"
	)

	unexpectedCredHelperAuth := types.AuthConfig{
		Username: "file_store_user",
		Password: "file_store_pass",
	}
	expectedCredHelperAuth := types.AuthConfig{
		Username: "cred_helper_user",
		Password: "cred_helper_pass",
	}

	configFile := New("filename")
	configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix}

	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{
		testCredHelperRegistryHostname: expectedCredHelperAuth,
		// Add in an extra auth entry which doesn't appear in CredentialHelpers section of the configFile.
		// This verifies that only explicitly configured registries are being requested from the cred helpers.
		testExtraCredHelperRegistryHostname: unexpectedCredHelperAuth,
	}, nil)

	tmpNewNativeStore := newNativeStore
	defer func() { newNativeStore = tmpNewNativeStore }()
	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
		return testCredHelper
	}

	authConfigs, err := configFile.GetAllCredentials()
	assert.NilError(t, err)

	expected := make(map[string]types.AuthConfig)
	expected[testCredHelperRegistryHostname] = expectedCredHelperAuth
	assert.Check(t, is.DeepEqual(expected, authConfigs))
	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
}

func TestGetAllCredentialsFileStoreAndCredHelper(t *testing.T) {
	const (
		testFileStoreRegistryHostname  = "example.com"
		testCredHelperSuffix           = "test_cred_helper"
		testCredHelperRegistryHostname = "credhelper.com"
	)

	expectedFileStoreAuth := types.AuthConfig{
		Username: "file_store_user",
		Password: "file_store_pass",
	}
	expectedCredHelperAuth := types.AuthConfig{
		Username: "cred_helper_user",
		Password: "cred_helper_pass",
	}

	configFile := New("filename")
	configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix}
	configFile.AuthConfigs[testFileStoreRegistryHostname] = expectedFileStoreAuth

	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testCredHelperRegistryHostname: expectedCredHelperAuth}, nil)

	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
		return testCredHelper
	}

	tmpNewNativeStore := newNativeStore
	defer func() { newNativeStore = tmpNewNativeStore }()
	authConfigs, err := configFile.GetAllCredentials()
	assert.NilError(t, err)

	expected := make(map[string]types.AuthConfig)
	expected[testFileStoreRegistryHostname] = expectedFileStoreAuth
	expected[testCredHelperRegistryHostname] = expectedCredHelperAuth
	assert.Check(t, is.DeepEqual(expected, authConfigs))
	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
}

func TestGetAllCredentialsCredStoreAndCredHelper(t *testing.T) {
	const (
		testCredStoreSuffix            = "test_creds_store"
		testCredStoreRegistryHostname  = "credstore.com"
		testCredHelperSuffix           = "test_cred_helper"
		testCredHelperRegistryHostname = "credhelper.com"
	)

	configFile := New("filename")
	configFile.CredentialsStore = testCredStoreSuffix
	configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix}

	expectedCredStoreAuth := types.AuthConfig{
		Username: "cred_store_user",
		Password: "cred_store_pass",
	}
	expectedCredHelperAuth := types.AuthConfig{
		Username: "cred_helper_user",
		Password: "cred_helper_pass",
	}

	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testCredHelperRegistryHostname: expectedCredHelperAuth}, nil)
	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testCredStoreRegistryHostname: expectedCredStoreAuth}, nil)

	tmpNewNativeStore := newNativeStore
	defer func() { newNativeStore = tmpNewNativeStore }()
	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
		if helperSuffix == testCredHelperSuffix {
			return testCredHelper
		}
		return testCredsStore
	}

	authConfigs, err := configFile.GetAllCredentials()
	assert.NilError(t, err)

	expected := make(map[string]types.AuthConfig)
	expected[testCredStoreRegistryHostname] = expectedCredStoreAuth
	expected[testCredHelperRegistryHostname] = expectedCredHelperAuth
	assert.Check(t, is.DeepEqual(expected, authConfigs))
	assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount))
	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
}

func TestGetAllCredentialsCredHelperOverridesDefaultStore(t *testing.T) {
	const (
		testCredStoreSuffix  = "test_creds_store"
		testCredHelperSuffix = "test_cred_helper"
		testRegistryHostname = "example.com"
	)

	configFile := New("filename")
	configFile.CredentialsStore = testCredStoreSuffix
	configFile.CredentialHelpers = map[string]string{testRegistryHostname: testCredHelperSuffix}

	unexpectedCredStoreAuth := types.AuthConfig{
		Username: "cred_store_user",
		Password: "cred_store_pass",
	}
	expectedCredHelperAuth := types.AuthConfig{
		Username: "cred_helper_user",
		Password: "cred_helper_pass",
	}

	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: expectedCredHelperAuth}, nil)
	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: unexpectedCredStoreAuth}, nil)

	tmpNewNativeStore := newNativeStore
	defer func() { newNativeStore = tmpNewNativeStore }()
	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
		if helperSuffix == testCredHelperSuffix {
			return testCredHelper
		}
		return testCredsStore
	}

	authConfigs, err := configFile.GetAllCredentials()
	assert.NilError(t, err)

	expected := make(map[string]types.AuthConfig)
	expected[testRegistryHostname] = expectedCredHelperAuth
	assert.Check(t, is.DeepEqual(expected, authConfigs))
	assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount))
	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
}

func TestLoadFromReaderWithUsernamePassword(t *testing.T) {
	configFile := New("test-load")
	defer os.Remove("test-load")

	want := types.AuthConfig{
		Username: "user",
		Password: "pass",
	}

	for _, tc := range []types.AuthConfig{
		want,
		{
			Auth: encodeAuth(&want),
		},
	} {
		cf := ConfigFile{
			AuthConfigs: map[string]types.AuthConfig{
				"example.com/foo": tc,
			},
		}

		b, err := json.Marshal(cf)
		assert.NilError(t, err)

		err = configFile.LoadFromReader(bytes.NewReader(b))
		assert.NilError(t, err)

		got, err := configFile.GetAuthConfig("example.com/foo")
		assert.NilError(t, err)

		assert.Check(t, is.DeepEqual(want.Username, got.Username))
		assert.Check(t, is.DeepEqual(want.Password, got.Password))
	}
}

func TestSave(t *testing.T) {
	configFile := New("test-save")
	defer os.Remove("test-save")
	err := configFile.Save()
	assert.NilError(t, err)
	cfg, err := os.ReadFile("test-save")
	assert.NilError(t, err)
	assert.Equal(t, string(cfg), `{
	"auths": {}
}`)
}

func TestSaveCustomHTTPHeaders(t *testing.T) {
	configFile := New(t.Name())
	defer os.Remove(t.Name())
	configFile.HTTPHeaders["CUSTOM-HEADER"] = "custom-value"
	configFile.HTTPHeaders["User-Agent"] = "user-agent 1"
	configFile.HTTPHeaders["user-agent"] = "user-agent 2"
	err := configFile.Save()
	assert.NilError(t, err)
	cfg, err := os.ReadFile(t.Name())
	assert.NilError(t, err)
	assert.Equal(t, string(cfg), `{
	"auths": {},
	"HttpHeaders": {
		"CUSTOM-HEADER": "custom-value"
	}
}`)
}

func TestSaveWithSymlink(t *testing.T) {
	dir := fs.NewDir(t, t.Name(), fs.WithFile("real-config.json", `{}`))
	defer dir.Remove()

	symLink := dir.Join("config.json")
	realFile := dir.Join("real-config.json")
	err := os.Symlink(realFile, symLink)
	assert.NilError(t, err)

	configFile := New(symLink)

	err = configFile.Save()
	assert.NilError(t, err)

	fi, err := os.Lstat(symLink)
	assert.NilError(t, err)
	assert.Assert(t, fi.Mode()&os.ModeSymlink != 0, "expected %s to be a symlink", symLink)

	cfg, err := os.ReadFile(symLink)
	assert.NilError(t, err)
	assert.Check(t, is.Equal(string(cfg), "{\n	\"auths\": {}\n}"))

	cfg, err = os.ReadFile(realFile)
	assert.NilError(t, err)
	assert.Check(t, is.Equal(string(cfg), "{\n	\"auths\": {}\n}"))
}

func TestPluginConfig(t *testing.T) {
	configFile := New("test-plugin")
	defer os.Remove("test-plugin")

	// Populate some initial values
	configFile.SetPluginConfig("plugin1", "data1", "some string")
	configFile.SetPluginConfig("plugin1", "data2", "42")
	configFile.SetPluginConfig("plugin2", "data3", "some other string")

	// Save a config file with some plugin config
	err := configFile.Save()
	assert.NilError(t, err)

	// Read it back and check it has the expected content
	cfg, err := os.ReadFile("test-plugin")
	assert.NilError(t, err)
	golden.Assert(t, string(cfg), "plugin-config.golden")

	// Load it, resave and check again that the content is
	// preserved through a load/save cycle.
	configFile = New("test-plugin2")
	defer os.Remove("test-plugin2")
	assert.NilError(t, configFile.LoadFromReader(bytes.NewReader(cfg)))
	err = configFile.Save()
	assert.NilError(t, err)
	cfg, err = os.ReadFile("test-plugin2")
	assert.NilError(t, err)
	golden.Assert(t, string(cfg), "plugin-config.golden")

	// Check that the contents was reloaded properly
	v, ok := configFile.PluginConfig("plugin1", "data1")
	assert.Assert(t, ok)
	assert.Equal(t, v, "some string")
	v, ok = configFile.PluginConfig("plugin1", "data2")
	assert.Assert(t, ok)
	assert.Equal(t, v, "42")
	v, ok = configFile.PluginConfig("plugin1", "data3")
	assert.Assert(t, !ok)
	assert.Equal(t, v, "")
	v, ok = configFile.PluginConfig("plugin2", "data3")
	assert.Assert(t, ok)
	assert.Equal(t, v, "some other string")
	v, ok = configFile.PluginConfig("plugin2", "data4")
	assert.Assert(t, !ok)
	assert.Equal(t, v, "")
	v, ok = configFile.PluginConfig("plugin3", "data5")
	assert.Assert(t, !ok)
	assert.Equal(t, v, "")

	// Add, remove and modify
	configFile.SetPluginConfig("plugin1", "data1", "some replacement string") // replacing a key
	configFile.SetPluginConfig("plugin1", "data2", "")                        // deleting a key
	configFile.SetPluginConfig("plugin1", "data3", "some additional string")  // new key
	configFile.SetPluginConfig("plugin2", "data3", "")                        // delete the whole plugin, since this was the only data
	configFile.SetPluginConfig("plugin3", "data5", "a new plugin")            // add a new plugin

	err = configFile.Save()
	assert.NilError(t, err)

	// Read it back and check it has the expected content again
	cfg, err = os.ReadFile("test-plugin2")
	assert.NilError(t, err)
	golden.Assert(t, string(cfg), "plugin-config-2.golden")
}
