/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package cloudcmd

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"testing"

	"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
	"github.com/edgelesssys/constellation/v2/internal/file"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestTFPlan(t *testing.T) {
	const (
		templateDir       = "templateDir"
		existingWorkspace = "existing"
		backupDir         = "backup"
		testFile          = "testfile"
	)
	fsWithWorkspace := func(require *require.Assertions) file.Handler {
		fs := file.NewHandler(afero.NewMemMapFs())
		require.NoError(fs.Write(filepath.Join(existingWorkspace, testFile), []byte{}, file.OptMkdirAll))
		return fs
	}

	testCases := map[string]struct {
		prepareFs  func(require *require.Assertions) file.Handler
		tf         *stubUpgradePlanner
		wantDiff   bool
		wantBackup bool
		wantErr    bool
	}{
		"success no diff": {
			prepareFs:  fsWithWorkspace,
			tf:         &stubUpgradePlanner{},
			wantBackup: true,
		},
		"success diff": {
			prepareFs: fsWithWorkspace,
			tf: &stubUpgradePlanner{
				planDiff: true,
			},
			wantDiff:   true,
			wantBackup: true,
		},
		"workspace is empty": {
			prepareFs: func(require *require.Assertions) file.Handler {
				return file.NewHandler(afero.NewMemMapFs())
			},
			tf: &stubUpgradePlanner{},
		},
		"backup dir already exists": {
			prepareFs: func(require *require.Assertions) file.Handler {
				fs := fsWithWorkspace(require)
				require.NoError(fs.MkdirAll(backupDir))
				return fs
			},
			tf:      &stubUpgradePlanner{},
			wantErr: true,
		},
		"prepare workspace error": {
			prepareFs: fsWithWorkspace,
			tf: &stubUpgradePlanner{
				prepareWorkspaceErr: assert.AnError,
			},
			wantBackup: true,
			wantErr:    true,
		},
		"plan error": {
			prepareFs: fsWithWorkspace,
			tf: &stubUpgradePlanner{
				planErr: assert.AnError,
			},
			wantErr:    true,
			wantBackup: true,
		},
		"show plan error": {
			prepareFs: fsWithWorkspace,
			tf: &stubUpgradePlanner{
				planDiff:    true,
				showPlanErr: assert.AnError,
			},
			wantErr:    true,
			wantBackup: true,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			assert := assert.New(t)
			fs := tc.prepareFs(require.New(t))

			hasDiff, planErr := plan(
				context.Background(), tc.tf, fs, io.Discard, terraform.LogLevelDebug,
				&terraform.QEMUVariables{},
				templateDir, existingWorkspace, backupDir,
			)

			if tc.wantBackup {
				_, err := fs.Stat(filepath.Join(backupDir, testFile))
				assert.NoError(err)
			}

			if tc.wantErr {
				assert.Error(planErr)
				return
			}
			assert.NoError(planErr)
			assert.Equal(tc.wantDiff, hasDiff)
		})
	}
}

func TestRestoreBackup(t *testing.T) {
	existingWorkspace := "foo"
	backupDir := "bar"
	testFile := "file"

	testCases := map[string]struct {
		prepareFs            func(require *require.Assertions) file.Handler
		wantRemoveWorkingDir bool
		wantErr              bool
	}{
		"success": {
			prepareFs: func(require *require.Assertions) file.Handler {
				fs := file.NewHandler(afero.NewMemMapFs())
				require.NoError(fs.Write(filepath.Join(existingWorkspace, testFile), []byte{}, file.OptMkdirAll))
				require.NoError(fs.Write(filepath.Join(backupDir, testFile), []byte{}, file.OptMkdirAll))
				return fs
			},
		},
		"only backup exists": {
			prepareFs: func(require *require.Assertions) file.Handler {
				fs := file.NewHandler(afero.NewMemMapFs())
				require.NoError(fs.Write(filepath.Join(backupDir, testFile), []byte{}, file.OptMkdirAll))
				return fs
			},
		},
		"only existingWorkspace exists": {
			prepareFs: func(require *require.Assertions) file.Handler {
				fs := file.NewHandler(afero.NewMemMapFs())
				require.NoError(fs.Write(filepath.Join(existingWorkspace, testFile), []byte{}, file.OptMkdirAll))
				return fs
			},
			wantRemoveWorkingDir: true,
		},
		"read only file system": {
			prepareFs: func(require *require.Assertions) file.Handler {
				memFS := afero.NewMemMapFs()
				fs := file.NewHandler(memFS)
				require.NoError(fs.Write(filepath.Join(existingWorkspace, testFile), []byte{}, file.OptMkdirAll))
				require.NoError(fs.Write(filepath.Join(backupDir, testFile), []byte{}, file.OptMkdirAll))
				return file.NewHandler(afero.NewReadOnlyFs(memFS))
			},
			wantErr: true,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			assert := assert.New(t)
			fs := tc.prepareFs(require.New(t))

			err := restoreBackup(fs, existingWorkspace, backupDir)
			if tc.wantErr {
				assert.Error(err)
				return
			}
			assert.NoError(err)
			_, err = fs.Stat(filepath.Join(backupDir, testFile))
			assert.ErrorIs(err, os.ErrNotExist)
			_, err = fs.Stat(filepath.Join(existingWorkspace, testFile))
			if tc.wantRemoveWorkingDir {
				assert.ErrorIs(err, os.ErrNotExist)
			} else {
				assert.NoError(err)
			}
		})
	}
}

func TestEnsureFileNotExist(t *testing.T) {
	testCases := map[string]struct {
		fs       file.Handler
		fileName string
		wantErr  bool
	}{
		"file does not exist": {
			fs:       file.NewHandler(afero.NewMemMapFs()),
			fileName: "foo",
		},
		"file exists": {
			fs: func() file.Handler {
				fs := file.NewHandler(afero.NewMemMapFs())
				err := fs.Write("foo", []byte{})
				require.NoError(t, err)
				return fs
			}(),
			fileName: "foo",
			wantErr:  true,
		},
		"directory exists": {
			fs: func() file.Handler {
				fs := file.NewHandler(afero.NewMemMapFs())
				err := fs.MkdirAll("foo/bar")
				require.NoError(t, err)
				return fs
			}(),
			fileName: "foo/bar",
			wantErr:  true,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			err := ensureFileNotExist(tc.fs, tc.fileName)
			if tc.wantErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

type stubUpgradePlanner struct {
	prepareWorkspaceErr error
	planDiff            bool
	planErr             error
	showPlanErr         error
}

func (s *stubUpgradePlanner) PrepareWorkspace(_ string, _ terraform.Variables) error {
	return s.prepareWorkspaceErr
}

func (s *stubUpgradePlanner) Plan(_ context.Context, _ terraform.LogLevel) (bool, error) {
	return s.planDiff, s.planErr
}

func (s *stubUpgradePlanner) ShowPlan(_ context.Context, _ terraform.LogLevel, _ io.Writer) error {
	return s.showPlanErr
}