package creds_test

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/docker/docker-credential-helpers/credentials"

	"knative.dev/func/pkg/docker"
	"knative.dev/func/pkg/docker/creds"
	. "knative.dev/func/pkg/testing"
)

var homeTempDir string

func TestMain(m *testing.M) {
	// github.com/containers/image only computes $HOME once so we need to set it
	// globally for all the tests
	var err error
	homeTempDir, err = os.MkdirTemp("", "")
	if err != nil {
		panic("failed to create tempdir" + err.Error())
	}
	os.Setenv(testHomeEnvName(), homeTempDir)
	if runtime.GOOS == "linux" {
		os.Setenv("XDG_CONFIG_HOME", filepath.Join(homeTempDir, ".config"))
	}

	os.Exit(m.Run())
}

func Test_registryEquals(t *testing.T) {
	tests := []struct {
		name string
		urlA string
		urlB string
		want bool
	}{
		{"no port matching host", "quay.io", "quay.io", true},
		{"non-matching host added sub-domain", "sub.quay.io", "quay.io", false},
		{"non-matching host different sub-domain", "sub.quay.io", "sub3.quay.io", false},
		{"localhost", "localhost", "localhost", true},
		{"localhost with standard ports", "localhost:80", "localhost:443", false},
		{"localhost with matching port", "https://localhost:1234", "http://localhost:1234", true},
		{"localhost with match by default port 80", "http://localhost", "localhost:80", true},
		{"localhost with match by default port 443", "https://localhost", "localhost:443", true},
		{"localhost with mismatch by non-default port 5000", "https://localhost", "localhost:5000", false},
		{"localhost with match by empty ports", "https://localhost", "http://localhost", true},
		{"docker.io matching host https", "https://docker.io", "docker.io", true},
		{"docker.io matching host http", "http://docker.io", "docker.io", true},
		{"docker.io with path", "docker.io/v1/", "docker.io", true},
		{"docker.io with protocol and path", "https://docker.io/v1/", "docker.io", true},
		{"docker.io with subdomain index.", "https://index.docker.io/v1/", "docker.io", true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := creds.RegistryEquals(tt.urlA, tt.urlB); got != tt.want {
				t.Errorf("to2ndLevelDomain() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestCheckAuth(t *testing.T) {

	const (
		uname        = "testuser"
		pwd          = "testpwd"
		incorrectPwd = "badpwd"
	)

	localhost, localhostTLS, cert := startServer(t, uname, pwd)

	_, portTLS, err := net.SplitHostPort(localhostTLS)
	if err != nil {
		t.Fatal(err)
	}

	nonLocalhostTLS := "test.io:" + portTLS

	type args struct {
		ctx      context.Context
		username string
		password string
		registry string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "correct credentials localhost no-TLS",
			args: args{
				ctx:      context.Background(),
				username: uname,
				password: pwd,
				registry: localhost,
			},
			wantErr: false,
		},
		{
			name: "correct credentials localhost",
			args: args{
				ctx:      context.Background(),
				username: uname,
				password: pwd,
				registry: localhostTLS,
			},
			wantErr: false,
		},
		{
			name: "correct credentials non-localhost",
			args: args{
				ctx:      context.Background(),
				username: uname,
				password: pwd,
				registry: nonLocalhostTLS,
			},
			wantErr: false,
		},
		{
			name: "incorrect credentials localhost no-TLS",
			args: args{
				ctx:      context.Background(),
				username: uname,
				password: incorrectPwd,
				registry: localhost,
			},
			wantErr: true,
		},
		{
			name: "incorrect credentials localhost",
			args: args{
				ctx:      context.Background(),
				username: uname,
				password: incorrectPwd,
				registry: localhostTLS,
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := docker.Credentials{
				Username: tt.args.username,
				Password: tt.args.password,
			}
			// create trusted certificates pool and add our certificate
			certPool := x509.NewCertPool()
			certPool.AddCert(cert)

			// client transport with the certificate
			transport := &http.Transport{
				TLSClientConfig: &tls.Config{
					RootCAs: certPool,
				},
			}

			dialer := &net.Dialer{}

			transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
				h, p, err := net.SplitHostPort(addr)
				if err != nil {
					return nil, err
				}
				if h == "test.io" {
					h = "localhost"
				}
				return dialer.DialContext(ctx, network, net.JoinHostPort(h, p))
			}
			if err := creds.CheckAuth(tt.args.ctx, tt.args.registry+"/someorg/someimage:sometag", c, transport); (err != nil) != tt.wantErr {
				t.Errorf("CheckAuth() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestCheckAuthEmptyCreds(t *testing.T) {

	localhost, _, _ := startServer(t, "", "")
	err := creds.CheckAuth(context.Background(), localhost+"/someorg/someimage:sometag", docker.Credentials{}, http.DefaultTransport)
	if err != nil {
		t.Error(err)
	}
}

// generate Certificates
func generateCert(t *testing.T) (tls.Certificate, *x509.Certificate) {
	var randReader = rand.Reader

	caPublicKey, caPrivateKey, err := ed25519.GenerateKey(randReader)
	if err != nil {
		t.Fatal(err)
	}

	caTemplate := &x509.Certificate{
		SerialNumber:          big.NewInt(1),
		Subject:               pkix.Name{CommonName: "localhost"},
		IPAddresses:           []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
		DNSNames:              []string{"localhost", "test.io"},
		NotBefore:             time.Now(),
		NotAfter:              time.Now().AddDate(1, 0, 0),
		IsCA:                  true,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		ExtraExtensions:       []pkix.Extension{},
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
	}

	caBytes, err := x509.CreateCertificate(randReader, caTemplate, caTemplate, caPublicKey, caPrivateKey)
	if err != nil {
		t.Fatal(err)
	}

	ca, err := x509.ParseCertificate(caBytes)
	if err != nil {
		t.Fatal(err)
	}

	tls := tls.Certificate{
		Certificate: [][]byte{caBytes},
		PrivateKey:  caPrivateKey,
		Leaf:        ca,
	}
	return tls, ca
}

func startServer(t *testing.T, uname, pwd string) (addr, addrTLS string, ca *x509.Certificate) {
	// create a custom handler function
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// no authentication required, empty creds
		if uname == "" || pwd == "" {
			if r.Method == http.MethodPost {
				w.WriteHeader(http.StatusCreated)
			} else {
				w.WriteHeader(http.StatusOK)
			}
			return
		}

		w.Header().Add("WWW-Authenticate", "basic")
		if u, p, ok := r.BasicAuth(); ok {
			if u == uname && p == pwd {
				if r.Method == http.MethodPost {
					w.WriteHeader(http.StatusCreated)
				} else {
					w.WriteHeader(http.StatusOK)
				}
				return
			}
		}
		w.WriteHeader(http.StatusUnauthorized)
	})

	// Setup certificates
	// tls Cert for the TLS server (has ca as Leaf)
	// x509 certificate which is its own CA for client
	tlsCert, ca := generateCert(t)

	// create Server config
	server := http.Server{
		Handler: handler,
		TLSConfig: &tls.Config{
			ServerName: "localhost",
			// with the TLS certificate
			Certificates: []tls.Certificate{tlsCert},
		},
	}

	// non-TLS listener
	listener, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		t.Fatal(err)
	}

	// TLS listener
	listenerTLS, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		t.Fatal(err)
	}
	addr = listener.Addr().String()
	addrTLS = listenerTLS.Addr().String()

	// listen for requests
	go func() {
		err := server.ServeTLS(listenerTLS, "", "")
		if err != nil && err != http.ErrServerClosed {
			panic(err)
		}
	}()

	go func() {
		err := server.Serve(listener)
		if err != nil && err != http.ErrServerClosed {
			panic(err)
		}
	}()
	// shutdown servers at cleanup
	t.Cleanup(func() {
		err := server.Shutdown(context.Background())
		if err != nil {
			t.Fatal(err)
		}
	})

	return
}

const (
	dockerIoUser    = "testUser1"
	dockerIoUserPwd = "goodPwd1"
	quayIoUser      = "testUser2"
	quayIoUserPwd   = "goodPwd2"
)

type Credentials = docker.Credentials

func TestNewCredentialsProvider(t *testing.T) {
	helperWithQuayIO := newInMemoryHelper()

	err := helperWithQuayIO.Add(&credentials.Credentials{
		ServerURL: "quay.io",
		Username:  quayIoUser,
		Secret:    quayIoUserPwd,
	})
	if err != nil {
		t.Fatal(err)
	}

	type args struct {
		promptUser        creds.CredentialsCallback
		verifyCredentials creds.VerifyCredentialsCallback
		additionalLoaders []creds.CredentialsCallback
		registry          string
		setUpEnv          setUpEnv
	}
	tests := []struct {
		name string
		args args
		want Credentials
	}{
		{
			name: "test user callback correct password on first try",
			args: args{
				promptUser:        correctPwdCallback,
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
			},
			want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
		{
			name: "test user callback correct password on second try",
			args: args{
				promptUser:        pwdCbkFirstWrongThenCorrect(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
			},
			want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
		{
			name: "get quay-io credentials with func config populated",
			args: args{
				promptUser:        pwdCbkThatShallNotBeCalled(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "quay.io",
				setUpEnv:          withPopulatedFuncAuthConfig,
			},
			want: Credentials{Username: quayIoUser, Password: quayIoUserPwd},
		},
		{
			name: "get docker-io credentials with func config populated",
			args: args{
				promptUser:        pwdCbkThatShallNotBeCalled(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
				setUpEnv:          withPopulatedFuncAuthConfig,
			},
			want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
		{
			name: "get quay-io credentials with docker config populated",
			args: args{
				promptUser:        pwdCbkThatShallNotBeCalled(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "quay.io",
				setUpEnv: all(
					withPopulatedDockerAuthConfig,
					setUpMockHelper("docker-credential-mock", helperWithQuayIO)),
			},
			want: Credentials{Username: quayIoUser, Password: quayIoUserPwd},
		},
		{
			name: "get docker-io credentials with docker config populated",
			args: args{
				promptUser:        pwdCbkThatShallNotBeCalled(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
				setUpEnv: all(
					withPopulatedDockerAuthConfig,
					setUpMockHelper("docker-credential-mock", newInMemoryHelper())),
			},
			want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
		{
			name: "get docker-io credentials from custom loader",
			args: args{
				promptUser:        pwdCbkThatShallNotBeCalled(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
				additionalLoaders: []creds.CredentialsCallback{correctPwdCallback},
			},
			want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resetHomeDir(t)
			if tt.args.setUpEnv != nil {
				tt.args.setUpEnv(t)
			}

			credentialsProvider := creds.NewCredentialsProvider(
				testConfigPath(t),
				creds.WithPromptForCredentials(tt.args.promptUser),
				creds.WithVerifyCredentials(tt.args.verifyCredentials),
				creds.WithAdditionalCredentialLoaders(tt.args.additionalLoaders...))
			got, err := credentialsProvider(context.Background(), tt.args.registry+"/someorg/someimage:sometag")
			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}
			if got != tt.want {
				t.Errorf("credentialsProvider() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestNewCredentialsProviderEmptyCreds(t *testing.T) {
	resetHomeDir(t)

	credentialsProvider := creds.NewCredentialsProvider(testConfigPath(t), creds.WithVerifyCredentials(func(ctx context.Context, image string, credentials docker.Credentials) error {
		if image == "localhost:5555/someorg/someimage:sometag" && credentials == (docker.Credentials{}) {
			return nil
		}
		t.Fatal("unreachable")
		return nil
	}))
	c, err := credentialsProvider(context.Background(), "localhost:5555/someorg/someimage:sometag")
	if err != nil {
		t.Error(err)
	}
	if c != (docker.Credentials{}) {
		t.Error("unexpected credentials")
	}
}

func TestCredentialsProviderSavingFromUserInput(t *testing.T) {
	resetHomeDir(t)

	helper := newInMemoryHelper()
	setUpMockHelper("docker-credential-mock", helper)(t)

	var pwdCbkInvocations int
	pwdCbk := func(r string) (Credentials, error) {
		pwdCbkInvocations++
		return correctPwdCallback(r)
	}

	chooseNoStore := func(available []string) (string, error) {
		if len(available) < 1 {
			t.Errorf("this should have been invoked with non empty list")
		}
		return "", nil
	}
	chooseMockStore := func(available []string) (string, error) {
		if len(available) < 1 {
			t.Errorf("this should have been invoked with non empty list")
		}
		return "docker-credential-mock", nil
	}
	shallNotBeInvoked := func(available []string) (string, error) {
		t.Fatal("this choose helper callback shall not be invoked")
		return "", errors.New("this callback shall not be invoked")
	}

	credentialsProvider := creds.NewCredentialsProvider(
		testConfigPath(t),
		creds.WithPromptForCredentials(pwdCbk),
		creds.WithVerifyCredentials(correctVerifyCbk),
		creds.WithPromptForCredentialStore(chooseNoStore))
	_, err := credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
		return
	}

	// now credentials should not be saved because no helper was provided
	l, err := helper.List()
	if err != nil {
		t.Fatal(err)
	}
	credsInStore := len(l)
	if credsInStore != 0 {
		t.Errorf("expected to have zero credentials in store, but has: %d", credsInStore)
	}
	credentialsProvider = creds.NewCredentialsProvider(
		testConfigPath(t),
		creds.WithPromptForCredentials(pwdCbk),
		creds.WithVerifyCredentials(correctVerifyCbk),
		creds.WithPromptForCredentialStore(chooseMockStore))
	_, err = credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
		return
	}
	if pwdCbkInvocations != 2 {
		t.Errorf("the pwd callback should have been invoked exactly twice but was invoked %d time", pwdCbkInvocations)
	}

	// now credentials should be saved in the mock secure store
	l, err = helper.List()
	if err != nil {
		t.Fatal(err)
	}
	credsInStore = len(l)
	if len(l) != 1 {
		t.Errorf("expected to have exactly one credentials in store, but has: %d", credsInStore)
	}
	credentialsProvider = creds.NewCredentialsProvider(
		testConfigPath(t),
		creds.WithPromptForCredentials(pwdCbkThatShallNotBeCalled(t)),
		creds.WithVerifyCredentials(correctVerifyCbk),
		creds.WithPromptForCredentialStore(shallNotBeInvoked))
	_, err = credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
		return
	}
}

// TestCredentialsWithoutHome ensures that credentialProvider works when HOME is
// not set or config is empty
func TestCredentialsWithoutHome(t *testing.T) {
	type args struct {
		promptUser        creds.CredentialsCallback
		verifyCredentials creds.VerifyCredentialsCallback
		registry          string
		setUpEnv          setUpEnv
	}
	tests := []struct {
		name              string
		testHomePathEmpty bool
		args              args
		want              Credentials
	}{
		{
			name:              "empty home with correct user prompt",
			testHomePathEmpty: true,
			args: args{
				promptUser:        correctPwdCallback, // user inputs correct credentials
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
				setUpEnv:          setEmptyHome,
			},
			want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
		{
			name: "empty config with user prompt",
			args: args{
				promptUser:        correctPwdCallback,
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
			},
			want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
	}

	// reset HOME to the original value after tests since they may change it
	defer func() {
		os.Setenv("HOME", homeTempDir)
	}()

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resetHomeDir(t)
			// set up HOME
			if tt.testHomePathEmpty {
				os.Unsetenv("HOME")
			} else {
				os.Setenv("HOME", homeTempDir)
			}
			credentialsProvider := creds.NewCredentialsProvider(
				testConfigPath(t),
				creds.WithPromptForCredentials(tt.args.promptUser),
				creds.WithVerifyCredentials(tt.args.verifyCredentials),
			)

			got, err := credentialsProvider(context.Background(), tt.args.registry+"/someorg/someimage:sometag")

			// ASSERT
			if err != nil {
				t.Errorf("%v", err)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("got: %v, want: %v", got, tt.want)
			}
		})
	}
}

// TestCredentialsHomePermissions tests whether the credentials provider
// works in scenarios where HOME has different permissions
func TestCredentialsHomePermissions(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skip windows perms for this test until windows perms are added")
	}

	if os.Getenv("GITHUB_ACTION") == "" {
		// skip for prow because its running as root
		t.Skip()
	}

	type args struct {
		promptUser        creds.CredentialsCallback
		verifyCredentials creds.VerifyCredentialsCallback
		registry          string
		setUpEnv          setUpEnv
	}
	tests := []struct {
		name              string
		perms             os.FileMode
		args              args
		expPermsDeniedErr bool
		want              Credentials
	}{
		{
			name:  "home with 0000 permissions (no perms)",
			perms: 0000,
			args: args{
				promptUser: pwdCbkThatShallNotBeCalled(t),
				setUpEnv:   setHomeWithPermissions(0000),
			},
			expPermsDeniedErr: true,
		},
		{
			name:  "home with 0333 permissions (write-execute only)",
			perms: 0333,
			args: args{

				promptUser:        pwdCbkThatShallNotBeCalled(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
				setUpEnv: all(
					withPopulatedDockerAuthConfig,
					setUpMockHelper("docker-credential-mock", newInMemoryHelper()),
					setHomeWithPermissions(0333)),
			},

			expPermsDeniedErr: false,
			want:              Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
		{
			name:  "home with 0444 permissions (read-only)",
			perms: 0444,
			args: args{
				promptUser: pwdCbkThatShallNotBeCalled(t),
				setUpEnv:   setHomeWithPermissions(0444)},
			expPermsDeniedErr: true,
		},
		{
			name:  "home with 0555 permissions (read-execute-only)",
			perms: 0555,
			args: args{
				promptUser: pwdCbkThatShallNotBeCalled(t),
				setUpEnv:   setHomeWithPermissions(0555),
			},
			expPermsDeniedErr: true,
		},
		{
			name:  "home with 0666 permissions (read-write-execute)",
			perms: 0666,
			args: args{
				promptUser: pwdCbkThatShallNotBeCalled(t),
				setUpEnv:   setHomeWithPermissions(0666),
			},
			expPermsDeniedErr: true,
		},
		{
			name:  "home with 0777 permissions (full access)",
			perms: 0777,

			args: args{
				promptUser:        pwdCbkThatShallNotBeCalled(t),
				verifyCredentials: correctVerifyCbk,
				registry:          "docker.io",
				setUpEnv: all(
					withPopulatedDockerAuthConfig,
					setUpMockHelper("docker-credential-mock", newInMemoryHelper()),
					setHomeWithPermissions(0777),
				),
			},
			expPermsDeniedErr: false,
			want:              Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
		},
	}

	// return HOME dir into its original state
	defer func() {
		resetHomePermissions(t) //reset home permissions to 0700
	}()

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {

			resetHomePermissions(t) //needs to be reset so that dir can be removed
			resetHomeDir(t)

			if tt.args.setUpEnv != nil {
				tt.args.setUpEnv(t)
			}

			// try to create HOME/.config/func
			_, err := testConfigPathError(t)
			if err != nil { // If error was returned
				if os.IsPermission(err) { // and its a permission error
					if !tt.expPermsDeniedErr { // but it wasnt expected
						t.Fatalf("didnt expect permissions denied error, but got: %s", err)
					}

				} else { // and it wasnt permission error
					t.Fatalf("got unexpected error: %v", err)
				}
			} else { // Else no error was returned
				if tt.expPermsDeniedErr { // but it was expected
					t.Fatal("expected permissions denied error, but got none")
				}
			}

			// if permissions were not denied, try to create Provider
			if !tt.expPermsDeniedErr {

				// try to stat HOME for permissions
				info, err := os.Stat(os.Getenv(testHomeEnvName()))
				if err != nil {
					t.Fatalf("failed to stat HOME: %s", err)
				}

				if info.Mode().Perm() != tt.perms {
					t.Errorf("expected permissions '%v', got '%v'", tt.perms, info.Mode().Perm())
				}
				credentialsProvider := creds.NewCredentialsProvider(
					testConfigPath(t),
					creds.WithPromptForCredentials(tt.args.promptUser),
					creds.WithVerifyCredentials(tt.args.verifyCredentials),
				)

				got, err := credentialsProvider(context.Background(), tt.args.registry+"/someorg/someimage:sometag")
				if err != nil {
					t.Errorf("%v", err)
					return
				}
				if !reflect.DeepEqual(got, tt.want) {
					t.Errorf("got: %v, want: %v", got, tt.want)
				}

			}
		})
	}
}

// ********************** helper functions below **************************** \\

func resetHomeDir(t *testing.T) {
	t.TempDir()
	if err := os.RemoveAll(homeTempDir); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(homeTempDir, 0700); err != nil {
		t.Fatal(err)
	}
}

// resetHomePermissions resets the HOME perms to 0700 (same as resetHomeDir(t))
func resetHomePermissions(t *testing.T) {
	if err := os.Chmod(homeTempDir, 0700); err != nil {
		t.Fatal(err)
	}
}

type setUpEnv = func(t *testing.T)

func withPopulatedDockerAuthConfig(t *testing.T) {
	t.Helper()
	home, err := os.UserHomeDir()
	if err != nil {
		t.Fatal(err)
	}
	dockerConfigDir := filepath.Join(home, ".docker")
	dockerConfigPath := filepath.Join(dockerConfigDir, "config.json")
	err = os.MkdirAll(filepath.Dir(dockerConfigPath), 0700)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = os.RemoveAll(dockerConfigDir) })

	configJSON := `{
	"auths": {
		"docker.io": { "auth": "%s" },
		"quay.io": {}
	},
	"credsStore": "mock"
}`
	configJSON = fmt.Sprintf(configJSON, base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd)))

	err = os.WriteFile(dockerConfigPath, []byte(configJSON), 0600)
	if err != nil {
		t.Fatal(err)
	}
}

func withPopulatedFuncAuthConfig(t *testing.T) {
	t.Helper()

	var err error

	authConfig := filepath.Join(testConfigPath(t), "auth.json")
	err = os.MkdirAll(filepath.Dir(authConfig), 0700)
	if err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() { _ = os.RemoveAll(authConfig) })

	authJSON := `{
	"auths": {
		"docker.io": { "auth": "%s" },
		"quay.io":   { "auth": "%s" }
	}
}`
	authJSON = fmt.Sprintf(authJSON,
		base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd)),
		base64.StdEncoding.EncodeToString([]byte(quayIoUser+":"+quayIoUserPwd)))

	err = os.WriteFile(authConfig, []byte(authJSON), 0600)
	if err != nil {
		t.Fatal(err)
	}
}

func pwdCbkThatShallNotBeCalled(t *testing.T) creds.CredentialsCallback {
	t.Helper()
	return func(registry string) (Credentials, error) {
		return Credentials{}, errors.New("this pwd cbk code shall not be called")
	}
}

func pwdCbkFirstWrongThenCorrect(t *testing.T) func(registry string) (Credentials, error) {
	t.Helper()
	var firstInvocation bool
	return func(registry string) (Credentials, error) {
		// registry is in form of registry/repository, need to extract registry only
		registry = strings.Split(registry, "/")[0]
		if registry != "index.docker.io" && registry != "quay.io" {
			return Credentials{}, fmt.Errorf("unexpected registry: %s", registry)
		}
		if firstInvocation {
			firstInvocation = false
			return Credentials{Username: dockerIoUser, Password: "badPwd"}, nil
		}
		return correctPwdCallback(registry)
	}
}

func correctPwdCallback(registry string) (Credentials, error) {
	// registry is in form of registry/repository, need to extract registry only
	registry = strings.Split(registry, "/")[0]
	if registry == "index.docker.io" {
		return Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, nil
	}
	if registry == "quay.io" {
		return Credentials{Username: quayIoUser, Password: quayIoUserPwd}, nil
	}
	return Credentials{}, errors.New("this cbk don't know the pwd")
}

func correctVerifyCbk(ctx context.Context, image string, credentials Credentials) error {
	username, password := credentials.Username, credentials.Password
	if username == dockerIoUser && password == dockerIoUserPwd && image == "docker.io/someorg/someimage:sometag" {
		return nil
	}
	if username == quayIoUser && password == quayIoUserPwd && image == "quay.io/someorg/someimage:sometag" {
		return nil
	}
	return creds.ErrUnauthorized
}

func testHomeEnvName() string {
	if runtime.GOOS == "windows" {
		return "USERPROFILE"
	}
	return "HOME"
}

func testConfigPath(t *testing.T) string {
	t.Helper()
	home := os.Getenv(testHomeEnvName())
	var configPath string
	if home != "" { // if HOME is not set, don't create config dir
		configPath = filepath.Join(home, ".config", "func")
		if err := os.MkdirAll(configPath, os.ModePerm); err != nil {
			t.Fatal(err)
		}
	}
	return configPath
}

// testConfigPathError tries to create a config dir in HOME/.config/func.
// Compared to testConfigPath, this returns the path AND error instead of failing
func testConfigPathError(t *testing.T) (string, error) {
	t.Helper()
	home := os.Getenv(testHomeEnvName())
	configPath := filepath.Join(home, ".config", "func")
	return configPath, os.MkdirAll(configPath, os.ModePerm)
}

func handlerForCredHelper(t *testing.T, credHelper credentials.Helper) http.Handler {
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		defer request.Body.Close()

		var err error
		var outBody interface{}

		uri := strings.Trim(request.RequestURI, "/")

		var serverURL string
		if uri == "get" || uri == "erase" {
			data, err := io.ReadAll(request.Body)
			if err != nil {
				writer.WriteHeader(http.StatusInternalServerError)
				return
			}
			serverURL = string(data)
			serverURL = strings.Trim(serverURL, "\n\r\t ")
		}

		switch uri {
		case "list":
			var list map[string]string
			list, err = credHelper.List()
			if err == nil {
				outBody = &list
			}
		case "store":
			creds := credentials.Credentials{}
			dec := json.NewDecoder(request.Body)
			err = dec.Decode(&creds)
			if err != nil {
				break
			}
			err = credHelper.Add(&creds)
		case "get":
			var user, secret string
			user, secret, err = credHelper.Get(serverURL)
			if err == nil {
				outBody = &credentials.Credentials{ServerURL: serverURL, Username: user, Secret: secret}
			}
		case "erase":
			err = credHelper.Delete(serverURL)
		default:
			writer.WriteHeader(http.StatusNotFound)
			return
		}

		if err != nil {
			if credentials.IsErrCredentialsNotFound(err) {
				writer.WriteHeader(http.StatusNotFound)
			} else {
				writer.WriteHeader(http.StatusInternalServerError)
				writer.Header().Add("Content-Type", "text/plain")
				fmt.Fprintf(writer, "error: %+v\n", err)
			}
			return
		}

		if outBody != nil {
			var data []byte
			data, err = json.Marshal(outBody)
			if err != nil {
				writer.WriteHeader(http.StatusInternalServerError)
				return
			}
			writer.Header().Add("Content-Type", "application/json")
			_, err = writer.Write(data)
			if err != nil {
				t.Fatal(err)
			}
		}
	})

}

// Go source code of mock docker-credential-helper implementation.
// Its storage is backed by inMemoryHelper instantiated in test and exposed via HTTP.
const helperGoSrc = `package main

import (
	"errors"
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	var (
		baseURL = os.Getenv("HELPER_BASE_URL")
		resp *http.Response
		err error
	)
	cmd := os.Args[1]
	switch cmd {
	case "list":
		resp, err = http.Get(baseURL + "/" + cmd)
		if err != nil {
			log.Fatal(err)
		}
		io.Copy(os.Stdout, resp.Body)
	case "get", "erase":
		resp, err = http.Post(baseURL+ "/" + cmd, "text/plain", os.Stdin)
		if err != nil {
			log.Fatal(err)
		}
		io.Copy(os.Stdout, resp.Body)
	case "store":
		resp, err = http.Post(baseURL+ "/" + cmd, "application/json", os.Stdin)
		if err != nil {
			log.Fatal(err)
		}
	default:
		log.Fatal(errors.New("unknown cmd: " + cmd))
	}
	if resp.StatusCode != http.StatusOK {
		log.Fatal(errors.New(resp.Status))
	}
	return
}
`

// Creates executable with name determined by the helperName parameter and puts it on $PATH.
//
// The executable behaves like docker credential helper (https://github.com/docker/docker-credential-helpers).
//
// The content of the store presented by the executable is backed by the helper parameter.
func setUpMockHelper(helperName string, helper credentials.Helper) func(t *testing.T) {
	return func(t *testing.T) {

		WithExecutable(t, helperName, helperGoSrc)

		listener, err := net.Listen("tcp", "localhost:0")
		if err != nil {
			t.Fatal(err)
		}

		t.Cleanup(func() { _ = listener.Close() })

		baseURL := fmt.Sprintf("http://%s", listener.Addr().String())
		t.Setenv("HELPER_BASE_URL", baseURL)

		server := http.Server{Handler: handlerForCredHelper(t, helper)}
		servErrChan := make(chan error)
		go func() {
			servErrChan <- server.Serve(listener)
		}()

		t.Cleanup(func() {
			ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
			defer cancel()
			_ = server.Shutdown(ctx)
			e := <-servErrChan
			if !errors.Is(e, http.ErrServerClosed) {
				t.Fatal(e)
			}
		})
	}
}

// combines multiple setUp routines into one setUp routine
func all(fns ...setUpEnv) setUpEnv {
	return func(t *testing.T) {
		t.Helper()
		for _, fn := range fns {
			fn(t)
		}
	}
}

func newInMemoryHelper() *inMemoryHelper {
	return &inMemoryHelper{lock: &sync.Mutex{}, credentials: make(map[string]credentials.Credentials)}
}

type inMemoryHelper struct {
	credentials map[string]credentials.Credentials
	lock        sync.Locker
}

func (i *inMemoryHelper) Add(credentials *credentials.Credentials) error {
	i.lock.Lock()
	defer i.lock.Unlock()

	i.credentials[credentials.ServerURL] = *credentials

	return nil
}

func (i *inMemoryHelper) Get(serverURL string) (string, string, error) {
	i.lock.Lock()
	defer i.lock.Unlock()

	if result, ok := i.credentials[serverURL]; ok {
		return result.Username, result.Secret, nil
	}

	return "", "", credentials.NewErrCredentialsNotFound()
}

func (i *inMemoryHelper) List() (map[string]string, error) {
	i.lock.Lock()
	defer i.lock.Unlock()

	result := make(map[string]string, len(i.credentials))

	for k, v := range i.credentials {
		result[k] = v.Username
	}

	return result, nil
}

func (i *inMemoryHelper) Delete(serverURL string) error {
	i.lock.Lock()
	defer i.lock.Unlock()

	if _, ok := i.credentials[serverURL]; ok {
		delete(i.credentials, serverURL)
		return nil
	}

	return credentials.NewErrCredentialsNotFound()
}

// set home variables to empty values
func setEmptyHome(t *testing.T) {
	t.Helper()
	t.Setenv("HOME", "")
	t.Setenv("XDG_CONFIG_HOME", "")
}

// setHomeWithPermissions sets home dir to specified permissions
func setHomeWithPermissions(perm os.FileMode) func(t *testing.T) {
	return func(t *testing.T) {
		t.Helper()
		homeDir := os.Getenv("HOME")

		// if home is empty, nothing to do
		if homeDir == "" {
			t.Fatal("home dir is empty, cant set perms")
		}

		fmt.Printf("setting permissions (%v) on home dir: %s\n", perm, homeDir)
		err := os.Chmod(homeDir, perm)
		if err != nil {
			t.Fatalf("failed to set permissions on home dir: %s", err)
		}
	}
}
