// Copyright 2017 Microsoft Corporation
//
//  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 auth

import (
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"github.com/Azure/go-autorest/autorest"
	"github.com/Azure/go-autorest/autorest/azure"
)

var (
	expectedEnvironment = EnvironmentSettings{
		Values: map[string]string{
			SubscriptionID:      "sub-abc-123",
			TenantID:            "tenant-abc-123",
			ClientID:            "client-abc-123",
			ClientSecret:        "client-secret-123",
			CertificatePath:     "~/some/path/cert.pfx",
			CertificatePassword: "certificate-password",
			Username:            "user-name-abc",
			Password:            "user-password-123",
			Resource:            "my-resource",
		},
		Environment: azure.PublicCloud,
	}
	expectedFile = FileSettings{
		Values: map[string]string{
			ClientID:                "client-id-123",
			ClientSecret:            "client-secret-456",
			SubscriptionID:          "sub-id-789",
			TenantID:                "tenant-id-123",
			ActiveDirectoryEndpoint: "https://login.microsoftonline.com",
			ResourceManagerEndpoint: "https://management.azure.com/",
			GraphResourceID:         "https://graph.windows.net/",
			SQLManagementEndpoint:   "https://management.core.windows.net:8443/",
			GalleryEndpoint:         "https://gallery.azure.com/",
			ManagementEndpoint:      "https://management.core.windows.net/",
		},
	}
)

func setDefaultEnv() {
	os.Setenv(SubscriptionID, expectedEnvironment.Values[SubscriptionID])
	os.Setenv(TenantID, expectedEnvironment.Values[TenantID])
	os.Setenv(ClientID, expectedEnvironment.Values[ClientID])
	os.Setenv(ClientSecret, expectedEnvironment.Values[ClientSecret])
	os.Setenv(CertificatePath, expectedEnvironment.Values[CertificatePath])
	os.Setenv(CertificatePassword, expectedEnvironment.Values[CertificatePassword])
	os.Setenv(Username, expectedEnvironment.Values[Username])
	os.Setenv(Password, expectedEnvironment.Values[Password])
	os.Setenv(Resource, expectedEnvironment.Values[Resource])
}

func TestGetSettingsFromEnvironment(t *testing.T) {
	setDefaultEnv()
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		t.Logf("failed to get settings: %v", err)
		t.Fail()
	}
	if !reflect.DeepEqual(expectedEnvironment, settings) {
		t.Logf("expected %v, got %v", expectedEnvironment, settings)
		t.Fail()
	}
	if settings.GetSubscriptionID() != expectedEnvironment.Values[SubscriptionID] {
		t.Log("settings.GetSubscriptionID() return value didn't match")
		t.Fail()
	}
}

func TestGetSettingsFromEnvironmentBadEnvironmentName(t *testing.T) {
	os.Setenv(EnvironmentName, "badenvironment")
	defer func() {
		// must undo this value else other tests will fail
		os.Setenv(EnvironmentName, "")
	}()
	_, err := GetSettingsFromEnvironment()
	if err == nil {
		t.Log("unexpected nil error")
		t.Fail()
	}
}

func TestEnvGetClientCertificate(t *testing.T) {
	setDefaultEnv()
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		t.Logf("failed to get settings: %v", err)
		t.Fail()
	}
	cfg, err := settings.GetClientCertificate()
	if err != nil {
		t.Logf("failed to get config for client cert: %v", err)
		t.Fail()
	}
	if cfg.CertificatePath != expectedEnvironment.Values[CertificatePath] {
		t.Log("bad certificate path")
		t.Fail()
	}
	if cfg.CertificatePassword != expectedEnvironment.Values[CertificatePassword] {
		t.Log("bad certificate password")
		t.Fail()
	}
	// should fail as the certificate doesn't exist
	_, err = cfg.Authorizer()
	if err == nil {
		t.Log("unexpected nil error")
		t.Fail()
	}
}

func TestEnvGetUsernamePassword(t *testing.T) {
	setDefaultEnv()
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		t.Logf("failed to get settings: %v", err)
		t.Fail()
	}
	cfg, err := settings.GetUsernamePassword()
	if err != nil {
		t.Logf("failed to get config for username/password: %v", err)
		t.Fail()
	}
	_, err = cfg.Authorizer()
	if err != nil {
		t.Logf("failed to get authorizer for username/password: %v", err)
		t.Fail()
	}
}

func TestEnvGetMSI(t *testing.T) {
	setDefaultEnv()
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		t.Logf("failed to get settings: %v", err)
		t.Fail()
	}
	cfg := settings.GetMSI()
	if cfg.Resource == "" {
		t.Fatal("unexpected empty resource")
	}
}

func TestEnvGetDeviceFlow(t *testing.T) {
	setDefaultEnv()
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		t.Logf("failed to get settings: %v", err)
		t.Fail()
	}
	cfg := settings.GetDeviceFlow()
	// TODO mock device flow?
	if cfg.ClientID != expectedEnvironment.Values[ClientID] {
		t.Log("bad client ID")
		t.Fail()
	}
	if cfg.TenantID != expectedEnvironment.Values[TenantID] {
		t.Log("bad tenant ID")
		t.Fail()
	}
}

