/*
Copyright (c) Edgeless Systems GmbH

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

package file

import (
	"encoding/json"
	"io/fs"
	"path/filepath"
	"testing"

	"github.com/edgelesssys/constellation/v2/internal/constants"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/goleak"
	"gopkg.in/yaml.v3"
)

func TestMain(m *testing.M) {
	goleak.VerifyTestMain(m, goleak.IgnoreAnyFunction("github.com/bazelbuild/rules_go/go/tools/bzltestutil.RegisterTimeoutHandler.func1"))
}

func TestWrite(t *testing.T) {
	testCases := map[string]struct {
		fs              afero.Fs
		setupFs         func(af afero.Afero) error
		name            string
		content         string
		expectedContent string
		options         Option
		wantErr         bool
		wantAppend      bool
	}{
		"successful write": {
			fs:              afero.NewMemMapFs(),
			content:         "asdf",
			expectedContent: "asdf",
			name:            "somedir/somefile",
		},
		"successful overwrite": {
			fs:              afero.NewMemMapFs(),
			setupFs:         func(af afero.Afero) error { return af.WriteFile("somedir/somefile", []byte{}, 0o644) },
			content:         "asdf",
			expectedContent: "asdf",
			name:            "somedir/somefile",
			options:         OptOverwrite,
		},
		"successful append": {
			fs:              afero.NewMemMapFs(),
			setupFs:         func(af afero.Afero) error { return af.WriteFile("somedir/somefile", []byte("fdsa"), 0o644) },
			content:         "asdf",
			expectedContent: "fdsaasdf",
			name:            "somedir/somefile",
			options:         OptAppend,
		},
		"read only fs": {
			fs:      afero.NewReadOnlyFs(afero.NewMemMapFs()),
			name:    "somedir/somefile",
			wantErr: true,
		},
		"file already exists": {
			fs:      afero.NewMemMapFs(),
			setupFs: func(af afero.Afero) error { return af.WriteFile("somedir/somefile", []byte{}, 0o644) },
			name:    "somedir/somefile",
			wantErr: true,
		},
	}

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

			handler := NewHandler(tc.fs)
			if tc.setupFs != nil {
				require.NoError(tc.setupFs(afero.Afero{Fs: tc.fs}))
			}

			if tc.wantErr {
				assert.Error(handler.Write(tc.name, []byte(tc.content), tc.options))
			} else {
				assert.NoError(handler.Write(tc.name, []byte(tc.content), tc.options))
				content, err := handler.Read(tc.name)
				require.NoError(err)
				assert.Equal(tc.expectedContent, string(content))
			}
		})
	}
}

func TestReadJSON(t *testing.T) {
	type testContent struct {
		First  string
		Second int
	}
	someContent := testContent{
		First:  "first",
		Second: 2,
	}
	jsonContent, err := json.MarshalIndent(someContent, "", "\t")
	require.NoError(t, err)

	testCases := map[string]struct {
		fs          afero.Fs
		setupFs     func(fs *afero.Afero) error
		name        string
		wantContent any
		wantErr     bool
	}{
		"successful read": {
			fs:          afero.NewMemMapFs(),
			name:        "test/statefile",
			setupFs:     func(fs *afero.Afero) error { return fs.WriteFile("test/statefile", jsonContent, 0o755) },
			wantContent: someContent,
		},
		"file not existent": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			wantErr: true,
		},
		"file not json": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			setupFs: func(fs *afero.Afero) error { return fs.WriteFile("test/statefile", []byte{0x1}, 0o755) },
			wantErr: true,
		},
	}

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

			handler := NewHandler(tc.fs)
			if tc.setupFs != nil {
				require.NoError(tc.setupFs(handler.fs))
			}

			resultContent := &testContent{}
			if tc.wantErr {
				assert.Error(handler.ReadJSON(tc.name, resultContent))
			} else {
				assert.NoError(handler.ReadJSON(tc.name, resultContent))
				assert.Equal(tc.wantContent, *resultContent)
			}
		})
	}
}

func TestWriteJSON(t *testing.T) {
	type testContent struct {
		First  string
		Second int
	}
	someContent := testContent{
		First:  "first",
		Second: 2,
	}
	notMarshalableContent := struct{ Foo chan int }{Foo: make(chan int)}

	testCases := map[string]struct {
		fs      afero.Fs
		setupFs func(af afero.Afero) error
		name    string
		content any
		options Option
		wantErr bool
	}{
		"successful write": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			content: someContent,
		},
		"successful overwrite": {
			fs:      afero.NewMemMapFs(),
			setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
			name:    "test/statefile",
			content: someContent,
			options: OptOverwrite,
		},
		"read only fs": {
			fs:      afero.NewReadOnlyFs(afero.NewMemMapFs()),
			name:    "test/statefile",
			content: someContent,
			wantErr: true,
		},
		"file already exists": {
			fs:      afero.NewMemMapFs(),
			setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
			name:    "test/statefile",
			content: someContent,
			wantErr: true,
		},
		"marshal error": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			content: notMarshalableContent,
			wantErr: true,
		},
		"mkdirAll works": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			content: someContent,
			options: OptMkdirAll,
		},
		// TODO(malt3): add tests for mkdirAll actually creating the necessary folders when https://github.com/spf13/afero/issues/270 is fixed.
		// Currently, MemMapFs will create files in nonexistent directories due to a bug in afero,
		// making it impossible to test the actual behavior of the mkdirAll parameter.
	}

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

			handler := NewHandler(tc.fs)
			if tc.setupFs != nil {
				require.NoError(tc.setupFs(afero.Afero{Fs: tc.fs}))
			}

			if tc.wantErr {
				assert.Error(handler.WriteJSON(tc.name, tc.content, tc.options))
			} else {
				assert.NoError(handler.WriteJSON(tc.name, tc.content, tc.options))
				resultContent := &testContent{}
				assert.NoError(handler.ReadJSON(tc.name, resultContent))
				assert.Equal(tc.content, *resultContent)
			}
		})
	}
}

func TestReadYAML(t *testing.T) {
	type testContent struct {
		First  string
		Second int
	}
	someContent := testContent{
		First:  "first",
		Second: 2,
	}
	yamlContent, err := yaml.Marshal(someContent)
	require.NoError(t, err)

	testCases := map[string]struct {
		fs          afero.Fs
		setupFs     func(fs *afero.Afero) error
		name        string
		wantContent any
		wantErr     bool
	}{
		"successful read": {
			fs:          afero.NewMemMapFs(),
			name:        "test/config.yaml",
			setupFs:     func(fs *afero.Afero) error { return fs.WriteFile("test/config.yaml", yamlContent, 0o755) },
			wantContent: someContent,
		},
		"file not existent": {
			fs:      afero.NewMemMapFs(),
			name:    "test/config.yaml",
			wantErr: true,
		},
		"file not yaml": {
			fs:      afero.NewMemMapFs(),
			name:    "test/config.yaml",
			setupFs: func(fs *afero.Afero) error { return fs.WriteFile("test/config.yaml", []byte{0x1}, 0o755) },
			wantErr: true,
		},
	}

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

			handler := NewHandler(tc.fs)
			if tc.setupFs != nil {
				require.NoError(tc.setupFs(handler.fs))
			}

			resultContent := &testContent{}
			if tc.wantErr {
				assert.Error(handler.ReadYAML(tc.name, resultContent))
			} else {
				assert.NoError(handler.ReadYAML(tc.name, resultContent))
				assert.Equal(tc.wantContent, *resultContent)
			}
		})
	}
}

func TestReadYAMLStrictUnknownFieldFails(t *testing.T) {
	assert := assert.New(t)

	type SampleConfig struct {
		Version string `yaml:"version"`
		Value   string `yaml:"value"`
	}
	yamlConfig := `
	version: "1.0.0"
	value: "foobar"
	sneakyValue: "superSecret"
	`

	handler := NewHandler(afero.NewMemMapFs())
	assert.NoError(handler.Write(constants.ConfigFilename, []byte(yamlConfig), OptNone))

	var readInConfig SampleConfig
	assert.Error(handler.ReadYAMLStrict(constants.ConfigFilename, &readInConfig))
}

func TestWriteYAML(t *testing.T) {
	type testContent struct {
		First  string
		Second int
	}
	someContent := testContent{
		First:  "first",
		Second: 2,
	}
	notMarshalableContent := struct{ Foo chan int }{Foo: make(chan int)}

	testCases := map[string]struct {
		fs      afero.Fs
		setupFs func(af afero.Afero) error
		name    string
		content any
		options Option
		wantErr bool
	}{
		"successful write": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			content: someContent,
		},
		"successful overwrite": {
			fs:      afero.NewMemMapFs(),
			setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
			name:    "test/statefile",
			content: someContent,
			options: OptOverwrite,
		},
		"read only fs": {
			fs:      afero.NewReadOnlyFs(afero.NewMemMapFs()),
			name:    "test/statefile",
			content: someContent,
			wantErr: true,
		},
		"file already exists": {
			fs:      afero.NewMemMapFs(),
			setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
			name:    "test/statefile",
			content: someContent,
			wantErr: true,
		},
		"marshal error": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			content: notMarshalableContent,
			wantErr: true,
		},
		"mkdirAll works": {
			fs:      afero.NewMemMapFs(),
			name:    "test/statefile",
			content: someContent,
			options: OptMkdirAll,
		},
		// TODO(malt3): add tests for mkdirAll actually creating the necessary folders when https://github.com/spf13/afero/issues/270 is fixed.
		// Currently, MemMapFs will create files in nonexistent directories due to a bug in afero,
		// making it impossible to test the actual behavior of the mkdirAll parameter.
	}

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

			handler := NewHandler(tc.fs)
			if tc.setupFs != nil {
				require.NoError(tc.setupFs(afero.Afero{Fs: tc.fs}))
			}

			if tc.wantErr {
				assert.Error(handler.WriteYAML(tc.name, tc.content, tc.options))
			} else {
				assert.NoError(handler.WriteYAML(tc.name, tc.content, tc.options))
				resultContent := &testContent{}
				assert.NoError(handler.ReadYAML(tc.name, resultContent))
				assert.Equal(tc.content, *resultContent)
			}
		})
	}
}

func TestRemove(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	fs := afero.NewMemMapFs()
	handler := NewHandler(fs)
	aferoHelper := afero.Afero{Fs: fs}
	require.NoError(aferoHelper.WriteFile("a", []byte{0xa}, 0o644))
	require.NoError(aferoHelper.WriteFile("b", []byte{0xb}, 0o644))
	require.NoError(aferoHelper.WriteFile("c", []byte{0xc}, 0o644))

	assert.NoError(handler.Remove("a"))
	assert.NoError(handler.Remove("b"))
	assert.NoError(handler.Remove("c"))

	_, err := handler.fs.Stat("a")
	assert.ErrorIs(err, afero.ErrFileNotFound)
	_, err = handler.fs.Stat("b")
	assert.ErrorIs(err, afero.ErrFileNotFound)
	_, err = handler.fs.Stat("c")
	assert.ErrorIs(err, afero.ErrFileNotFound)

	assert.Error(handler.Remove("d"))
}

func TestCopyFile(t *testing.T) {
	perms := fs.FileMode(0o644)

	setupFs := func(existingFiles ...string) afero.Fs {
		fs := afero.NewMemMapFs()
		aferoHelper := afero.Afero{Fs: fs}
		for _, file := range existingFiles {
			require.NoError(t, aferoHelper.WriteFile(file, []byte{}, perms))
		}
		return fs
	}

	testCases := map[string]struct {
		fs         afero.Fs
		copyFiles  [][]string
		checkFiles []string
		opts       []Option
		wantErr    bool
	}{
		"successful copy": {
			fs:         setupFs("a"),
			copyFiles:  [][]string{{"a", "b"}},
			checkFiles: []string{"b"},
		},
		"copy to existing file overwrite": {
			fs:         setupFs("a", "b"),
			copyFiles:  [][]string{{"a", "b"}},
			checkFiles: []string{"b"},
			opts:       []Option{OptOverwrite},
		},
		"copy to existing file no overwrite": {
			fs:         setupFs("a", "b"),
			copyFiles:  [][]string{{"a", "b"}},
			checkFiles: []string{"b"},
			wantErr:    true,
		},
		"file doesn't exist": {
			fs:         setupFs("a"),
			copyFiles:  [][]string{{"b", "c"}},
			checkFiles: []string{"a"},
			wantErr:    true,
		},
	}

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

			handler := NewHandler(tc.fs)
			for _, files := range tc.copyFiles {
				err := handler.CopyFile(files[0], files[1], tc.opts...)
				if tc.wantErr {
					assert.Error(err)
				} else {
					assert.NoError(err)
				}
			}

			for _, file := range tc.checkFiles {
				info, err := handler.fs.Stat(file)
				assert.Equal(perms, info.Mode())
				require.NoError(err)
			}
		})
	}
}

func TestCopyDir(t *testing.T) {
	setupHandler := func(existingFiles ...string) Handler {
		fs := afero.NewMemMapFs()
		handler := NewHandler(fs)
		for _, file := range existingFiles {
			err := handler.Write(file, []byte("some content"), OptMkdirAll)
			require.NoError(t, err)
		}
		return handler
	}

	testCases := map[string]struct {
		handler    Handler
		copyFiles  [][]string
		checkFiles []string
		opts       []Option
	}{
		"successful copy": {
			handler:    setupHandler(filepath.Join("someDir", "someFile"), filepath.Join("someDir", "someOtherDir", "someOtherFile")),
			copyFiles:  [][]string{{"someDir", "copiedDir"}},
			checkFiles: []string{filepath.Join("copiedDir", "someFile"), filepath.Join("copiedDir", "someOtherDir", "someOtherFile")},
		},
		"copy file": {
			handler:    setupHandler("someFile"),
			copyFiles:  [][]string{{"someFile", "copiedFile"}},
			checkFiles: []string{"copiedFile"},
		},
		"copy to existing dir overwrite": {
			handler:    setupHandler(filepath.Join("someDir", "someFile"), filepath.Join("someDir", "someOtherDir", "someOtherFile"), filepath.Join("copiedDir", "someExistingFile")),
			copyFiles:  [][]string{{"someDir", "copiedDir"}},
			checkFiles: []string{filepath.Join("copiedDir", "someFile"), filepath.Join("copiedDir", "someOtherDir", "someOtherFile"), filepath.Join("copiedDir", "someExistingFile")},
			opts:       []Option{OptOverwrite},
		},
	}

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

			for _, files := range tc.copyFiles {
				err := tc.handler.CopyDir(files[0], files[1], tc.opts...)
				require.NoError(err)
			}

			for _, file := range tc.checkFiles {
				_, err := tc.handler.fs.Stat(file)
				require.NoError(err)
			}
		})
	}
}

func TestRename(t *testing.T) {
	setupHandler := func(existingFiles ...string) Handler {
		fs := afero.NewMemMapFs()
		handler := NewHandler(fs)
		for _, file := range existingFiles {
			err := handler.Write(file, []byte("some content"), OptMkdirAll)
			require.NoError(t, err)
		}
		return handler
	}

	testCases := map[string]struct {
		handler    Handler
		renames    map[string]string
		checkFiles []string
		wantErr    bool
	}{
		"successful rename": {
			handler:    setupHandler("someFile"),
			renames:    map[string]string{"someFile": "someOtherFile"},
			checkFiles: []string{"someOtherFile"},
		},
		"rename to existing file, overwrite": {
			handler:    setupHandler("someFile", "someOtherFile"),
			renames:    map[string]string{"someFile": "someOtherFile"},
			checkFiles: []string{"someOtherFile"},
		},
		"file does not exist": {
			handler: setupHandler(),
			renames: map[string]string{"someFile": "someOtherFile"},
			wantErr: true,
		},
	}

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

			for old, new := range tc.renames {
				err := tc.handler.RenameFile(old, new)
				if tc.wantErr {
					require.Error(err)
				} else {
					require.NoError(err)
				}
			}

			for _, file := range tc.checkFiles {
				_, err := tc.handler.fs.Stat(file)
				require.NoError(err)
			}
		})
	}
}

func TestIsEmpty(t *testing.T) {
	testCases := map[string]struct {
		setupFs     func(fs *afero.Afero, dirName string) error
		wantIsEmpty bool
		wantErr     bool
	}{
		"empty directory": {
			setupFs:     func(fs *afero.Afero, dirName string) error { return fs.Mkdir(dirName, 0o755) },
			wantIsEmpty: true,
		},
		"directory not empty": {
			setupFs: func(fs *afero.Afero, dirName string) error {
				return fs.WriteFile(filepath.Join(dirName, "file"), []byte("some content"), 0o755)
			},
		},
		"directory not existent": {
			wantErr: true,
		},
	}

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

			handler := NewHandler(afero.NewMemMapFs())
			if tc.setupFs != nil {
				require.NoError(tc.setupFs(handler.fs, dirName))
			}

			isEmpty, err := handler.IsEmpty(dirName)
			if tc.wantErr {
				assert.Error(err)
			} else {
				assert.NoError(err)
				assert.Equal(tc.wantIsEmpty, isEmpty)
			}
		})
	}
}