// Copyright 2016 Google, 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 credhelper

import (
	"errors"
	"fmt"
	"strings"
	"testing"

	"github.com/GoogleCloudPlatform/docker-credential-gcr/v2/mock/mock_cmd"
	"github.com/GoogleCloudPlatform/docker-credential-gcr/v2/mock/mock_config" // mocks must be generated before test execution
	"github.com/GoogleCloudPlatform/docker-credential-gcr/v2/mock/mock_store"

	"github.com/GoogleCloudPlatform/docker-credential-gcr/v2/config"
	"github.com/GoogleCloudPlatform/docker-credential-gcr/v2/store"
	"github.com/GoogleCloudPlatform/docker-credential-gcr/v2/util/cmd"
	"github.com/golang/mock/gomock"
)

var expectedGCRUsername = fmt.Sprintf("_dcgcr_%s_token", strings.ReplaceAll(config.Version, ".", "_"))
var expectedGCRZeroUsername = "_dcgcr_0_0_0_token"

var testGCRHosts = [...]string{
	"gcr.io",
	"us.gcr.io",
	"eu.gcr.io",
	"asia.gcr.io",
	"staging-k8s.gcr.io",
	"marketplace.gcr.io",
	"appengine.gcr.io",
	"hypothetical-alias.gcr.io",
}

func TestGet_GCRCredentials(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// create a mocks for the helper to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)
	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)

	// mock the helper methods used by getGCRAccessToken
	expectedSecret := "secrets!"
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return expectedSecret, nil
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return "", errors.New("no token here")
		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return "", errors.New("no token here")
		},
	}

	// Verify that all of GCR's hostnames return GCR's access token.
	for _, host := range testGCRHosts {
		mockUserCfg.EXPECT().TokenSources().Return(config.DefaultTokenSources[:])
		username, secret, err := tested.Get("https://" + host)
		if err != nil {
			t.Errorf("get returned an error: %v", err)
		} else if username != expectedGCRUsername && username != expectedGCRZeroUsername {
			t.Errorf("expected GCR username: %s but got: %s", expectedGCRUsername, username)
		} else if secret != expectedSecret {
			t.Errorf("expected secret: %s but got: %s", expectedSecret, secret)
		}
	}
}

/*
	The following tests verify the behavior of getGCRAccessToken. Preference
	is defined by tokenSources
*/

func TestGetGCRAccessToken_Env(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	// create a mock store to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)

	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)
	mockUserCfg.EXPECT().TokenSources().Return(config.DefaultTokenSources[:])

	// mock the helper methods used by getGCRAccessToken
	const expected = "application default creds!"
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return expected, nil
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return "", errors.New("no token from gcloud")
		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return "", errors.New("no token in the cred store")
		},
	}

	token, err := tested.getGCRAccessToken()

	if err != nil {
		t.Fatalf("getGCRAccessToken returned an error: %v", err)
	} else if token != expected {
		t.Fatalf("Expected: %s got: %s", expected, token)
	}
}

func TestGetGCRAccessToken_PrivateStore(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// create a mock store to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)

	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)
	mockUserCfg.EXPECT().TokenSources().Return(config.DefaultTokenSources[:])

	// mock the helper methods used by getGCRAccessToken
	const expected = "private creds!"
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return "creds from `env`", nil
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return "creds from `gcloud`", nil

		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return expected, nil
		},
	}

	token, err := tested.getGCRAccessToken()

	if err != nil {
		t.Fatalf("getGCRAccessToken returned an error: %v", err)
	} else if token != expected {
		t.Fatalf("Expected: %s got: %s", expected, token)
	}
}

func TestGetGCRAccessToken_NoneExist(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// create a mock store to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)

	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)
	mockUserCfg.EXPECT().TokenSources().Return(config.DefaultTokenSources[:])

	// mock the helper methods used by getGCRAccessToken
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return "", errors.New("no token here")
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return "", errors.New("still no token here")
		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return "", errors.New("sad panda")
		},
	}

	token, err := tested.getGCRAccessToken()

	if err == nil {
		t.Fatalf("Expected an error, got token: %s", token)
	}
}

