package e2e

import (
	"context"
	"fmt"
	"strings"
	"time"

	. "github.com/onsi/gomega" // nolint
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"
	"k8s.io/cloud-provider/volume/helpers"
	"k8s.io/kubernetes/test/e2e/framework"
	e2elog "k8s.io/kubernetes/test/e2e/framework/log"
	testutils "k8s.io/kubernetes/test/utils"
)

func expandPVCSize(c kubernetes.Interface, pvc *v1.PersistentVolumeClaim, size string, t int) error {
	pvcName := pvc.Name
	updatedPVC := pvc.DeepCopy()
	var err error

	updatedPVC, err = c.CoreV1().PersistentVolumeClaims(pvc.Namespace).Get(context.TODO(), pvcName, metav1.GetOptions{})
	if err != nil {
		return fmt.Errorf("error fetching pvc %q with %v", pvcName, err)
	}
	timeout := time.Duration(t) * time.Minute

	updatedPVC.Spec.Resources.Requests[v1.ResourceStorage] = resource.MustParse(size)
	_, err = c.CoreV1().PersistentVolumeClaims(updatedPVC.Namespace).Update(context.TODO(), updatedPVC, metav1.UpdateOptions{})
	Expect(err).Should(BeNil())

	start := time.Now()
	e2elog.Logf("Waiting up to %v to be in Resized state", pvc)
	return wait.PollImmediate(poll, timeout, func() (bool, error) {
		e2elog.Logf("waiting for PVC %s (%d seconds elapsed)", updatedPVC.Name, int(time.Since(start).Seconds()))
		updatedPVC, err = c.CoreV1().PersistentVolumeClaims(updatedPVC.Namespace).Get(context.TODO(), pvcName, metav1.GetOptions{})
		if err != nil {
			e2elog.Logf("Error getting pvc in namespace: '%s': %v", updatedPVC.Namespace, err)
			if testutils.IsRetryableAPIError(err) {
				return false, nil
			}
			return false, err
		}
		pvcConditions := updatedPVC.Status.Conditions
		if len(pvcConditions) > 0 {
			e2elog.Logf("pvc state %v", pvcConditions[0].Type)
			if pvcConditions[0].Type == v1.PersistentVolumeClaimResizing || pvcConditions[0].Type == v1.PersistentVolumeClaimFileSystemResizePending {
				return false, nil
			}
		}

		if updatedPVC.Status.Capacity[v1.ResourceStorage] != resource.MustParse(size) {
			e2elog.Logf("current size in status %v,expected size %v", updatedPVC.Status.Capacity[v1.ResourceStorage], resource.MustParse(size))
			return false, nil
		}
		return true, nil
	})
}

func resizePVCAndValidateSize(pvcPath, appPath string, f *framework.Framework) error {
	size := "1Gi"
	expandSize := "10Gi"
	pvc, err := loadPVC(pvcPath)
	if pvc == nil {
		return err
	}
	pvc.Namespace = f.UniqueName

	resizePvc, err := loadPVC(pvcPath)
	if resizePvc == nil {
		return err
	}
	resizePvc.Namespace = f.UniqueName

	app, err := loadApp(appPath)
	if err != nil {
		return err
	}
	pvc.Spec.Resources.Requests[v1.ResourceStorage] = resource.MustParse(size)
	app.Labels = map[string]string{"app": "resize-pvc"}
	app.Namespace = f.UniqueName

	err = createPVCAndApp("", f, pvc, app, deployTimeout)
	if err != nil {
		return err
	}

	opt := metav1.ListOptions{
		LabelSelector: "app=resize-pvc",
	}

	pvc, err = f.ClientSet.CoreV1().PersistentVolumeClaims(pvc.Namespace).Get(context.TODO(), pvc.Name, metav1.GetOptions{})
	if err != nil {
		return err
	}
	if *pvc.Spec.VolumeMode == v1.PersistentVolumeFilesystem {
		err = checkDirSize(app, f, &opt, size)
		if err != nil {
			return err
		}
	}

	if *pvc.Spec.VolumeMode == v1.PersistentVolumeBlock {
		err = checkDeviceSize(app, f, &opt, size)
		if err != nil {
			return err
		}
	}
	// resize PVC
	err = expandPVCSize(f.ClientSet, resizePvc, expandSize, deployTimeout)
	if err != nil {
		return err
	}
	// wait for application pod to come up after resize
	err = waitForPodInRunningState(app.Name, app.Namespace, f.ClientSet, deployTimeout)
	if err != nil {
		return err
	}
	if *pvc.Spec.VolumeMode == v1.PersistentVolumeFilesystem {
		err = checkDirSize(app, f, &opt, expandSize)
		if err != nil {
			return err
		}
	}

	if *pvc.Spec.VolumeMode == v1.PersistentVolumeBlock {
		err = checkDeviceSize(app, f, &opt, expandSize)
		if err != nil {
			return err
		}
	}
	err = deletePVCAndApp("", f, resizePvc, app)
	return err
}

func checkDirSize(app *v1.Pod, f *framework.Framework, opt *metav1.ListOptions, size string) error {
	cmd := getDirSizeCheckCmd(app.Spec.Containers[0].VolumeMounts[0].MountPath)
	return checkAppMntSize(f, opt, size, cmd, app.Namespace, deployTimeout)
}

func checkDeviceSize(app *v1.Pod, f *framework.Framework, opt *metav1.ListOptions, size string) error {
	cmd := getDeviceSizeCheckCmd(app.Spec.Containers[0].VolumeDevices[0].DevicePath)
	return checkAppMntSize(f, opt, size, cmd, app.Namespace, deployTimeout)
}

func getDirSizeCheckCmd(dirPath string) string {
	return fmt.Sprintf("df -h|grep %s |awk '{print $2}'", dirPath)
}

func getDeviceSizeCheckCmd(devPath string) string {
	return fmt.Sprintf("blockdev --getsize64 %s", devPath)
}

func checkAppMntSize(f *framework.Framework, opt *metav1.ListOptions, size, cmd, ns string, t int) error {
	timeout := time.Duration(t) * time.Minute
	start := time.Now()

	return wait.PollImmediate(poll, timeout, func() (bool, error) {
		e2elog.Logf("executing cmd %s (%d seconds elapsed)", cmd, int(time.Since(start).Seconds()))
		output, stdErr := execCommandInPod(f, cmd, ns, opt)

		if stdErr != "" {
			e2elog.Logf("failed to execute command in app pod %v", stdErr)
			return false, nil
		}
		s := resource.MustParse(strings.TrimSpace(output))
		actualSize := helpers.RoundUpToGiB(s)

		s = resource.MustParse(size)
		expectedSize := helpers.RoundUpToGiB(s)

		if actualSize != expectedSize {
			e2elog.Logf("expected size %s found %s information", size, output)
			return false, nil
		}
		return true, nil
	})
}
