package common

import (
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/packer/packer-plugin-sdk/template/config"
	"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
)

func TestBlockDevice(t *testing.T) {
	cases := []struct {
		Config *BlockDevice
		Result *ec2.BlockDeviceMapping
	}{
		{
			Config: &BlockDevice{
				DeviceName:          "/dev/sdb",
				SnapshotId:          "snap-1234",
				VolumeType:          "standard",
				VolumeSize:          8,
				DeleteOnTermination: true,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					SnapshotId:          aws.String("snap-1234"),
					VolumeType:          aws.String("standard"),
					VolumeSize:          aws.Int64(8),
					DeleteOnTermination: aws.Bool(true),
				},
			},
		},
		{
			Config: &BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeSize: 8,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					VolumeSize:          aws.Int64(8),
					DeleteOnTermination: aws.Bool(false),
				},
			},
		},
		{
			Config: &BlockDevice{
				DeviceName:          "/dev/sdb",
				VolumeType:          "io1",
				VolumeSize:          8,
				DeleteOnTermination: true,
				IOPS:                1000,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					VolumeType:          aws.String("io1"),
					VolumeSize:          aws.Int64(8),
					DeleteOnTermination: aws.Bool(true),
					Iops:                aws.Int64(1000),
				},
			},
		},
		{
			Config: &BlockDevice{
				DeviceName:          "/dev/sdb",
				VolumeType:          "io2",
				VolumeSize:          8,
				DeleteOnTermination: true,
				IOPS:                1000,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					VolumeType:          aws.String("io2"),
					VolumeSize:          aws.Int64(8),
					DeleteOnTermination: aws.Bool(true),
					Iops:                aws.Int64(1000),
				},
			},
		},
		{
			Config: &BlockDevice{
				DeviceName:          "/dev/sdb",
				VolumeType:          "gp2",
				VolumeSize:          8,
				DeleteOnTermination: true,
				Encrypted:           config.TriTrue,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					VolumeType:          aws.String("gp2"),
					VolumeSize:          aws.Int64(8),
					DeleteOnTermination: aws.Bool(true),
					Encrypted:           aws.Bool(true),
				},
			},
		},
		{
			Config: &BlockDevice{
				DeviceName:          "/dev/sdb",
				VolumeType:          "gp2",
				VolumeSize:          8,
				DeleteOnTermination: true,
				Encrypted:           config.TriTrue,
				KmsKeyId:            "2Fa48a521f-3aff-4b34-a159-376ac5d37812",
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					VolumeType:          aws.String("gp2"),
					VolumeSize:          aws.Int64(8),
					DeleteOnTermination: aws.Bool(true),
					Encrypted:           aws.Bool(true),
					KmsKeyId:            aws.String("2Fa48a521f-3aff-4b34-a159-376ac5d37812"),
				},
			},
		},
		{
			Config: &BlockDevice{
				DeviceName:          "/dev/sdb",
				VolumeType:          "standard",
				DeleteOnTermination: true,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					VolumeType:          aws.String("standard"),
					DeleteOnTermination: aws.Bool(true),
				},
			},
		},
		{
			Config: &BlockDevice{
				DeviceName:  "/dev/sdb",
				VirtualName: "ephemeral0",
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName:  aws.String("/dev/sdb"),
				VirtualName: aws.String("ephemeral0"),
			},
		},
		{
			Config: &BlockDevice{
				DeviceName: "/dev/sdb",
				NoDevice:   true,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				NoDevice:   aws.String(""),
			},
		},
		{
			Config: &BlockDevice{
				DeviceName:          "/dev/sdb",
				VolumeType:          "gp3",
				VolumeSize:          8,
				Throughput:          125,
				IOPS:                3000,
				DeleteOnTermination: true,
				Encrypted:           config.TriTrue,
			},

			Result: &ec2.BlockDeviceMapping{
				DeviceName: aws.String("/dev/sdb"),
				Ebs: &ec2.EbsBlockDevice{
					VolumeType:          aws.String("gp3"),
					VolumeSize:          aws.Int64(8),
					Throughput:          aws.Int64(125),
					Iops:                aws.Int64(3000),
					DeleteOnTermination: aws.Bool(true),
					Encrypted:           aws.Bool(true),
				},
			},
		},
	}

	for _, tc := range cases {
		var amiBlockDevices BlockDevices = []BlockDevice{*tc.Config}

		var launchBlockDevices BlockDevices = []BlockDevice{*tc.Config}

		expected := []*ec2.BlockDeviceMapping{tc.Result}

		amiResults := amiBlockDevices.BuildEC2BlockDeviceMappings()
		if diff := cmp.Diff(expected, amiResults); diff != "" {
			t.Fatalf("Bad block device: %s", diff)
		}

		launchResults := launchBlockDevices.BuildEC2BlockDeviceMappings()
		if diff := cmp.Diff(expected, launchResults); diff != "" {
			t.Fatalf("Bad block device: %s", diff)
		}
	}
}