func TestGetGCRAccessToken_CustomTokenSources(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// create a mock store to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)

	// Mock a user config, re-arranging the token sources.
	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)
	mockUserCfg.EXPECT().TokenSources().Return([]string{"store", "gcloud", "env"}) // reversed from default

	const (
		gcloudCreds = "gcloud sdk creds!"
		storeCreds  = "private creds!"
		envCreds    = "environment creds!"
	)
	// mock the helper methods used by getGCRAccessToken
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return envCreds, nil
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return gcloudCreds, nil
		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return storeCreds, nil
		},
	}

	token, err := tested.getGCRAccessToken()

	if err != nil {
		t.Fatalf("getGCRAccessToken returned an error: %v", err)
	} else if token != storeCreds {
		t.Fatalf("Expected: %s got: %s", storeCreds, token)
	}
}

func TestGetGCRAccessToken_CustomTokenSources_ValidSourcesDisabled(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// create a mock store to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)

	// Mock a user config, disabling some token sources.
	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)
	mockUserCfg.EXPECT().TokenSources().Return([]string{"gcloud"}) // gcloud only configured source

	const (
		storeCreds = "private creds!"
		envCreds   = "environment creds!"
	)
	// mock the helper methods used by getGCRAccessToken
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return envCreds, nil
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return "", errors.New("no token here")
		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return storeCreds, nil
		},
	}

	token, err := tested.getGCRAccessToken()

	if err == nil {
		t.Fatalf("Expected an error, got token: %s", token)
	}
}

func TestGetGCRAccessToken_OldGcloudSdkTokenSourceString(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// create a mock store to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)

	// Mock a user config, disabling some token sources.
	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)
	mockUserCfg.EXPECT().TokenSources().Return([]string{"gcloud_sdk"}) // the string that was initially used for specify gcloud

	const (
		envCreds    = "environment creds!"
		gcloudCreds = "gcloud sdk creds!"
		storeCreds  = "private creds!"
	)
	// mock the helper methods used by getGCRAccessToken
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return envCreds, nil
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return gcloudCreds, nil
		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return storeCreds, nil
		},
	}

	token, err := tested.getGCRAccessToken()

	if err != nil {
		t.Fatalf("tokenFromGcloudSDK returned an error: %v", err)
	} else if token != gcloudCreds {
		t.Fatalf("Expected: '%s' got: '%s'", gcloudCreds, token)
	}
}

func TestGetGCRAccessToken_CustomTokenSources_InvalidSource(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// create a mock store to use
	mockStore := mock_store.NewMockGCRCredStore(mockCtrl)

	mockUserCfg := mock_config.NewMockUserConfig(mockCtrl)
	mockUserCfg.EXPECT().TokenSources().Return([]string{"invalid"})

	const (
		gcloudCreds = "gcloud sdk creds!"
		storeCreds  = "private creds!"
		envCreds    = "environment creds!"
	)
	// mock the helper methods used by getGCRAccessToken
	tested := &gcrCredHelper{
		store:   mockStore,
		userCfg: mockUserCfg,
		envToken: func() (string, error) {
			return envCreds, nil
		},
		gcloudSDKToken: func(_ cmd.Command) (string, error) {
			return gcloudCreds, nil
		},
		credStoreToken: func(_ store.GCRCredStore) (string, error) {
			return storeCreds, nil
		},
	}

	token, err := tested.getGCRAccessToken()

	if err == nil {
		t.Fatalf("Expected an error, got token: %s", token)
	}
}

func TestTokenFromGcloudSDK(t *testing.T) {
	t.Parallel()
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	const gcloudCreds = "gcloud sdk creds!"

	// This test is more-or-less tautological, but it's important to verify
	// that gcloud is being queried in a supported way.
	mockCmd := mock_cmd.NewMockCommand(mockCtrl)
	mockCmd.EXPECT().Exec("config", "config-helper", "--force-auth-refresh", "--format=value(credential.access_token)").Return([]uint8(gcloudCreds), nil)

	token, err := tokenFromGcloudSDK(mockCmd)

	if err != nil {
		t.Fatalf("tokenFromGcloudSDK returned an error: %v", err)
	} else if token != gcloudCreds {
		t.Fatalf("Expected: '%s' got: '%s'", gcloudCreds, token)
	}
}
