/*
Copyright 2014 The Kubernetes Authors.

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 gce

import (
	"context"
	"reflect"
	"strings"
	"testing"

	"golang.org/x/oauth2/google"

	"k8s.io/kubernetes/pkg/cloudprovider"
)

func TestReadConfigFile(t *testing.T) {
	const s = `[Global]
token-url = my-token-url
token-body = my-token-body
project-id = my-project
network-project-id = my-network-project
network-name = my-network
subnetwork-name = my-subnetwork
secondary-range-name = my-secondary-range
node-tags = my-node-tag1
node-instance-prefix = my-prefix
multizone = true
   `
	reader := strings.NewReader(s)
	config, err := readConfig(reader)
	if err != nil {
		t.Fatalf("Unexpected config parsing error %v", err)
	}

	expected := &ConfigFile{Global: ConfigGlobal{
		TokenURL:           "my-token-url",
		TokenBody:          "my-token-body",
		ProjectID:          "my-project",
		NetworkProjectID:   "my-network-project",
		NetworkName:        "my-network",
		SubnetworkName:     "my-subnetwork",
		SecondaryRangeName: "my-secondary-range",
		NodeTags:           []string{"my-node-tag1"},
		NodeInstancePrefix: "my-prefix",
		Multizone:          true,
	}}

	if !reflect.DeepEqual(expected, config) {
		t.Fatalf("Expected config file values to be read into ConfigFile struct.  \nExpected:\n%+v\nActual:\n%+v", expected, config)
	}
}

func TestExtraKeyInConfig(t *testing.T) {
	const s = `[Global]
project-id = my-project
unknown-key = abc
network-name = my-network
   `
	reader := strings.NewReader(s)
	config, err := readConfig(reader)
	if err != nil {
		t.Fatalf("Unexpected config parsing error %v", err)
	}
	if config.Global.ProjectID != "my-project" || config.Global.NetworkName != "my-network" {
		t.Fatalf("Expected config values to continue to be read despite extra key-value pair.")
	}
}

func TestGetRegion(t *testing.T) {
	zoneName := "us-central1-b"
	regionName, err := GetGCERegion(zoneName)
	if err != nil {
		t.Fatalf("unexpected error from GetGCERegion: %v", err)
	}
	if regionName != "us-central1" {
		t.Errorf("Unexpected region from GetGCERegion: %s", regionName)
	}
	gce := &GCECloud{
		localZone: zoneName,
		region:    regionName,
	}
	zones, ok := gce.Zones()
	if !ok {
		t.Fatalf("Unexpected missing zones impl")
	}
	zone, err := zones.GetZone(context.TODO())
	if err != nil {
		t.Fatalf("unexpected error %v", err)
	}
	if zone.Region != "us-central1" {
		t.Errorf("Unexpected region: %s", zone.Region)
	}
}

func TestComparingHostURLs(t *testing.T) {
	tests := []struct {
		host1       string
		zone        string
		name        string
		expectEqual bool
	}{
		{
			host1:       "https://www.googleapis.com/compute/v1/projects/1234567/zones/us-central1-f/instances/kubernetes-node-fhx1",
			zone:        "us-central1-f",
			name:        "kubernetes-node-fhx1",
			expectEqual: true,
		},
		{
			host1:       "https://www.googleapis.com/compute/v1/projects/cool-project/zones/us-central1-f/instances/kubernetes-node-fhx1",
			zone:        "us-central1-f",
			name:        "kubernetes-node-fhx1",
			expectEqual: true,
		},
		{
			host1:       "https://www.googleapis.com/compute/v23/projects/1234567/zones/us-central1-f/instances/kubernetes-node-fhx1",
			zone:        "us-central1-f",
			name:        "kubernetes-node-fhx1",
			expectEqual: true,
		},
		{
			host1:       "https://www.googleapis.com/compute/v24/projects/1234567/regions/us-central1/zones/us-central1-f/instances/kubernetes-node-fhx1",
			zone:        "us-central1-f",
			name:        "kubernetes-node-fhx1",
			expectEqual: true,
		},
		{
			host1:       "https://www.googleapis.com/compute/v1/projects/1234567/zones/us-central1-f/instances/kubernetes-node-fhx1",
			zone:        "us-central1-c",
			name:        "kubernetes-node-fhx1",
			expectEqual: false,
		},
		{
			host1:       "https://www.googleapis.com/compute/v1/projects/1234567/zones/us-central1-f/instances/kubernetes-node-fhx",
			zone:        "us-central1-f",
			name:        "kubernetes-node-fhx1",
			expectEqual: false,
		},
		{
			host1:       "https://www.googleapis.com/compute/v1/projects/1234567/zones/us-central1-f/instances/kubernetes-node-fhx1",
			zone:        "us-central1-f",
			name:        "kubernetes-node-fhx",
			expectEqual: false,
		},
	}

	for _, test := range tests {
		link1 := hostURLToComparablePath(test.host1)
		testInstance := &gceInstance{
			Name: canonicalizeInstanceName(test.name),
			Zone: test.zone,
		}
		link2 := testInstance.makeComparableHostPath()
		if test.expectEqual && link1 != link2 {
			t.Errorf("expected link1 and link2 to be equal, got %s and %s", link1, link2)
		} else if !test.expectEqual && link1 == link2 {
			t.Errorf("expected link1 and link2 not to be equal, got %s and %s", link1, link2)
		}
	}
}

func TestSplitProviderID(t *testing.T) {
	providers := []struct {
		providerID string

		project  string
		zone     string
		instance string

		fail bool
	}{
		{
			providerID: ProviderName + "://project-example-164317/us-central1-f/kubernetes-node-fhx1",
			project:    "project-example-164317",
			zone:       "us-central1-f",
			instance:   "kubernetes-node-fhx1",
			fail:       false,
		},
		{
			providerID: ProviderName + "://project-example.164317/us-central1-f/kubernetes-node-fhx1",
			project:    "project-example.164317",
			zone:       "us-central1-f",
			instance:   "kubernetes-node-fhx1",
			fail:       false,
		},
		{
			providerID: ProviderName + "://project-example-164317/us-central1-fkubernetes-node-fhx1",
			project:    "",
			zone:       "",
			instance:   "",
			fail:       true,
		},
		{
			providerID: ProviderName + ":/project-example-164317/us-central1-f/kubernetes-node-fhx1",
			project:    "",
			zone:       "",
			instance:   "",
			fail:       true,
		},
		{
			providerID: "aws://project-example-164317/us-central1-f/kubernetes-node-fhx1",
			project:    "",
			zone:       "",
			instance:   "",
			fail:       true,
		},
		{
			providerID: ProviderName + "://project-example-164317/us-central1-f/kubernetes-node-fhx1/",
			project:    "",
			zone:       "",
			instance:   "",
			fail:       true,
		},
		{
			providerID: ProviderName + "://project-example.164317//kubernetes-node-fhx1",
			project:    "",
			zone:       "",
			instance:   "",
			fail:       true,
		},
		{
			providerID: ProviderName + "://project-example.164317/kubernetes-node-fhx1",
			project:    "",
			zone:       "",
			instance:   "",
			fail:       true,
		},
	}

	for _, test := range providers {
		project, zone, instance, err := splitProviderID(test.providerID)
		if (err != nil) != test.fail {
			t.Errorf("Expected to fail=%t, with pattern %v", test.fail, test)
		}

		if test.fail {
			continue
		}

		if project != test.project {
			t.Errorf("Expected %v, but got %v", test.project, project)
		}
		if zone != test.zone {
			t.Errorf("Expected %v, but got %v", test.zone, zone)
		}
		if instance != test.instance {
			t.Errorf("Expected %v, but got %v", test.instance, instance)
		}
	}
}

func TestGetZoneByProviderID(t *testing.T) {
	tests := []struct {
		providerID string

		expectedZone cloudprovider.Zone

		fail        bool
		description string
	}{
		{
			providerID:   ProviderName + "://project-example-164317/us-central1-f/kubernetes-node-fhx1",
			expectedZone: cloudprovider.Zone{FailureDomain: "us-central1-f", Region: "us-central1"},
			fail:         false,
			description:  "standard gce providerID",
		},
		{
			providerID:   ProviderName + "://project-example-164317/us-central1-f/kubernetes-node-fhx1/",
			expectedZone: cloudprovider.Zone{},
			fail:         true,
			description:  "too many slashes('/') trailing",
		},
		{
			providerID:   ProviderName + "://project-example.164317//kubernetes-node-fhx1",
			expectedZone: cloudprovider.Zone{},
			fail:         true,
			description:  "too many slashes('/') embedded",
		},
		{
			providerID:   ProviderName + "://project-example-164317/uscentral1f/kubernetes-node-fhx1",
			expectedZone: cloudprovider.Zone{},
			fail:         true,
			description:  "invalid name of the GCE zone",
		},
	}

	gce := &GCECloud{
		localZone: "us-central1-f",
		region:    "us-central1",
	}
	for _, test := range tests {
		zone, err := gce.GetZoneByProviderID(context.TODO(), test.providerID)
		if (err != nil) != test.fail {
			t.Errorf("Expected to fail=%t, provider ID %v, tests %s", test.fail, test, test.description)
		}

		if test.fail {
			continue
		}

		if zone != test.expectedZone {
			t.Errorf("Expected %v, but got %v", test.expectedZone, zone)
		}
	}
}

func TestGenerateCloudConfigs(t *testing.T) {
	configBoilerplate := ConfigGlobal{
		TokenURL:           "",
		TokenBody:          "",
		ProjectID:          "project-id",
		NetworkName:        "network-name",
		SubnetworkName:     "",
		SecondaryRangeName: "",
		NodeTags:           []string{"node-tag"},
		NodeInstancePrefix: "node-prefix",
		Multizone:          false,
		ApiEndpoint:        "",
		LocalZone:          "us-central1-a",
		AlphaFeatures:      []string{},
	}

	cloudBoilerplate := CloudConfig{
		ApiEndpoint:        "",
		ProjectID:          "project-id",
		NetworkProjectID:   "",
		Region:             "us-central1",
		Zone:               "us-central1-a",
		ManagedZones:       []string{"us-central1-a"},
		NetworkName:        "network-name",
		SubnetworkName:     "",
		NetworkURL:         "",
		SubnetworkURL:      "",
		SecondaryRangeName: "",
		NodeTags:           []string{"node-tag"},
		TokenSource:        google.ComputeTokenSource(""),
		NodeInstancePrefix: "node-prefix",
		UseMetadataServer:  true,
		AlphaFeatureGate:   &AlphaFeatureGate{map[string]bool{}},
	}

	testCases := []struct {
		name   string
		config func() ConfigGlobal
		cloud  func() CloudConfig
	}{
		{
			name:   "Empty Config",
			config: func() ConfigGlobal { return configBoilerplate },
			cloud:  func() CloudConfig { return cloudBoilerplate },
		},
		{
			name: "Nil token URL",
			config: func() ConfigGlobal {
				v := configBoilerplate
				v.TokenURL = "nil"
				return v
			},
			cloud: func() CloudConfig {
				v := cloudBoilerplate
				v.TokenSource = nil
				return v
			},
		},
		{
			name: "Network Project ID",
			config: func() ConfigGlobal {
				v := configBoilerplate
				v.NetworkProjectID = "my-awesome-project"
				return v
			},
			cloud: func() CloudConfig {
				v := cloudBoilerplate
				v.NetworkProjectID = "my-awesome-project"
				return v
			},
		},
		{
			name: "Specified API Endpint",
			config: func() ConfigGlobal {
				v := configBoilerplate
				v.ApiEndpoint = "https://www.googleapis.com/compute/staging_v1/"
				return v
			},
			cloud: func() CloudConfig {
				v := cloudBoilerplate
				v.ApiEndpoint = "https://www.googleapis.com/compute/staging_v1/"
				return v
			},
		},
		{
			name: "Network & Subnetwork names",
			config: func() ConfigGlobal {
				v := configBoilerplate
				v.NetworkName = "my-network"
				v.SubnetworkName = "my-subnetwork"
				return v
			},
			cloud: func() CloudConfig {
				v := cloudBoilerplate
				v.NetworkName = "my-network"
				v.SubnetworkName = "my-subnetwork"
				return v
			},
		},
		{
			name: "Network & Subnetwork URLs",
			config: func() ConfigGlobal {
				v := configBoilerplate
				v.NetworkName = "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/my-network"
				v.SubnetworkName = "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/my-subnetwork"
				return v
			},
			cloud: func() CloudConfig {
				v := cloudBoilerplate
				v.NetworkName = ""
				v.SubnetworkName = ""
				v.NetworkURL = "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/my-network"
				v.SubnetworkURL = "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/my-subnetwork"
				return v
			},
		},
		{
			name: "Multizone",
			config: func() ConfigGlobal {
				v := configBoilerplate
				v.Multizone = true
				return v
			},
			cloud: func() CloudConfig {
				v := cloudBoilerplate
				v.ManagedZones = nil
				return v
			},
		},
		{
			name: "Secondary Range Name",
			config: func() ConfigGlobal {
				v := configBoilerplate
				v.SecondaryRangeName = "my-secondary"
				return v
			},
			cloud: func() CloudConfig {
				v := cloudBoilerplate
				v.SecondaryRangeName = "my-secondary"
				return v
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			resultCloud, err := generateCloudConfig(&ConfigFile{Global: tc.config()})
			if err != nil {
				t.Fatalf("Unexpect error: %v", err)
			}

			v := tc.cloud()
			if !reflect.DeepEqual(*resultCloud, v) {
				t.Errorf("Got: \n%v\nWant\n%v\n", v, *resultCloud)
			}
		})
	}
}

func TestNewAlphaFeatureGate(t *testing.T) {
	testCases := []struct {
		alphaFeatures  []string
		expectEnabled  []string
		expectDisabled []string
	}{
		// enable foo bar
		{
			alphaFeatures:  []string{"foo", "bar"},
			expectEnabled:  []string{"foo", "bar"},
			expectDisabled: []string{"aaa"},
		},
		// no alpha feature
		{
			alphaFeatures:  []string{},
			expectEnabled:  []string{},
			expectDisabled: []string{"foo", "bar"},
		},
		// unsupported alpha feature
		{
			alphaFeatures:  []string{"aaa", "foo"},
			expectEnabled:  []string{"foo"},
			expectDisabled: []string{},
		},
		// enable foo
		{
			alphaFeatures:  []string{"foo"},
			expectEnabled:  []string{"foo"},
			expectDisabled: []string{"bar"},
		},
	}

	for _, tc := range testCases {
		featureGate := NewAlphaFeatureGate(tc.alphaFeatures)

		for _, key := range tc.expectEnabled {
			if !featureGate.Enabled(key) {
				t.Errorf("Expect %q to be enabled.", key)
			}
		}
		for _, key := range tc.expectDisabled {
			if featureGate.Enabled(key) {
				t.Errorf("Expect %q to be disabled.", key)
			}
		}
	}
}

func TestGetRegionInURL(t *testing.T) {
	cases := map[string]string{
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-central1/subnetworks/a": "us-central1",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-west2/subnetworks/b":    "us-west2",
		"projects/my-project/regions/asia-central1/subnetworks/c":                                     "asia-central1",
		"regions/europe-north2":                                                                       "europe-north2",
		"my-url":                                                                                      "",
		"":                                                                                            "",
	}
	for input, output := range cases {
		result := getRegionInURL(input)
		if result != output {
			t.Errorf("Actual result %q does not match expected result %q for input: %q", result, output, input)
		}
	}
}

func TestFindSubnetForRegion(t *testing.T) {
	s := []string{
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-central1/subnetworks/default-38b01f54907a15a7",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-west1/subnetworks/default",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-east1/subnetworks/default-277eec3815f742b6",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-east4/subnetworks/default",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/asia-northeast1/subnetworks/default",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/asia-east1/subnetworks/default-8e020b4b8b244809",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/australia-southeast1/subnetworks/default",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/southamerica-east1/subnetworks/default",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west3/subnetworks/default",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/asia-southeast1/subnetworks/default",
		"",
	}
	actual := findSubnetForRegion(s, "asia-east1")
	expectedResult := "https://www.googleapis.com/compute/v1/projects/my-project/regions/asia-east1/subnetworks/default-8e020b4b8b244809"
	if actual != expectedResult {
		t.Errorf("Actual result %q does not match expected result %q", actual, expectedResult)
	}

	var nilSlice []string
	res := findSubnetForRegion(nilSlice, "us-central1")
	if res != "" {
		t.Errorf("expected an empty result, got %v", res)
	}
}

func TestLastComponent(t *testing.T) {
	cases := map[string]string{
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-central1/subnetworks/a": "a",
		"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-central1/subnetworks/b": "b",
		"projects/my-project/regions/us-central1/subnetworks/c":                                       "c",
		"d": "d",
		"":  "",
	}
	for input, output := range cases {
		result := lastComponent(input)
		if result != output {
			t.Errorf("Actual result %q does not match expected result %q for input: %q", result, output, input)
		}
	}
}