func TestGetSettingsFromFile(t *testing.T) {
	os.Setenv("AZURE_AUTH_LOCATION", "./testdata/credsutf16le.json")
	settings, err := GetSettingsFromFile()
	if err != nil {
		t.Logf("failed to load config file: %v", err)
		t.Fail()
	}
	if !reflect.DeepEqual(expectedFile, settings) {
		t.Logf("expected %v, got %v", expectedFile, settings)
		t.Fail()
	}
	if settings.GetSubscriptionID() != expectedFile.Values[SubscriptionID] {
		t.Log("settings.GetSubscriptionID() return value didn't match")
		t.Fail()
	}
}

func TestNewAuthorizerFromFile(t *testing.T) {
	os.Setenv("AZURE_AUTH_LOCATION", "./testdata/credsutf16le.json")
	authorizer, err := NewAuthorizerFromFile("https://management.azure.com")
	if err != nil || authorizer == nil {
		t.Logf("NewAuthorizerFromFile failed, got error %v", err)
		t.Fail()
	}
}

func TestNewAuthorizerFromFileWithResource(t *testing.T) {
	os.Setenv("AZURE_AUTH_LOCATION", "./testdata/credsutf16le.json")
	authorizer, err := NewAuthorizerFromFileWithResource("https://my.vault.azure.net")
	if err != nil || authorizer == nil {
		t.Logf("NewAuthorizerFromFileWithResource failed, got error %v", err)
		t.Fail()
	}
}

func TestNewAuthorizerFromEnvironment(t *testing.T) {
	setDefaultEnv()
	authorizer, err := NewAuthorizerFromEnvironment()

	if err != nil || authorizer == nil {
		t.Logf("NewAuthorizerFromEnvironment failed, got error %v", err)
		t.Fail()
	}
}

func TestNewAuthorizerFromEnvironmentWithResource(t *testing.T) {
	setDefaultEnv()
	authorizer, err := NewAuthorizerFromEnvironmentWithResource("https://my.vault.azure.net")

	if err != nil || authorizer == nil {
		t.Logf("NewAuthorizerFromEnvironmentWithResource failed, got error %v", err)
		t.Fail()
	}
}

func TestDecodeAndUnmarshal(t *testing.T) {
	tests := []string{
		"credsutf8.json",
		"credsutf16le.json",
		"credsutf16be.json",
	}
	for _, test := range tests {
		os.Setenv("AZURE_AUTH_LOCATION", filepath.Join("./testdata/", test))
		settings, err := GetSettingsFromFile()
		if err != nil {
			t.Logf("error reading file '%s': %s", test, err)
			t.Fail()
		}
		if !reflect.DeepEqual(expectedFile, settings) {
			t.Logf("unmarshaled map expected %v, got %v", expectedFile, settings)
			t.Fail()
		}
	}
}

func TestFileClientCertificateAuthorizer(t *testing.T) {
	os.Setenv("AZURE_AUTH_LOCATION", "./testdata/credsutf8.json")
	settings, err := GetSettingsFromFile()
	if err != nil {
		t.Logf("failed to load file settings: %v", err)
		t.Fail()
	}
	// add certificate settings
	settings.Values[CertificatePath] = "~/fake/path/cert.pfx"
	settings.Values[CertificatePassword] = "fake-password"
	_, err = settings.ClientCertificateAuthorizer("https://management.azure.com")
	if err == nil {
		t.Log("unexpected nil error")
		t.Fail()
	}
}

func TestFileGetAuthorizerClientCert(t *testing.T) {
	t.Setenv("AZURE_AUTH_LOCATION", "./testdata/credsutf8.json")
	settings, err := GetSettingsFromFile()
	if err != nil {
		t.Logf("failed to load file settings: %v", err)
		t.Fail()
	}
	// add certificate settings
	settings.Values[CertificatePath] = "~/fake/path/cert.pfx"
	settings.Values[CertificatePassword] = "fake-password"
	auth, err := settings.GetAuthorizer("https://management.azure.com")
	if err != nil {
		t.Logf("failed to get authorizer: %v", err)
		t.Fail()
	}
	if _, ok := auth.(*autorest.BearerAuthorizer); !ok {
		t.Fatalf("unexpected authorizer type %T", auth)
	}
}

func TestMultitenantClientCredentials(t *testing.T) {
	setDefaultEnv()
	os.Setenv(AuxiliaryTenantIDs, "aux-tenant-1;aux-tenant-2;aux-tenant3")
	defer func() {
		os.Setenv(AuxiliaryTenantIDs, "")
	}()
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		t.Fatalf("failed to get settings from environment: %v", err)
	}
	if settings.Values[AuxiliaryTenantIDs] == "" {
		t.Fatal("auxiliary tenant IDs are missing in settings")
	}
	ccc, err := settings.GetClientCredentials()
	if err != nil {
		t.Fatalf("failed to get client credentials config: %v", err)
	}
	if len(ccc.AuxTenants) == 0 {
		t.Fatal("auxiliary tenant IDs are missing in config")
	}
	expected := []string{"aux-tenant-1", "aux-tenant-2", "aux-tenant3"}
	if !reflect.DeepEqual(ccc.AuxTenants, expected) {
		t.Fatalf("expected auxiliary tenants '%s', got '%s'", expected, ccc.AuxTenants)
	}
	a, err := ccc.Authorizer()
	if err != nil {
		t.Fatalf("failed to create authorizer: %v", err)
	}
	if _, ok := a.(autorest.MultiTenantServicePrincipalTokenAuthorizer); !ok {
		t.Fatal("authorizer doesn't implement MultiTenantServicePrincipalTokenAuthorizer")
	}
}
