/*
Copyright 2020 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 instancegroups

import (
	"context"
	"testing"
	"time"

	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups"
	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
	"github.com/stretchr/testify/assert"
	v1 "k8s.io/api/core/v1"
	v1meta "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes/fake"
	kopsapi "k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/assets"
	"k8s.io/kops/pkg/client/simple/vfsclientset"
	"k8s.io/kops/pkg/cloudinstances"
	"k8s.io/kops/pkg/testutils"
	"k8s.io/kops/upup/pkg/fi/cloudup"
	"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
	"k8s.io/kops/util/pkg/vfs"
)

func getTestSetupOS(t *testing.T, ctx context.Context) (*RollingUpdateCluster, *openstack.MockCloud) {
	vfs.Context.ResetMemfsContext(true)

	k8sClient := fake.NewClientset()

	mockcloud := testutils.SetupMockOpenstack()

	inCluster := testutils.BuildMinimalClusterAWS("test.k8s.local")

	inCluster.Spec.CloudProvider.Openstack = &kopsapi.OpenstackSpec{}
	inCluster.Name = "test.k8s.local"

	err := cloudup.PerformAssignments(inCluster, vfs.Context, mockcloud)
	if err != nil {
		t.Fatalf("Failed to perform assignments: %v", err)
	}

	assetBuilder := assets.NewAssetBuilder(vfs.Context, inCluster.Spec.Assets, false)
	basePath, _ := vfs.Context.BuildVfsPath(inCluster.Spec.ConfigStore.Base)
	clientset := vfsclientset.NewVFSClientset(vfs.Context, basePath)
	cluster, err := cloudup.PopulateClusterSpec(ctx, clientset, inCluster, nil, mockcloud, assetBuilder)
	if err != nil {
		t.Fatalf("Failed to populate cluster spec: %v", err)
	}

	sshPublicKey := []byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDF2sghZsClUBXJB4mBMIw8rb0hJWjg1Vz4eUeXwYmTdi92Gf1zNc5xISSip9Y+PWX/jJokPB7tgPnMD/2JOAKhG1bi4ZqB15pYRmbbBekVpM4o4E0dx+czbqjiAm6wlccTrINK5LYenbucAAQt19eH+D0gJwzYUK9SYz1hWnlGS+qurt2bz7rrsG73lN8E2eiNvGtIXqv3GabW/Hea3acOBgCUJQWUDTRu0OmmwxzKbFN/UpNKeRaHlCqwZWjVAsmqA8TX8LIocq7Np7MmIBwt7EpEeZJxThcmC8DEJs9ClAjD+jlLIvMPXKC3JWCPgwCLGxHjy7ckSGFCSzbyPduh")
	sshCredentialStore, err := clientset.SSHCredentialStore(cluster)
	if err != nil {
		t.Fatalf("Failed to get credential store: %v", err)
	}

	sshCredentialStore.AddSSHPublicKey(ctx, sshPublicKey)

	c := &RollingUpdateCluster{
		Cloud:                   mockcloud,
		MasterInterval:          1 * time.Millisecond,
		NodeInterval:            1 * time.Millisecond,
		BastionInterval:         1 * time.Millisecond,
		Force:                   false,
		K8sClient:               k8sClient,
		ClusterValidator:        &successfulClusterValidator{},
		FailOnValidate:          true,
		ValidateTickDuration:    1 * time.Millisecond,
		ValidateSuccessDuration: 5 * time.Millisecond,
		ValidateCount:           2,
		Cluster:                 cluster,
		Clientset:               clientset,
	}

	return c, mockcloud
}

// Ref: https://github.com/dominikh/go-tools/issues/633#issuecomment-606560616
var TempTestSkip = func(t *testing.T, message string) {
	t.Skip(message)
}

func TestRollingUpdateDisabledSurgeOS(t *testing.T) {
	ctx := context.TODO()

	TempTestSkip(t, "Failing in new release PR when build is not yet published")

	t.Setenv("OS_REGION_NAME", "us-test1")

	c, cloud := getTestSetupOS(t, ctx)

	groups, igList := getGroupsAllNeedUpdateOS(t, c)
	err := c.RollingUpdate(ctx, groups, igList)
	assert.NoError(t, err, "rolling update")

	assertGroupInstanceCountOS(t, cloud, "node-1", 3)
	assertGroupInstanceCountOS(t, cloud, "node-2", 3)
	assertGroupInstanceCountOS(t, cloud, "master-1", 2)
	assertGroupInstanceCountOS(t, cloud, "bastion-1", 1)
}

func makeGroupOS(t *testing.T, groups map[string]*cloudinstances.CloudInstanceGroup, igList *kopsapi.InstanceGroupList,
	c *RollingUpdateCluster, subnet string, role kopsapi.InstanceGroupRole, count int, needUpdate int) {
	ctx := context.TODO()

	cloud := c.Cloud.(*openstack.MockCloud)
	igif := c.Clientset.InstanceGroupsFor(c.Cluster)
	fakeClient := c.K8sClient.(*fake.Clientset)

	var newIg kopsapi.InstanceGroup
	switch role {
	case kopsapi.InstanceGroupRoleNode:
		newIg = testutils.BuildMinimalNodeInstanceGroup("nodes-"+subnet, subnet)
	case kopsapi.InstanceGroupRoleControlPlane:
		newIg = testutils.BuildMinimalMasterInstanceGroup(subnet)
	case kopsapi.InstanceGroupRoleBastion:
		newIg = testutils.BuildMinimalBastionInstanceGroup("bastion-"+subnet, subnet)
	}

	newIg.Spec.MachineType = "n1-standard-2"
	newIg.Spec.Image = "Ubuntu-20.04"

	igList.Items = append(igList.Items, newIg)

	ig, err := igif.Create(ctx, &newIg, v1meta.CreateOptions{})
	if err != nil {
		t.Fatalf("Failed to create ig %v: %v", subnet, err)
	}

	groups[subnet] = &cloudinstances.CloudInstanceGroup{
		HumanName:     ig.ObjectMeta.Name,
		InstanceGroup: ig,
	}
	for i := 0; i < count; i++ {
		name := subnet + string(rune('a'+i))
		port, err := cloud.CreatePort(ports.CreateOpts{
			Name:      name,
			NetworkID: "test",
		})
		if err != nil {
			t.Fatalf("Failed to make port: %v", err)
		}
		server, err := cloud.CreateInstance(servers.CreateOpts{
			Name: name,
			Networks: []servers.Network{
				{
					Port: port.ID,
				},
			},
		}, nil, "")
		if err != nil {
			t.Fatalf("Failed to make group: %v", err)
		}
		id := server.ID
		var node *v1.Node
		if role != kopsapi.InstanceGroupRoleBastion {
			node = &v1.Node{
				ObjectMeta: v1meta.ObjectMeta{Name: id + ".local"},
			}
			_ = fakeClient.Tracker().Add(node)
		}
		member := cloudinstances.CloudInstance{
			ID:                 id,
			Node:               node,
			CloudInstanceGroup: groups[subnet],
		}
		if i < needUpdate {
			groups[subnet].NeedUpdate = append(groups[subnet].NeedUpdate, &member)
		} else {
			groups[subnet].Ready = append(groups[subnet].Ready, &member)
		}
	}
}

func getGroupsAllNeedUpdateOS(t *testing.T, c *RollingUpdateCluster) (map[string]*cloudinstances.CloudInstanceGroup, *kopsapi.InstanceGroupList) {
	groups := make(map[string]*cloudinstances.CloudInstanceGroup)
	igList := &kopsapi.InstanceGroupList{}
	makeGroupOS(t, groups, igList, c, c.Cluster.Spec.Networking.Subnets[0].Name, kopsapi.InstanceGroupRoleNode, 3, 3)
	makeGroupOS(t, groups, igList, c, c.Cluster.Spec.Networking.Subnets[1].Name, kopsapi.InstanceGroupRoleNode, 3, 3)
	makeGroupOS(t, groups, igList, c, c.Cluster.Spec.Networking.Subnets[0].Name, kopsapi.InstanceGroupRoleControlPlane, 1, 1)
	makeGroupOS(t, groups, igList, c, c.Cluster.Spec.Networking.Subnets[1].Name, kopsapi.InstanceGroupRoleControlPlane, 1, 1)
	makeGroupOS(t, groups, igList, c, c.Cluster.Spec.Networking.Subnets[2].Name, kopsapi.InstanceGroupRoleControlPlane, 1, 1)
	makeGroupOS(t, groups, igList, c, c.Cluster.Spec.Networking.Subnets[0].Name, kopsapi.InstanceGroupRoleBastion, 1, 1)
	return groups, igList
}

func assertGroupInstanceCountOS(t *testing.T, cloud *openstack.MockCloud, groupName string, expected int) {
	groups, _ := cloud.ListServerGroups(servergroups.ListOpts{})
	for _, g := range groups {
		if g.Name == groupName {
			assert.Lenf(t, g.Members, expected, "%s instances", groupName)
		}
	}
}
