package common

import (
	"context"
	"fmt"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/hashicorp/packer/builder/vsphere/driver"
	"github.com/hashicorp/packer/packer-plugin-sdk/multistep"
)

func TestStepAddFloppy_Run(t *testing.T) {
	tc := []struct {
		name               string
		floppyPath         string
		uploadedPath       string
		step               *StepAddFloppy
		expectedAction     multistep.StepAction
		vmMock             *driver.VirtualMachineMock
		expectedVmMock     *driver.VirtualMachineMock
		driverMock         *driver.DriverMock
		expectedDriverMock *driver.DriverMock
		dsMock             *driver.DatastoreMock
		expectedDsMock     *driver.DatastoreMock
		fail               bool
		errMessage         string
	}{
		{
			name:         "Add floppy from state floppy path",
			floppyPath:   "floppy/path",
			uploadedPath: "vm/dir/packer-tmp-created-floppy.flp",
			step: &StepAddFloppy{
				Config:                     new(FloppyConfig),
				Datastore:                  "datastore",
				Host:                       "host",
				SetHostForDatastoreUploads: true,
			},
			expectedAction: multistep.ActionContinue,
			vmMock: &driver.VirtualMachineMock{
				GetDirResponse: "vm/dir",
			},
			expectedVmMock: &driver.VirtualMachineMock{
				GetDirResponse:     "vm/dir",
				GetDirCalled:       true,
				AddFloppyCalled:    true,
				AddFloppyImagePath: "resolved/path",
			},
			driverMock: new(driver.DriverMock),
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock: &driver.DatastoreMock{
				ResolvePathReturn: "resolved/path",
			},
			expectedDsMock: &driver.DatastoreMock{
				UploadFileCalled:  true,
				UploadFileSrc:     "floppy/path",
				UploadFileDst:     "vm/dir/packer-tmp-created-floppy.flp",
				UploadFileHost:    "host",
				UploadFileSetHost: true,
				ResolvePathCalled: true,
				ResolvePathReturn: "resolved/path",
			},
			fail: false,
		},
		{
			name:       "State floppy path - find datastore fail",
			floppyPath: "floppy/path",
			step: &StepAddFloppy{
				Config:                     new(FloppyConfig),
				Datastore:                  "datastore",
				Host:                       "host",
				SetHostForDatastoreUploads: true,
			},
			expectedAction: multistep.ActionHalt,
			vmMock:         new(driver.VirtualMachineMock),
			expectedVmMock: new(driver.VirtualMachineMock),
			driverMock: &driver.DriverMock{
				FindDatastoreErr: fmt.Errorf("error finding datastore"),
			},
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock:         new(driver.DatastoreMock),
			expectedDsMock: new(driver.DatastoreMock),
			fail:           true,
			errMessage:     "error finding datastore",
		},
		{
			name:       "State floppy path - vm get dir fail",
			floppyPath: "floppy/path",
			step: &StepAddFloppy{
				Config:                     new(FloppyConfig),
				Datastore:                  "datastore",
				Host:                       "host",
				SetHostForDatastoreUploads: true,
			},
			expectedAction: multistep.ActionHalt,
			vmMock: &driver.VirtualMachineMock{
				GetDirErr: fmt.Errorf("fail to get vm dir"),
			},
			expectedVmMock: &driver.VirtualMachineMock{
				GetDirCalled: true,
			},
			driverMock: new(driver.DriverMock),
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock:         new(driver.DatastoreMock),
			expectedDsMock: new(driver.DatastoreMock),
			fail:           true,
			errMessage:     "fail to get vm dir",
		},
		{
			name:       "State floppy path - datastore upload file fail",
			floppyPath: "floppy/path",
			step: &StepAddFloppy{
				Config:                     new(FloppyConfig),
				Datastore:                  "datastore",
				Host:                       "host",
				SetHostForDatastoreUploads: true,
			},
			expectedAction: multistep.ActionHalt,
			vmMock: &driver.VirtualMachineMock{
				GetDirResponse: "vm/dir",
			},
			expectedVmMock: &driver.VirtualMachineMock{
				GetDirResponse: "vm/dir",
				GetDirCalled:   true,
			},
			driverMock: new(driver.DriverMock),
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock: &driver.DatastoreMock{
				UploadFileErr: fmt.Errorf("failed to upload file"),
			},
			expectedDsMock: &driver.DatastoreMock{
				UploadFileCalled:  true,
				UploadFileSrc:     "floppy/path",
				UploadFileDst:     "vm/dir/packer-tmp-created-floppy.flp",
				UploadFileHost:    "host",
				UploadFileSetHost: true,
			},
			fail:       true,
			errMessage: "failed to upload file",
		},
		{
			name:         "State floppy path - vm fail to add floppy",
			floppyPath:   "floppy/path",
			uploadedPath: "vm/dir/packer-tmp-created-floppy.flp",
			step: &StepAddFloppy{
				Config:                     new(FloppyConfig),
				Datastore:                  "datastore",
				Host:                       "host",
				SetHostForDatastoreUploads: true,
			},
			expectedAction: multistep.ActionHalt,
			vmMock: &driver.VirtualMachineMock{
				GetDirResponse: "vm/dir",
				AddFloppyErr:   fmt.Errorf("failed to add floppy"),
			},
			expectedVmMock: &driver.VirtualMachineMock{
				GetDirResponse:     "vm/dir",
				GetDirCalled:       true,
				AddFloppyCalled:    true,
				AddFloppyImagePath: "resolved/path",
			},
			driverMock: new(driver.DriverMock),
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock: &driver.DatastoreMock{
				ResolvePathReturn: "resolved/path",
			},
			expectedDsMock: &driver.DatastoreMock{
				UploadFileCalled:  true,
				UploadFileSrc:     "floppy/path",
				UploadFileDst:     "vm/dir/packer-tmp-created-floppy.flp",
				UploadFileHost:    "host",
				UploadFileSetHost: true,
				ResolvePathCalled: true,
				ResolvePathReturn: "resolved/path",
			},
			fail:       true,
			errMessage: "failed to add floppy",
		},
		{
			name: "Add floppy from FloppyIMGPath config",
			step: &StepAddFloppy{
				Config: &FloppyConfig{
					FloppyIMGPath: "floppy/image/path",
				},
			},
			expectedAction: multistep.ActionContinue,
			vmMock:         new(driver.VirtualMachineMock),
			expectedVmMock: &driver.VirtualMachineMock{
				AddFloppyCalled:    true,
				AddFloppyImagePath: "floppy/image/path",
			},
			driverMock:         new(driver.DriverMock),
			expectedDriverMock: new(driver.DriverMock),
			dsMock:             new(driver.DatastoreMock),
			expectedDsMock:     new(driver.DatastoreMock),
			fail:               false,
		},
		{
			name: "Fail to add floppy from FloppyIMGPath config",
			step: &StepAddFloppy{
				Config: &FloppyConfig{
					FloppyIMGPath: "floppy/image/path",
				},
			},
			expectedAction: multistep.ActionHalt,
			vmMock: &driver.VirtualMachineMock{
				AddFloppyErr: fmt.Errorf("fail to add floppy"),
			},
			expectedVmMock: &driver.VirtualMachineMock{
				AddFloppyCalled:    true,
				AddFloppyImagePath: "floppy/image/path",
			},
			driverMock:         new(driver.DriverMock),
			expectedDriverMock: new(driver.DriverMock),
			dsMock:             new(driver.DatastoreMock),
			expectedDsMock:     new(driver.DatastoreMock),
			fail:               true,
			errMessage:         "fail to add floppy",
		},
	}

	for _, c := range tc {
		t.Run(c.name, func(t *testing.T) {
			state := basicStateBag(nil)
			state.Put("vm", c.vmMock)
			c.driverMock.DatastoreMock = c.dsMock
			state.Put("driver", c.driverMock)

			if c.floppyPath != "" {
				state.Put("floppy_path", c.floppyPath)
			}

			if action := c.step.Run(context.TODO(), state); action != c.expectedAction {
				t.Fatalf("unexpected action %v", action)
			}
			err, ok := state.Get("error").(error)
			if ok {
				if err.Error() != c.errMessage {
					t.Fatalf("unexpected error %s", err.Error())
				}
			} else {
				if c.fail {
					t.Fatalf("expected to fail but it didn't")
				}
			}

			uploadedPath, _ := state.Get("uploaded_floppy_path").(string)
			if uploadedPath != c.uploadedPath {
				t.Fatalf("Unexpected uploaded path state %s", uploadedPath)
			}

			if diff := cmp.Diff(c.vmMock, c.expectedVmMock,
				cmpopts.IgnoreInterfaces(struct{ error }{})); diff != "" {
				t.Fatalf("unexpected VirtualMachine calls: %s", diff)
			}
			c.expectedDriverMock.DatastoreMock = c.expectedDsMock
			if diff := cmp.Diff(c.driverMock, c.expectedDriverMock,
				cmpopts.IgnoreInterfaces(struct{ error }{})); diff != "" {
				t.Fatalf("unexpected Driver calls: %s", diff)
			}
			if diff := cmp.Diff(c.dsMock, c.expectedDsMock,
				cmpopts.IgnoreInterfaces(struct{ error }{})); diff != "" {
				t.Fatalf("unexpected Datastore calls: %s", diff)
			}
		})
	}
}

