/*
Copyright 2018 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package files

import (
	"fmt"
	"os"
	"path/filepath"
	"testing"

	utiltest "k8s.io/kubernetes/pkg/kubelet/kubeletconfig/util/test"
	utilfs "k8s.io/kubernetes/pkg/util/filesystem"
)

const (
	prefix = "test-util-files"
)

type file struct {
	name string
	// mode distinguishes file type,
	// we only check for regular vs. directory in these tests,
	// specify regular as 0, directory as os.ModeDir
	mode os.FileMode
	data string // ignored if mode == os.ModeDir
}

func (f *file) write(fs utilfs.Filesystem, dir string) error {
	path := filepath.Join(dir, f.name)
	if f.mode.IsDir() {
		if err := fs.MkdirAll(path, defaultPerm); err != nil {
			return err
		}
	} else if f.mode.IsRegular() {
		// create parent directories, if necessary
		parents := filepath.Dir(path)
		if err := fs.MkdirAll(parents, defaultPerm); err != nil {
			return err
		}
		// create the file
		handle, err := fs.Create(path)
		if err != nil {
			return err
		}
		_, err = handle.Write([]byte(f.data))
		if err != nil {
			if cerr := handle.Close(); cerr != nil {
				return fmt.Errorf("error %v closing file after error: %v", cerr, err)
			}
			return err
		}
	} else {
		return fmt.Errorf("mode not implemented for testing %s", f.mode.String())
	}
	return nil
}

func (f *file) expect(fs utilfs.Filesystem, dir string) error {
	path := filepath.Join(dir, f.name)
	if f.mode.IsDir() {
		info, err := fs.Stat(path)
		if err != nil {
			return err
		}
		if !info.IsDir() {
			return fmt.Errorf("expected directory, got mode %s", info.Mode().String())
		}
	} else if f.mode.IsRegular() {
		info, err := fs.Stat(path)
		if err != nil {
			return err
		}
		if !info.Mode().IsRegular() {
			return fmt.Errorf("expected regular file, got mode %s", info.Mode().String())
		}
		data, err := fs.ReadFile(path)
		if err != nil {
			return err
		}
		if f.data != string(data) {
			return fmt.Errorf("expected file data %q, got %q", f.data, string(data))
		}
	} else {
		return fmt.Errorf("mode not implemented for testing %s", f.mode.String())
	}
	return nil
}

// write files, perform some function, then attempt to read files back
// if err is non-empty, expects an error from the function performed in the test
// and skips reading back the expected files
type test struct {
	desc    string
	writes  []file
	expects []file
	fn      func(fs utilfs.Filesystem, dir string, c *test) []error
	err     string
}

func (c *test) write(t *testing.T, fs utilfs.Filesystem, dir string) {
	for _, f := range c.writes {
		if err := f.write(fs, dir); err != nil {
			t.Fatalf("error pre-writing file: %v", err)
		}
	}
}

// you can optionally skip calling t.Errorf by passing a nil t, and process the
// returned errors instead
func (c *test) expect(t *testing.T, fs utilfs.Filesystem, dir string) []error {
	errs := []error{}
	for _, f := range c.expects {
		if err := f.expect(fs, dir); err != nil {
			msg := fmt.Errorf("expect %#v, got error: %v", f, err)
			errs = append(errs, msg)
			if t != nil {
				t.Errorf("%s", msg)
			}
		}
	}
	return errs
}

// run a test case, with an arbitrary function to execute between write and expect
// if c.fn is nil, errors from c.expect are checked against c.err, instead of errors
// from fn being checked against c.err
func (c *test) run(t *testing.T, fs utilfs.Filesystem) {
	// isolate each test case in a new temporary directory
	dir, err := fs.TempDir("", prefix)
	if err != nil {
		t.Fatalf("error creating temporary directory for test: %v", err)
	}
	defer os.RemoveAll(dir)
	c.write(t, fs, dir)
	// if fn exists, check errors from fn, then check expected files
	if c.fn != nil {
		errs := c.fn(fs, dir, c)
		if len(errs) > 0 {
			for _, err := range errs {
				utiltest.ExpectError(t, err, c.err)
			}
			// skip checking expected files if we expected errors
			// (usually means we didn't create file)
			return
		}
		c.expect(t, fs, dir)
		return
	}
	// just check expected files, and compare errors from c.expect to c.err
	// (this lets us test the helper functions above)
	errs := c.expect(nil, fs, dir)
	for _, err := range errs {
		utiltest.ExpectError(t, err, c.err)
	}
}

// simple test of the above helper functions
func TestHelpers(t *testing.T) {
	// omitting the test.fn means test.err is compared to errors from test.expect
	cases := []test{
		{
			desc:    "regular file",
			writes:  []file{{name: "foo", data: "bar"}},
			expects: []file{{name: "foo", data: "bar"}},
		},
		{
			desc:    "directory",
			writes:  []file{{name: "foo", mode: os.ModeDir}},
			expects: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			desc:    "deep regular file",
			writes:  []file{{name: "foo/bar", data: "baz"}},
			expects: []file{{name: "foo/bar", data: "baz"}},
		},
		{
			desc:    "deep directory",
			writes:  []file{{name: "foo/bar", mode: os.ModeDir}},
			expects: []file{{name: "foo/bar", mode: os.ModeDir}},
		},
		{
			desc:    "missing file",
			expects: []file{{name: "foo", data: "bar"}},
			err:     "no such file or directory",
		},
		{
			desc:    "missing directory",
			expects: []file{{name: "foo/bar", mode: os.ModeDir}},
			err:     "no such file or directory",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestFileExists(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		ok, err := FileExists(fs, filepath.Join(dir, "foo"))
		if err != nil {
			return []error{err}
		}
		if !ok {
			return []error{fmt.Errorf("does not exist (test)")}
		}
		return nil
	}
	cases := []test{
		{
			fn:     fn,
			desc:   "file exists",
			writes: []file{{name: "foo"}},
		},
		{
			fn:   fn,
			desc: "file does not exist",
			err:  "does not exist (test)",
		},
		{
			fn:     fn,
			desc:   "object has non-file mode",
			writes: []file{{name: "foo", mode: os.ModeDir}},
			err:    "expected regular file",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestEnsureFile(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		var errs []error
		for _, f := range c.expects {
			if err := EnsureFile(fs, filepath.Join(dir, f.name)); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "file exists",
			writes:  []file{{name: "foo"}},
			expects: []file{{name: "foo"}},
		},
		{
			fn:      fn,
			desc:    "file does not exist",
			expects: []file{{name: "bar"}},
		},
		{
			fn:      fn,
			desc:    "neither parent nor file exists",
			expects: []file{{name: "baz/quux"}},
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

// Note: This transitively tests WriteTmpFile
func TestReplaceFile(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		var errs []error
		for _, f := range c.expects {
			if err := ReplaceFile(fs, filepath.Join(dir, f.name), []byte(f.data)); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "file exists",
			writes:  []file{{name: "foo"}},
			expects: []file{{name: "foo", data: "bar"}},
		},
		{
			fn:      fn,
			desc:    "file does not exist",
			expects: []file{{name: "foo", data: "bar"}},
		},
		{
			fn: func(fs utilfs.Filesystem, dir string, c *test) []error {
				if err := ReplaceFile(fs, filepath.Join(dir, "foo/bar"), []byte("")); err != nil {
					return []error{err}
				}
				return nil
			},
			desc: "neither parent nor file exists",
			err:  "no such file or directory",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestDirExists(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		ok, err := DirExists(fs, filepath.Join(dir, "foo"))
		if err != nil {
			return []error{err}
		}
		if !ok {
			return []error{fmt.Errorf("does not exist (test)")}
		}
		return nil
	}
	cases := []test{
		{
			fn:     fn,
			desc:   "dir exists",
			writes: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			fn:   fn,
			desc: "dir does not exist",
			err:  "does not exist (test)",
		},
		{
			fn:     fn,
			desc:   "object has non-dir mode",
			writes: []file{{name: "foo"}},
			err:    "expected dir",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestEnsureDir(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		var errs []error
		for _, f := range c.expects {
			if err := EnsureDir(fs, filepath.Join(dir, f.name)); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "dir exists",
			writes:  []file{{name: "foo", mode: os.ModeDir}},
			expects: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			fn:      fn,
			desc:    "dir does not exist",
			expects: []file{{name: "bar", mode: os.ModeDir}},
		},
		{
			fn:      fn,
			desc:    "neither parent nor dir exists",
			expects: []file{{name: "baz/quux", mode: os.ModeDir}},
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestWriteTempDir(t *testing.T) {
	// writing a tmp dir is covered by TestReplaceDir, but we additionally test filename validation here
	c := test{
		desc: "invalid file key",
		err:  "invalid file key",
		fn: func(fs utilfs.Filesystem, dir string, c *test) []error {
			if _, err := WriteTempDir(fs, filepath.Join(dir, "tmpdir"), map[string]string{"foo/bar": ""}); err != nil {
				return []error{err}
			}
			return nil
		},
	}
	c.run(t, utilfs.DefaultFs{})
}

func TestReplaceDir(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		errs := []error{}

		// compute filesets from expected files and call ReplaceDir for each
		// we don't nest dirs in test cases, order of ReplaceDir call is not guaranteed
		dirs := map[string]map[string]string{}

		// allocate dirs
		for _, f := range c.expects {
			if f.mode.IsDir() {
				path := filepath.Join(dir, f.name)
				if _, ok := dirs[path]; !ok {
					dirs[path] = map[string]string{}
				}
			} else if f.mode.IsRegular() {
				path := filepath.Join(dir, filepath.Dir(f.name))
				if _, ok := dirs[path]; !ok {
					// require an expectation for the parent directory if there is an expectation for the file
					errs = append(errs, fmt.Errorf("no prior parent directory in c.expects for file %s", f.name))
					continue
				}
				dirs[path][filepath.Base(f.name)] = f.data
			}
		}

		// short-circuit test case validation errors
		if len(errs) > 0 {
			return errs
		}

		// call ReplaceDir for each desired dir
		for path, files := range dirs {
			if err := ReplaceDir(fs, path, files); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "fn catches invalid test case",
			expects: []file{{name: "foo/bar"}},
			err:     "no prior parent directory",
		},
		{
			fn:      fn,
			desc:    "empty dir",
			expects: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			fn:   fn,
			desc: "dir with files",
			expects: []file{
				{name: "foo", mode: os.ModeDir},
				{name: "foo/bar", data: "baz"},
				{name: "foo/baz", data: "bar"},
			},
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}