func TestIOPSValidation(t *testing.T) {

	cases := []struct {
		device BlockDevice
		ok     bool
		msg    string
	}{
		// volume size unknown
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io1",
				IOPS:       1000,
			},
			ok: true,
		},
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io2",
				IOPS:       1000,
			},
			ok: true,
		},
		// ratio requirement satisfied
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io1",
				VolumeSize: 50,
				IOPS:       1000,
			},
			ok: true,
		},
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io2",
				VolumeSize: 100,
				IOPS:       1000,
			},
			ok: true,
		},
		// ratio requirement not satisfied
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io1",
				VolumeSize: 10,
				IOPS:       2000,
			},
			ok:  false,
			msg: "/dev/sdb: the maximum ratio of provisioned IOPS to requested volume size (in GiB) is 50:1 for io1 volumes",
		},
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io2",
				VolumeSize: 50,
				IOPS:       30000,
			},
			ok:  false,
			msg: "/dev/sdb: the maximum ratio of provisioned IOPS to requested volume size (in GiB) is 500:1 for io2 volumes",
		},
		// exceed max iops
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io2",
				VolumeSize: 500,
				IOPS:       99999,
			},
			ok:  false,
			msg: "IOPS must be between 100 and 64000 for device /dev/sdb",
		},
		// lower than min iops
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "io2",
				VolumeSize: 50,
				IOPS:       10,
			},
			ok:  false,
			msg: "IOPS must be between 100 and 64000 for device /dev/sdb",
		},
		// exceed max iops
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "gp3",
				VolumeSize: 50,
				Throughput: 125,
				IOPS:       99999,
			},
			ok:  false,
			msg: "IOPS must be between 3000 and 16000 for device /dev/sdb",
		},
		// lower than min iops
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "gp3",
				VolumeSize: 50,
				Throughput: 125,
				IOPS:       10,
			},
			ok:  false,
			msg: "IOPS must be between 3000 and 16000 for device /dev/sdb",
		},
	}

	ctx := interpolate.Context{}
	for _, testCase := range cases {
		err := testCase.device.Prepare(&ctx)
		if testCase.ok && err != nil {
			t.Fatalf("should not error, but: %v", err)
		}
		if !testCase.ok {
			if err == nil {
				t.Fatalf("should error")
			} else if err.Error() != testCase.msg {
				t.Fatalf("wrong error: expected %s, found: %v", testCase.msg, err)
			}
		}
	}
}

func TestThroughputValidation(t *testing.T) {

	cases := []struct {
		device BlockDevice
		ok     bool
		msg    string
	}{
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "gp3",
				Throughput: 125,
				IOPS:       3000,
			},
			ok: true,
		},
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "gp3",
				Throughput: 1000,
				IOPS:       3000,
			},
			ok: true,
		},
		// exceed max Throughput
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "gp3",
				Throughput: 1001,
				IOPS:       3000,
			},
			ok:  false,
			msg: "Throughput must be between 125 and 1000 for device /dev/sdb",
		},
		// lower than min Throughput
		{
			device: BlockDevice{
				DeviceName: "/dev/sdb",
				VolumeType: "gp3",
				Throughput: 124,
				IOPS:       3000,
			},
			ok:  false,
			msg: "Throughput must be between 125 and 1000 for device /dev/sdb",
		},
	}

	ctx := interpolate.Context{}
	for _, testCase := range cases {
		err := testCase.device.Prepare(&ctx)
		if testCase.ok && err != nil {
			t.Fatalf("should not error, but: %v", err)
		}
		if !testCase.ok {
			if err == nil {
				t.Fatalf("should error")
			} else if err.Error() != testCase.msg {
				t.Fatalf("wrong error: expected %s, found: %v", testCase.msg, err)
			}
		}
	}
}