func TestStepAddFloppy_Cleanup(t *testing.T) {
	tc := []struct {
		name               string
		uploadedPath       string
		multistepState     string
		step               *StepAddFloppy
		driverMock         *driver.DriverMock
		expectedDriverMock *driver.DriverMock
		dsMock             *driver.DatastoreMock
		expectedDsMock     *driver.DatastoreMock
		fail               bool
		errMessage         string
	}{
		{
			name:           "State cancelled clean up",
			uploadedPath:   "uploaded/path",
			multistepState: multistep.StateCancelled,
			step: &StepAddFloppy{
				Datastore: "datastore",
				Host:      "host",
			},
			driverMock: new(driver.DriverMock),
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock: &driver.DatastoreMock{
				DeleteCalled: true,
			},
			expectedDsMock: &driver.DatastoreMock{
				DeleteCalled: true,
				DeletePath:   "uploaded/path",
			},
			fail: false,
		},
		{
			name:           "State halted clean up",
			uploadedPath:   "uploaded/path",
			multistepState: multistep.StateHalted,
			step: &StepAddFloppy{
				Datastore: "datastore",
				Host:      "host",
			},
			driverMock: new(driver.DriverMock),
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock: &driver.DatastoreMock{
				DeleteCalled: true,
			},
			expectedDsMock: &driver.DatastoreMock{
				DeleteCalled: true,
				DeletePath:   "uploaded/path",
			},
			fail: false,
		},
		{
			name:               "Don't clean up without uploaded path",
			multistepState:     multistep.StateHalted,
			step:               new(StepAddFloppy),
			driverMock:         new(driver.DriverMock),
			expectedDriverMock: new(driver.DriverMock),
			dsMock:             new(driver.DatastoreMock),
			expectedDsMock:     new(driver.DatastoreMock),
			fail:               false,
		},
		{
			name:               "Don't clean up if state is not halted or canceled",
			multistepState:     "",
			step:               new(StepAddFloppy),
			driverMock:         new(driver.DriverMock),
			expectedDriverMock: new(driver.DriverMock),
			dsMock:             new(driver.DatastoreMock),
			expectedDsMock:     new(driver.DatastoreMock),
			fail:               false,
		},
		{
			name:           "Fail because datastore is not found",
			uploadedPath:   "uploaded/path",
			multistepState: multistep.StateHalted,
			step: &StepAddFloppy{
				Datastore: "datastore",
				Host:      "host",
			},
			driverMock: &driver.DriverMock{
				FindDatastoreErr: fmt.Errorf("fail to find datastore"),
			},
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock:         new(driver.DatastoreMock),
			expectedDsMock: new(driver.DatastoreMock),
			fail:           true,
			errMessage:     "fail to find datastore",
		},
		{
			name:           "Fail to delete floppy",
			uploadedPath:   "uploaded/path",
			multistepState: multistep.StateHalted,
			step: &StepAddFloppy{
				Datastore: "datastore",
				Host:      "host",
			},
			driverMock: new(driver.DriverMock),
			expectedDriverMock: &driver.DriverMock{
				FindDatastoreCalled: true,
				FindDatastoreName:   "datastore",
				FindDatastoreHost:   "host",
			},
			dsMock: &driver.DatastoreMock{
				DeleteCalled: true,
				DeleteErr:    fmt.Errorf("failed to delete floppy"),
			},
			expectedDsMock: &driver.DatastoreMock{
				DeleteCalled: true,
				DeletePath:   "uploaded/path",
			},
			fail:       true,
			errMessage: "failed to delete floppy",
		},
	}

	for _, c := range tc {
		t.Run(c.name, func(t *testing.T) {
			state := basicStateBag(nil)
			c.driverMock.DatastoreMock = c.dsMock
			state.Put("driver", c.driverMock)
			if c.uploadedPath != "" {
				state.Put("uploaded_floppy_path", c.uploadedPath)
			}

			if c.multistepState != "" {
				state.Put(c.multistepState, true)
			}

			c.step.Cleanup(state)
			err, ok := state.Get("error").(error)
			if ok {
				if err.Error() != c.errMessage {
					t.Fatalf("unexpected error %s", err.Error())
				}
			} else {
				if c.fail {
					t.Fatalf("expected to fail but it didn't")
				}
			}

			c.expectedDriverMock.DatastoreMock = c.expectedDsMock
			if diff := cmp.Diff(c.driverMock, c.expectedDriverMock,
				cmpopts.IgnoreInterfaces(struct{ error }{})); diff != "" {
				t.Fatalf("unexpected Driver calls: %s", diff)
			}
			if diff := cmp.Diff(c.dsMock, c.expectedDsMock,
				cmpopts.IgnoreInterfaces(struct{ error }{})); diff != "" {
				t.Fatalf("unexpected Datastore calls: %s", diff)
			}
		})
	}
}
