/*
Copyright 2021 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 (
	"errors"
	"fmt"
	"regexp"
	"testing"

	"github.com/stretchr/testify/assert"
	gce "google.golang.org/api/compute/v1"
	"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
)

var (
	errFetchMig             = errors.New("fetch migs error")
	errFetchMigInstances    = errors.New("fetch mig instances error")
	errFetchMigTargetSize   = errors.New("fetch mig target size error")
	errFetchMigBaseName     = errors.New("fetch mig basename error")
	errFetchMigTemplateName = errors.New("fetch mig template name error")
	errFetchMigTemplate     = errors.New("fetch mig template error")

	mig = &gceMig{
		gceRef: GceRef{
			Project: "project",
			Zone:    "us-test1",
			Name:    "mig",
		},
	}
)

type mockAutoscalingGceClient struct {
	fetchMigs            func(string) ([]*gce.InstanceGroupManager, error)
	fetchMigTargetSize   func(GceRef) (int64, error)
	fetchMigBasename     func(GceRef) (string, error)
	fetchMigInstances    func(GceRef) ([]cloudprovider.Instance, error)
	fetchMigTemplateName func(GceRef) (string, error)
	fetchMigTemplate     func(GceRef, string) (*gce.InstanceTemplate, error)
}

func (client *mockAutoscalingGceClient) FetchMachineType(_, _ string) (*gce.MachineType, error) {
	return nil, nil
}

func (client *mockAutoscalingGceClient) FetchMachineTypes(_ string) ([]*gce.MachineType, error) {
	return nil, nil
}

func (client *mockAutoscalingGceClient) FetchAllMigs(zone string) ([]*gce.InstanceGroupManager, error) {
	return client.fetchMigs(zone)
}

func (client *mockAutoscalingGceClient) FetchMigTargetSize(migRef GceRef) (int64, error) {
	return client.fetchMigTargetSize(migRef)
}

func (client *mockAutoscalingGceClient) FetchMigBasename(migRef GceRef) (string, error) {
	return client.fetchMigBasename(migRef)
}

func (client *mockAutoscalingGceClient) FetchMigInstances(migRef GceRef) ([]cloudprovider.Instance, error) {
	return client.fetchMigInstances(migRef)
}

func (client *mockAutoscalingGceClient) FetchMigTemplateName(migRef GceRef) (string, error) {
	return client.fetchMigTemplateName(migRef)
}

func (client *mockAutoscalingGceClient) FetchMigTemplate(migRef GceRef, templateName string) (*gce.InstanceTemplate, error) {
	return client.fetchMigTemplate(migRef, templateName)
}

func (client *mockAutoscalingGceClient) FetchMigsWithName(_ string, _ *regexp.Regexp) ([]string, error) {
	return nil, nil
}

func (client *mockAutoscalingGceClient) FetchZones(_ string) ([]string, error) {
	return nil, nil
}

func (client *mockAutoscalingGceClient) FetchAvailableCpuPlatforms() (map[string][]string, error) {
	return nil, nil
}

func (client *mockAutoscalingGceClient) ResizeMig(_ GceRef, _ int64) error {
	return nil
}

func (client *mockAutoscalingGceClient) DeleteInstances(_ GceRef, _ []GceRef) error {
	return nil
}

func (client *mockAutoscalingGceClient) CreateInstances(_ GceRef, _ string, _ int64, _ []string) error {
	return nil
}

func TestMigInfoProviderGetMigForInstance(t *testing.T) {
	instance := cloudprovider.Instance{
		Id: "gce://project/us-test1/base-instance-name-abcd",
	}
	instanceRef, err := GceRefFromProviderId(instance.Id)
	assert.Nil(t, err)

	testCases := []struct {
		name                     string
		instanceRef              GceRef
		cache                    *GceCache
		fetchMigInstances        func(GceRef) ([]cloudprovider.Instance, error)
		fetchMigBasename         func(GceRef) (string, error)
		expectedMig              Mig
		expectedErr              error
		expectedCachedMigRef     GceRef
		expectedCached           bool
		expectedCachedMigUnknown bool
	}{
		{
			name: "instance mig ref and mig in cache",
			cache: &GceCache{
				migs:           map[GceRef]Mig{mig.GceRef(): mig},
				instancesToMig: map[GceRef]GceRef{instanceRef: mig.GceRef()},
			},
			expectedMig:          mig,
			expectedCachedMigRef: mig.GceRef(),
			expectedCached:       true,
		},
		{
			name: "only instance mig ref in cache",
			cache: &GceCache{
				instancesToMig: map[GceRef]GceRef{instanceRef: mig.GceRef()},
			},
			expectedMig:          nil,
			expectedErr:          fmt.Errorf("instance %v belongs to unregistered mig %v", instanceRef, mig.GceRef()),
			expectedCachedMigRef: mig.GceRef(),
			expectedCached:       true,
		},
		{
			name: "instance mig unknown in cache",
			cache: &GceCache{
				instancesFromUnknownMig: map[GceRef]bool{instanceRef: true},
			},
			expectedCachedMigUnknown: true,
		},
		{
			name: "mig from cache fill",
			cache: &GceCache{
				migs:             map[GceRef]Mig{mig.GceRef(): mig},
				instances:        map[GceRef][]cloudprovider.Instance{},
				instancesToMig:   map[GceRef]GceRef{},
				migBaseNameCache: map[GceRef]string{mig.GceRef(): "base-instance-name"},
			},
			fetchMigInstances:    fetchMigInstancesConst([]cloudprovider.Instance{instance}),
			expectedMig:          mig,
			expectedCachedMigRef: mig.GceRef(),
			expectedCached:       true,
		},
		{
			name: "mig and basename from cache fill",
			cache: &GceCache{
				migs:             map[GceRef]Mig{mig.GceRef(): mig},
				instances:        map[GceRef][]cloudprovider.Instance{},
				instancesToMig:   map[GceRef]GceRef{},
				migBaseNameCache: map[GceRef]string{},
			},
			fetchMigInstances:    fetchMigInstancesConst([]cloudprovider.Instance{instance}),
			fetchMigBasename:     fetchMigBasenameConst("base-instance-name"),
			expectedMig:          mig,
			expectedCachedMigRef: mig.GceRef(),
			expectedCached:       true,
		},
		{
			name: "unknown mig from cache fill",
			cache: &GceCache{
				migs:                    map[GceRef]Mig{mig.GceRef(): mig},
				instances:               map[GceRef][]cloudprovider.Instance{},
				instancesFromUnknownMig: map[GceRef]bool{},
				migBaseNameCache:        map[GceRef]string{mig.GceRef(): "base-instance-name"},
			},
			fetchMigInstances:        fetchMigInstancesConst([]cloudprovider.Instance{}),
			expectedCachedMigUnknown: true,
		},
		{
			name: "no candidate mig during cache fill",
			cache: &GceCache{
				migs:             map[GceRef]Mig{mig.GceRef(): mig},
				migBaseNameCache: map[GceRef]string{mig.GceRef(): "different-base-instance-name"},
				instancesToMig:   map[GceRef]GceRef{},
			},
			fetchMigInstances: fetchMigInstancesConst([]cloudprovider.Instance{instance}),
		},
		{
			name: "fetch instances error during cache fill",
			cache: &GceCache{
				migs:             map[GceRef]Mig{mig.GceRef(): mig},
				instancesToMig:   map[GceRef]GceRef{},
				migBaseNameCache: map[GceRef]string{mig.GceRef(): "base-instance-name"},
			},
			fetchMigInstances: fetchMigInstancesFail,
			expectedErr:       errFetchMigInstances,
		},
		{
			name: "fetch basename error during cache fill",
			cache: &GceCache{
				migs:             map[GceRef]Mig{mig.GceRef(): mig},
				instancesToMig:   map[GceRef]GceRef{},
				migBaseNameCache: map[GceRef]string{},
			},
			fetchMigBasename: fetchMigBasenameFail,
			expectedErr:      nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := &mockAutoscalingGceClient{
				fetchMigBasename:  tc.fetchMigBasename,
				fetchMigInstances: tc.fetchMigInstances,
				fetchMigs:         fetchMigsConst(nil),
			}
			migLister := NewMigLister(tc.cache)
			provider := NewCachingMigInfoProvider(tc.cache, migLister, client, mig.GceRef().Project, 1)

			mig, err := provider.GetMigForInstance(instanceRef)

			cachedMigRef, cached := tc.cache.GetMigForInstance(instanceRef)
			cachedMigUnknown := tc.cache.IsMigUnknownForInstance(instanceRef)

			assert.Equal(t, tc.expectedMig, mig)
			assert.Equal(t, tc.expectedErr, err)

			assert.Equal(t, tc.expectedCachedMigRef, cachedMigRef)
			assert.Equal(t, tc.expectedCached, cached)
			assert.Equal(t, tc.expectedCachedMigUnknown, cachedMigUnknown)
		})
	}
}

func TestGetMigInstances(t *testing.T) {
	instances := []cloudprovider.Instance{
		{Id: "gce://project/us-test1/base-instance-name-abcd"},
		{Id: "gce://project/us-test1/base-instance-name-efgh"},
	}

	testCases := []struct {
		name                    string
		cache                   *GceCache
		fetchMigInstances       func(GceRef) ([]cloudprovider.Instance, error)
		expectedInstances       []cloudprovider.Instance
		expectedErr             error
		expectedCachedInstances []cloudprovider.Instance
		expectedCached          bool
	}{
		{
			name: "instances in cache",
			cache: &GceCache{
				migs:      map[GceRef]Mig{mig.GceRef(): mig},
				instances: map[GceRef][]cloudprovider.Instance{mig.GceRef(): instances},
			},
			expectedInstances:       instances,
			expectedCachedInstances: instances,
			expectedCached:          true,
		},
		{
			name: "instances cache fill",
			cache: &GceCache{
				migs:           map[GceRef]Mig{mig.GceRef(): mig},
				instances:      map[GceRef][]cloudprovider.Instance{},
				instancesToMig: map[GceRef]GceRef{},
			},
			fetchMigInstances:       fetchMigInstancesConst(instances),
			expectedInstances:       instances,
			expectedCachedInstances: instances,
			expectedCached:          true,
		},
		{
			name: "error during instances cache fill",
			cache: &GceCache{
				migs:      map[GceRef]Mig{mig.GceRef(): mig},
				instances: map[GceRef][]cloudprovider.Instance{},
			},
			fetchMigInstances:       fetchMigInstancesFail,
			expectedErr:             errFetchMigInstances,
			expectedCachedInstances: []cloudprovider.Instance{},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := &mockAutoscalingGceClient{
				fetchMigInstances: tc.fetchMigInstances,
			}
			migLister := NewMigLister(tc.cache)
			provider := NewCachingMigInfoProvider(tc.cache, migLister, client, mig.GceRef().Project, 1)
			instances, err := provider.GetMigInstances(mig.GceRef())
			cachedInstances, cached := tc.cache.GetMigInstances(mig.GceRef())

			assert.Equal(t, tc.expectedInstances, instances)
			assert.Equal(t, tc.expectedErr, err)

			assert.Equal(t, tc.expectedCachedInstances, cachedInstances)
			assert.Equal(t, tc.expectedCached, cached)
		})
	}
}

func TestRegenerateMigInstancesCache(t *testing.T) {
	otherMig := &gceMig{
		gceRef: GceRef{
			Project: "project",
			Zone:    "us-test1",
			Name:    "other-mig",
		},
	}

	instances := []cloudprovider.Instance{
		{Id: "gce://project/us-test1/base-instance-name-abcd"},
		{Id: "gce://project/us-test1/base-instance-name-efgh"},
	}
	otherInstances := []cloudprovider.Instance{
		{Id: "gce://project/us-test1/other-base-instance-name-abcd"},
		{Id: "gce://project/us-test1/other-base-instance-name-efgh"},
	}

	var instancesRefs, otherInstancesRefs []GceRef
	for _, instance := range instances {
		instanceRef, err := GceRefFromProviderId(instance.Id)
		assert.Nil(t, err)
		instancesRefs = append(instancesRefs, instanceRef)
	}
	for _, instance := range otherInstances {
		instanceRef, err := GceRefFromProviderId(instance.Id)
		assert.Nil(t, err)
		otherInstancesRefs = append(otherInstancesRefs, instanceRef)
	}

	testCases := []struct {
		name                   string
		cache                  *GceCache
		fetchMigInstances      func(GceRef) ([]cloudprovider.Instance, error)
		expectedErr            error
		expectedMigInstances   map[GceRef][]cloudprovider.Instance
		expectedInstancesToMig map[GceRef]GceRef
	}{
		{
			name: "fill empty cache for one mig",
			cache: &GceCache{
				migs:           map[GceRef]Mig{mig.GceRef(): mig},
				instances:      map[GceRef][]cloudprovider.Instance{},
				instancesToMig: map[GceRef]GceRef{},
			},
			fetchMigInstances: fetchMigInstancesConst(instances),
			expectedMigInstances: map[GceRef][]cloudprovider.Instance{
				mig.GceRef(): instances,
			},
			expectedInstancesToMig: map[GceRef]GceRef{
				instancesRefs[0]: mig.GceRef(),
				instancesRefs[1]: mig.GceRef(),
			},
		},
		{
			name: "fill empty cache for two migs",
			cache: &GceCache{
				migs: map[GceRef]Mig{
					mig.GceRef():      mig,
					otherMig.GceRef(): otherMig,
				},
				instances:      map[GceRef][]cloudprovider.Instance{},
				instancesToMig: map[GceRef]GceRef{},
			},
			fetchMigInstances: fetchMigInstancesMapping(map[GceRef][]cloudprovider.Instance{
				mig.GceRef():      instances,
				otherMig.GceRef(): otherInstances,
			}),
			expectedMigInstances: map[GceRef][]cloudprovider.Instance{
				mig.GceRef():      instances,
				otherMig.GceRef(): otherInstances,
			},
			expectedInstancesToMig: map[GceRef]GceRef{
				instancesRefs[0]:      mig.GceRef(),
				instancesRefs[1]:      mig.GceRef(),
				otherInstancesRefs[0]: otherMig.GceRef(),
				otherInstancesRefs[1]: otherMig.GceRef(),
			},
		},
		{
			name: "clear cache for removed mig",
			cache: &GceCache{
				migs: map[GceRef]Mig{
					mig.GceRef(): mig,
				},
				instances: map[GceRef][]cloudprovider.Instance{
					mig.GceRef():      instances,
					otherMig.GceRef(): otherInstances,
				},
				instancesToMig: map[GceRef]GceRef{
					instancesRefs[0]:      mig.GceRef(),
					instancesRefs[1]:      mig.GceRef(),
					otherInstancesRefs[0]: otherMig.GceRef(),
					otherInstancesRefs[1]: otherMig.GceRef(),
				},
			},
			fetchMigInstances: fetchMigInstancesMapping(map[GceRef][]cloudprovider.Instance{
				mig.GceRef():      instances,
				otherMig.GceRef(): otherInstances,
			}),
			expectedMigInstances: map[GceRef][]cloudprovider.Instance{
				mig.GceRef(): instances,
			},
			expectedInstancesToMig: map[GceRef]GceRef{
				instancesRefs[0]: mig.GceRef(),
				instancesRefs[1]: mig.GceRef(),
			},
		},
		{
			name: "override cache for changed instances",
			cache: &GceCache{
				migs: map[GceRef]Mig{
					mig.GceRef(): mig,
				},
				instances: map[GceRef][]cloudprovider.Instance{
					mig.GceRef(): instances,
				},
				instancesToMig: map[GceRef]GceRef{
					instancesRefs[0]: mig.GceRef(),
					instancesRefs[1]: mig.GceRef(),
				},
			},
			fetchMigInstances: fetchMigInstancesConst(otherInstances),
			expectedMigInstances: map[GceRef][]cloudprovider.Instance{
				mig.GceRef(): otherInstances,
			},
			expectedInstancesToMig: map[GceRef]GceRef{
				otherInstancesRefs[0]: mig.GceRef(),
				otherInstancesRefs[1]: mig.GceRef(),
			},
		},
		{
			name: "refill mig instances error",
			cache: &GceCache{
				migs: map[GceRef]Mig{
					mig.GceRef(): mig,
				},
				instances:      map[GceRef][]cloudprovider.Instance{},
				instancesToMig: map[GceRef]GceRef{},
			},
			fetchMigInstances: fetchMigInstancesFail,
			expectedErr:       errFetchMigInstances,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := &mockAutoscalingGceClient{
				fetchMigInstances: tc.fetchMigInstances,
			}
			migLister := NewMigLister(tc.cache)
			provider := NewCachingMigInfoProvider(tc.cache, migLister, client, mig.GceRef().Project, 1)
			err := provider.RegenerateMigInstancesCache()

			assert.Equal(t, tc.expectedErr, err)
			if tc.expectedErr == nil {
				assert.Equal(t, tc.expectedMigInstances, tc.cache.instances)
				assert.Equal(t, tc.expectedInstancesToMig, tc.cache.instancesToMig)
			}
		})
	}
}

func TestGetMigTargetSize(t *testing.T) {
	targetSize := int64(42)
	instanceGroupManager := &gce.InstanceGroupManager{
		Zone:       mig.GceRef().Zone,
		Name:       mig.GceRef().Name,
		TargetSize: targetSize,
	}

	testCases := []struct {
		name               string
		cache              *GceCache
		fetchMigs          func(string) ([]*gce.InstanceGroupManager, error)
		fetchMigTargetSize func(GceRef) (int64, error)
		expectedTargetSize int64
		expectedErr        error
	}{
		{
			name: "target size in cache",
			cache: &GceCache{
				migs:               map[GceRef]Mig{mig.GceRef(): mig},
				migTargetSizeCache: map[GceRef]int64{mig.GceRef(): targetSize},
			},
			expectedTargetSize: targetSize,
		},
		{
			name:               "target size from cache fill",
			cache:              emptyCache(),
			fetchMigs:          fetchMigsConst([]*gce.InstanceGroupManager{instanceGroupManager}),
			expectedTargetSize: targetSize,
		},
		{
			name:               "cache fill without mig, fallback success",
			cache:              emptyCache(),
			fetchMigs:          fetchMigsConst([]*gce.InstanceGroupManager{}),
			fetchMigTargetSize: fetchMigTargetSizeConst(targetSize),
			expectedTargetSize: targetSize,
		},
		{
			name:               "cache fill failure, fallback success",
			cache:              emptyCache(),
			fetchMigs:          fetchMigsFail,
			fetchMigTargetSize: fetchMigTargetSizeConst(targetSize),
			expectedTargetSize: targetSize,
		},
		{
			name:               "cache fill without mig, fallback failure",
			cache:              emptyCache(),
			fetchMigs:          fetchMigsConst([]*gce.InstanceGroupManager{}),
			fetchMigTargetSize: fetchMigTargetSizeFail,
			expectedErr:        errFetchMigTargetSize,
		},
		{
			name:               "cache fill failure, fallback failure",
			cache:              emptyCache(),
			fetchMigs:          fetchMigsFail,
			fetchMigTargetSize: fetchMigTargetSizeFail,
			expectedErr:        errFetchMigTargetSize,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := &mockAutoscalingGceClient{
				fetchMigs:          tc.fetchMigs,
				fetchMigTargetSize: tc.fetchMigTargetSize,
			}
			migLister := NewMigLister(tc.cache)
			provider := NewCachingMigInfoProvider(tc.cache, migLister, client, mig.GceRef().Project, 1)

			targetSize, err := provider.GetMigTargetSize(mig.GceRef())
			cachedTargetSize, found := tc.cache.GetMigTargetSize(mig.GceRef())

			assert.Equal(t, tc.expectedErr, err)
			assert.Equal(t, tc.expectedErr == nil, found)
			if tc.expectedErr == nil {
				assert.Equal(t, tc.expectedTargetSize, targetSize)
				assert.Equal(t, tc.expectedTargetSize, cachedTargetSize)
			}
		})
	}
}

func TestGetMigBasename(t *testing.T) {
	basename := "base-instance-name"
	instanceGroupManager := &gce.InstanceGroupManager{
		Zone:             mig.GceRef().Zone,
		Name:             mig.GceRef().Name,
		BaseInstanceName: basename,
	}

	testCases := []struct {
		name             string
		cache            *GceCache
		fetchMigs        func(string) ([]*gce.InstanceGroupManager, error)
		fetchMigBasename func(GceRef) (string, error)
		expectedBasename string
		expectedErr      error
	}{
		{
			name: "basename in cache",
			cache: &GceCache{
				migs:             map[GceRef]Mig{mig.GceRef(): mig},
				migBaseNameCache: map[GceRef]string{mig.GceRef(): basename},
			},
			expectedBasename: basename,
		},
		{
			name:             "target size from cache fill",
			cache:            emptyCache(),
			fetchMigs:        fetchMigsConst([]*gce.InstanceGroupManager{instanceGroupManager}),
			expectedBasename: basename,
		},
		{
			name:             "cache fill without mig, fallback success",
			cache:            emptyCache(),
			fetchMigs:        fetchMigsConst([]*gce.InstanceGroupManager{}),
			fetchMigBasename: fetchMigBasenameConst(basename),
			expectedBasename: basename,
		},
		{
			name:             "cache fill failure, fallback success",
			cache:            emptyCache(),
			fetchMigs:        fetchMigsFail,
			fetchMigBasename: fetchMigBasenameConst(basename),
			expectedBasename: basename,
		},
		{
			name:             "cache fill without mig, fallback failure",
			cache:            emptyCache(),
			fetchMigs:        fetchMigsConst([]*gce.InstanceGroupManager{}),
			fetchMigBasename: fetchMigBasenameFail,
			expectedErr:      errFetchMigBaseName,
		},
		{
			name:             "cache fill failure, fallback failure",
			cache:            emptyCache(),
			fetchMigs:        fetchMigsFail,
			fetchMigBasename: fetchMigBasenameFail,
			expectedErr:      errFetchMigBaseName,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := &mockAutoscalingGceClient{
				fetchMigs:        tc.fetchMigs,
				fetchMigBasename: tc.fetchMigBasename,
			}
			migLister := NewMigLister(tc.cache)
			provider := NewCachingMigInfoProvider(tc.cache, migLister, client, mig.GceRef().Project, 1)

			basename, err := provider.GetMigBasename(mig.GceRef())
			cachedBasename, found := tc.cache.GetMigBasename(mig.GceRef())

			assert.Equal(t, tc.expectedErr, err)
			assert.Equal(t, tc.expectedErr == nil, found)
			if tc.expectedErr == nil {
				assert.Equal(t, tc.expectedBasename, basename)
				assert.Equal(t, tc.expectedBasename, cachedBasename)
			}
		})
	}
}

func TestGetMigInstanceTemplateName(t *testing.T) {
	templateName := "template-name"
	instanceGroupManager := &gce.InstanceGroupManager{
		Zone:             mig.GceRef().Zone,
		Name:             mig.GceRef().Name,
		InstanceTemplate: templateName,
	}

	testCases := []struct {
		name                 string
		cache                *GceCache
		fetchMigs            func(string) ([]*gce.InstanceGroupManager, error)
		fetchMigTemplateName func(GceRef) (string, error)
		expectedTemplateName string
		expectedErr          error
	}{
		{
			name: "template name in cache",
			cache: &GceCache{
				migs:                      map[GceRef]Mig{mig.GceRef(): mig},
				instanceTemplateNameCache: map[GceRef]string{mig.GceRef(): templateName},
			},
			expectedTemplateName: templateName,
		},
		{
			name:                 "target size from cache fill",
			cache:                emptyCache(),
			fetchMigs:            fetchMigsConst([]*gce.InstanceGroupManager{instanceGroupManager}),
			expectedTemplateName: templateName,
		},
		{
			name:                 "cache fill without mig, fallback success",
			cache:                emptyCache(),
			fetchMigs:            fetchMigsConst([]*gce.InstanceGroupManager{}),
			fetchMigTemplateName: fetchMigTemplateNameConst(templateName),
			expectedTemplateName: templateName,
		},
		{
			name:                 "cache fill failure, fallback success",
			cache:                emptyCache(),
			fetchMigs:            fetchMigsFail,
			fetchMigTemplateName: fetchMigTemplateNameConst(templateName),
			expectedTemplateName: templateName,
		},
		{
			name:                 "cache fill without mig, fallback failure",
			cache:                emptyCache(),
			fetchMigs:            fetchMigsConst([]*gce.InstanceGroupManager{}),
			fetchMigTemplateName: fetchMigTemplateNameFail,
			expectedErr:          errFetchMigTemplateName,
		},
		{
			name:                 "cache fill failure, fallback failure",
			cache:                emptyCache(),
			fetchMigs:            fetchMigsFail,
			fetchMigTemplateName: fetchMigTemplateNameFail,
			expectedErr:          errFetchMigTemplateName,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := &mockAutoscalingGceClient{
				fetchMigs:            tc.fetchMigs,
				fetchMigTemplateName: tc.fetchMigTemplateName,
			}
			migLister := NewMigLister(tc.cache)
			provider := NewCachingMigInfoProvider(tc.cache, migLister, client, mig.GceRef().Project, 1)

			templateName, err := provider.GetMigInstanceTemplateName(mig.GceRef())
			cachedTemplateName, found := tc.cache.GetMigInstanceTemplateName(mig.GceRef())

			assert.Equal(t, tc.expectedErr, err)
			assert.Equal(t, tc.expectedErr == nil, found)
			if tc.expectedErr == nil {
				assert.Equal(t, tc.expectedTemplateName, templateName)
				assert.Equal(t, tc.expectedTemplateName, cachedTemplateName)
			}
		})
	}
}

func TestGetMigInstanceTemplate(t *testing.T) {
	templateName := "template-name"
	template := &gce.InstanceTemplate{
		Name:        templateName,
		Description: "instance template",
	}
	oldTemplate := &gce.InstanceTemplate{
		Name:        "old-template-name",
		Description: "old instance template",
	}

	testCases := []struct {
		name                   string
		cache                  *GceCache
		fetchMigs              func(string) ([]*gce.InstanceGroupManager, error)
		fetchMigTemplateName   func(GceRef) (string, error)
		fetchMigTemplate       func(GceRef, string) (*gce.InstanceTemplate, error)
		expectedTemplate       *gce.InstanceTemplate
		expectedCachedTemplate *gce.InstanceTemplate
		expectedErr            error
	}{
		{
			name: "template in cache",
			cache: &GceCache{
				migs:                      map[GceRef]Mig{mig.GceRef(): mig},
				instanceTemplateNameCache: map[GceRef]string{mig.GceRef(): templateName},
				instanceTemplatesCache:    map[GceRef]*gce.InstanceTemplate{mig.GceRef(): template},
			},
			expectedTemplate:       template,
			expectedCachedTemplate: template,
		},
		{
			name: "cache without template, fetch success",
			cache: &GceCache{
				migs:                      map[GceRef]Mig{mig.GceRef(): mig},
				instanceTemplateNameCache: map[GceRef]string{mig.GceRef(): templateName},
				instanceTemplatesCache:    make(map[GceRef]*gce.InstanceTemplate),
			},
			fetchMigTemplate:       fetchMigTemplateConst(template),
			expectedTemplate:       template,
			expectedCachedTemplate: template,
		},
		{
			name: "cache with old template, fetch success",
			cache: &GceCache{
				migs:                      map[GceRef]Mig{mig.GceRef(): mig},
				instanceTemplateNameCache: map[GceRef]string{mig.GceRef(): templateName},
				instanceTemplatesCache:    map[GceRef]*gce.InstanceTemplate{mig.GceRef(): oldTemplate},
			},
			fetchMigTemplate:       fetchMigTemplateConst(template),
			expectedTemplate:       template,
			expectedCachedTemplate: template,
		},
		{
			name: "cache without template, fetch failure",
			cache: &GceCache{
				migs:                      map[GceRef]Mig{mig.GceRef(): mig},
				instanceTemplateNameCache: map[GceRef]string{mig.GceRef(): templateName},
				instanceTemplatesCache:    make(map[GceRef]*gce.InstanceTemplate),
			},
			fetchMigTemplate: fetchMigTemplateFail,
			expectedErr:      errFetchMigTemplate,
		},
		{
			name: "cache with old template, fetch failure",
			cache: &GceCache{
				migs:                      map[GceRef]Mig{mig.GceRef(): mig},
				instanceTemplateNameCache: map[GceRef]string{mig.GceRef(): templateName},
				instanceTemplatesCache:    map[GceRef]*gce.InstanceTemplate{mig.GceRef(): oldTemplate},
			},
			fetchMigTemplate:       fetchMigTemplateFail,
			expectedCachedTemplate: oldTemplate,
			expectedErr:            errFetchMigTemplate,
		},
		{
			name:                 "template name fetch failure",
			cache:                emptyCache(),
			fetchMigs:            fetchMigsFail,
			fetchMigTemplateName: fetchMigTemplateNameFail,
			expectedErr:          errFetchMigTemplateName,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := &mockAutoscalingGceClient{
				fetchMigs:            tc.fetchMigs,
				fetchMigTemplateName: tc.fetchMigTemplateName,
				fetchMigTemplate:     tc.fetchMigTemplate,
			}
			migLister := NewMigLister(tc.cache)
			provider := NewCachingMigInfoProvider(tc.cache, migLister, client, mig.GceRef().Project, 1)

			template, err := provider.GetMigInstanceTemplate(mig.GceRef())
			cachedTemplate, found := tc.cache.GetMigInstanceTemplate(mig.GceRef())

			assert.Equal(t, tc.expectedErr, err)
			if tc.expectedErr == nil {
				assert.Equal(t, tc.expectedTemplate, template)
			}

			assert.Equal(t, tc.expectedCachedTemplate != nil, found)
			if tc.expectedCachedTemplate != nil {
				assert.Equal(t, tc.expectedCachedTemplate, cachedTemplate)
			}
		})
	}
}

func emptyCache() *GceCache {
	return &GceCache{
		migs:                      map[GceRef]Mig{mig.GceRef(): mig},
		instances:                 make(map[GceRef][]cloudprovider.Instance),
		migTargetSizeCache:        make(map[GceRef]int64),
		migBaseNameCache:          make(map[GceRef]string),
		instanceTemplateNameCache: make(map[GceRef]string),
		instanceTemplatesCache:    make(map[GceRef]*gce.InstanceTemplate),
	}
}

func fetchMigsFail(_ string) ([]*gce.InstanceGroupManager, error) {
	return nil, errFetchMig
}

func fetchMigsConst(migs []*gce.InstanceGroupManager) func(string) ([]*gce.InstanceGroupManager, error) {
	return func(string) ([]*gce.InstanceGroupManager, error) {
		return migs, nil
	}
}

func fetchMigInstancesFail(_ GceRef) ([]cloudprovider.Instance, error) {
	return nil, errFetchMigInstances
}

func fetchMigInstancesConst(instances []cloudprovider.Instance) func(GceRef) ([]cloudprovider.Instance, error) {
	return func(GceRef) ([]cloudprovider.Instance, error) {
		return instances, nil
	}
}

func fetchMigInstancesMapping(instancesMapping map[GceRef][]cloudprovider.Instance) func(GceRef) ([]cloudprovider.Instance, error) {
	return func(migRef GceRef) ([]cloudprovider.Instance, error) {
		return instancesMapping[migRef], nil
	}
}

func fetchMigTargetSizeFail(_ GceRef) (int64, error) {
	return 0, errFetchMigTargetSize
}

func fetchMigTargetSizeConst(targetSize int64) func(GceRef) (int64, error) {
	return func(GceRef) (int64, error) {
		return targetSize, nil
	}
}

func fetchMigBasenameFail(_ GceRef) (string, error) {
	return "", errFetchMigBaseName
}

func fetchMigBasenameConst(basename string) func(GceRef) (string, error) {
	return func(GceRef) (string, error) {
		return basename, nil
	}
}

func fetchMigTemplateNameFail(_ GceRef) (string, error) {
	return "", errFetchMigTemplateName
}

func fetchMigTemplateNameConst(templateName string) func(GceRef) (string, error) {
	return func(GceRef) (string, error) {
		return templateName, nil
	}
}

func fetchMigTemplateFail(_ GceRef, _ string) (*gce.InstanceTemplate, error) {
	return nil, errFetchMigTemplate
}

func fetchMigTemplateConst(template *gce.InstanceTemplate) func(GceRef, string) (*gce.InstanceTemplate, error) {
	return func(GceRef, string) (*gce.InstanceTemplate, error) {
		return template, nil
	}
}
