/*
Copyright 2020 Google LLC

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 filesystem

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"testing"

	"github.com/GoogleContainerTools/kaniko/pkg/util"
)

func Test_ResolvePaths(t *testing.T) {
	validateResults := func(
		t *testing.T,
		actualFiles,
		expectedFiles []string,
		err error,
	) {
		if err != nil {
			t.Errorf("expected err to be nil but was %s", err)
		}

		// Sort so that comparison is against consistent order
		sort.Strings(actualFiles)
		sort.Strings(expectedFiles)

		if !reflect.DeepEqual(actualFiles, expectedFiles) {
			t.Errorf("expected files to equal %s but was %s",
				expectedFiles, actualFiles,
			)
		}
	}

	t.Run("list of files", func(t *testing.T) {
		dir, err := ioutil.TempDir("", "snapshot-test")
		if err != nil {
			t.Fatal(err)
		}

		defer os.RemoveAll(dir)

		files := []string{
			"/foo/bar.txt",
			"/baz/boom.txt",
		}

		t.Run("all are symlinks", func(t *testing.T) {
			for _, f := range files {
				fLink := filepath.Join(dir, "link", f)
				fTarget := filepath.Join(dir, "target", f)

				if err := os.MkdirAll(filepath.Dir(fTarget), 0777); err != nil {
					t.Fatal(err)
				}

				if err := ioutil.WriteFile(fTarget, []byte{}, 0777); err != nil {
					t.Fatal(err)
				}

				if err := os.MkdirAll(filepath.Dir(fLink), 0777); err != nil {
					t.Fatal(err)
				}

				if err := os.Symlink(fTarget, fLink); err != nil {
					t.Fatal(err)
				}
			}

			t.Run("none are ignored", func(t *testing.T) {
				wl := []util.IgnoreListEntry{}

				inputFiles := []string{}
				expectedFiles := []string{}

				for _, f := range files {
					link := filepath.Join(dir, "link", f)
					expectedFiles = append(expectedFiles, link)
					inputFiles = append(inputFiles, link)

					target := filepath.Join(dir, "target", f)
					expectedFiles = append(expectedFiles, target)
				}

				expectedFiles = filesWithParentDirs(expectedFiles)

				files, err := ResolvePaths(inputFiles, wl)

				validateResults(t, files, expectedFiles, err)
			})

			t.Run("some are ignored", func(t *testing.T) {
				wl := []util.IgnoreListEntry{
					{
						Path: filepath.Join(dir, "link", "baz"),
					},
					{
						Path: filepath.Join(dir, "target", "foo"),
					},
				}

				expectedFiles := []string{}
				inputFiles := []string{}

				for _, f := range files {
					link := filepath.Join(dir, "link", f)
					inputFiles = append(inputFiles, link)

					if util.IsInProvidedIgnoreList(link, wl) {
						t.Logf("skipping %s", link)
						continue
					}

					expectedFiles = append(expectedFiles, link)

					target := filepath.Join(dir, "target", f)

					if util.IsInProvidedIgnoreList(target, wl) {
						t.Logf("skipping %s", target)
						continue
					}

					expectedFiles = append(expectedFiles, target)
				}

				link := filepath.Join(dir, "link", "zoom/")

				target := filepath.Join(dir, "target", "zaam/")
				if err := os.MkdirAll(target, 0777); err != nil {
					t.Fatal(err)
				}

				if err := ioutil.WriteFile(filepath.Join(target, "meow.txt"), []byte{}, 0777); err != nil {
					t.Fatal(err)
				}

				if err := os.Symlink(target, link); err != nil {
					t.Fatal(err)
				}

				file := filepath.Join(link, "meow.txt")
				inputFiles = append(inputFiles, file)

				expectedFiles = append(expectedFiles, link)

				targetFile := filepath.Join(target, "meow.txt")
				expectedFiles = append(expectedFiles, targetFile)

				expectedFiles = filesWithParentDirs(expectedFiles)

				files, err := ResolvePaths(inputFiles, wl)

				validateResults(t, files, expectedFiles, err)
			})
		})
	})

	t.Run("empty set of files", func(t *testing.T) {
		inputFiles := []string{}
		expectedFiles := []string{}

		wl := []util.IgnoreListEntry{}

		files, err := ResolvePaths(inputFiles, wl)

		validateResults(t, files, expectedFiles, err)
	})
}

func Test_resolveSymlinkAncestor(t *testing.T) {
	setupDirs := func(t *testing.T) (string, string) {
		testDir, err := ioutil.TempDir("", "")
		if err != nil {
			t.Fatal(err)
		}

		targetDir := filepath.Join(testDir, "bar", "baz")

		if err := os.MkdirAll(targetDir, 0777); err != nil {
			t.Fatal(err)
		}

		targetPath := filepath.Join(targetDir, "bam.txt")

		if err := ioutil.WriteFile(targetPath, []byte("meow"), 0777); err != nil {
			t.Fatal(err)
		}

		return testDir, targetPath
	}

	t.Run("path is a symlink", func(t *testing.T) {
		testDir, targetPath := setupDirs(t)
		defer os.RemoveAll(testDir)

		linkDir := filepath.Join(testDir, "foo", "buzz")

		if err := os.MkdirAll(linkDir, 0777); err != nil {
			t.Fatal(err)
		}

		linkPath := filepath.Join(linkDir, "zoom.txt")

		if err := os.Symlink(targetPath, linkPath); err != nil {
			t.Fatal(err)
		}

		expected := linkPath

		actual, err := resolveSymlinkAncestor(linkPath)
		if err != nil {
			t.Errorf("expected err to be nil but was %s", err)
		}

		if actual != expected {
			t.Errorf("expected result to be %s not %s", expected, actual)
		}
	})

	t.Run("dir ends with / is not a symlink", func(t *testing.T) {
		testDir, _ := setupDirs(t)
		defer os.RemoveAll(testDir)

		linkDir := filepath.Join(testDir, "var", "www")
		if err := os.MkdirAll(linkDir, 0777); err != nil {
			t.Fatal(err)
		}

		expected := linkDir

		actual, err := resolveSymlinkAncestor(fmt.Sprintf("%s/", linkDir))
		if err != nil {
			t.Errorf("expected err to be nil but was %s", err)
		}

		if actual != expected {
			t.Errorf("expected result to be %s not %s", expected, actual)
		}
	})

	t.Run("path is a dead symlink", func(t *testing.T) {
		testDir, targetPath := setupDirs(t)
		defer os.RemoveAll(testDir)

		linkDir := filepath.Join(testDir, "foo", "buzz")

		if err := os.MkdirAll(linkDir, 0777); err != nil {
			t.Fatal(err)
		}

		linkPath := filepath.Join(linkDir, "zoom.txt")

		if err := os.Symlink(targetPath, linkPath); err != nil {
			t.Fatal(err)
		}

		if err := os.Remove(targetPath); err != nil {
			t.Fatal(err)
		}

		expected := linkPath

		actual, err := resolveSymlinkAncestor(linkPath)
		if err != nil {
			t.Errorf("expected err to be nil but was %s", err)
		}

		if actual != expected {
			t.Errorf("expected result to be %s not %s", expected, actual)
		}
	})

	t.Run("path is not a symlink", func(t *testing.T) {
		testDir, targetPath := setupDirs(t)
		defer os.RemoveAll(testDir)

		expected := targetPath

		actual, err := resolveSymlinkAncestor(targetPath)
		if err != nil {
			t.Errorf("expected err to be nil but was %s", err)
		}

		if actual != expected {
			t.Errorf("expected result to be %s not %s", expected, actual)
		}
	})

	t.Run("parent of path is a symlink", func(t *testing.T) {
		testDir, targetPath := setupDirs(t)
		defer os.RemoveAll(testDir)

		targetDir := filepath.Dir(targetPath)

		linkDir := filepath.Join(testDir, "foo")

		if err := os.MkdirAll(linkDir, 0777); err != nil {
			t.Fatal(err)
		}

		linkDir = filepath.Join(linkDir, "gaz")

		if err := os.Symlink(targetDir, linkDir); err != nil {
			t.Fatal(err)
		}

		linkPath := filepath.Join(linkDir, filepath.Base(targetPath))

		expected := linkDir

		actual, err := resolveSymlinkAncestor(linkPath)
		if err != nil {
			t.Errorf("expected err to be nil but was %s", err)
		}

		if actual != expected {
			t.Errorf("expected result to be %s not %s", expected, actual)
		}
	})

	t.Run("parent of path is a dead symlink", func(t *testing.T) {
		testDir, targetPath := setupDirs(t)
		defer os.RemoveAll(testDir)

		targetDir := filepath.Dir(targetPath)

		linkDir := filepath.Join(testDir, "foo")

		if err := os.MkdirAll(linkDir, 0777); err != nil {
			t.Fatal(err)
		}

		linkDir = filepath.Join(linkDir, "gaz")

		if err := os.Symlink(targetDir, linkDir); err != nil {
			t.Fatal(err)
		}

		if err := os.RemoveAll(targetDir); err != nil {
			t.Fatal(err)
		}

		linkPath := filepath.Join(linkDir, filepath.Base(targetPath))

		_, err := resolveSymlinkAncestor(linkPath)
		if err == nil {
			t.Error("expected err to not be nil")
		}
	})

	t.Run("great grandparent of path is a symlink", func(t *testing.T) {
		testDir, targetPath := setupDirs(t)
		defer os.RemoveAll(testDir)

		targetDir := filepath.Dir(targetPath)

		linkDir := filepath.Join(testDir, "foo")

		if err := os.Symlink(filepath.Dir(targetDir), linkDir); err != nil {
			t.Fatal(err)
		}

		linkPath := filepath.Join(
			linkDir,
			filepath.Join(
				filepath.Base(targetDir),
				filepath.Base(targetPath),
			),
		)

		expected := linkDir

		actual, err := resolveSymlinkAncestor(linkPath)
		if err != nil {
			t.Errorf("expected err to be nil but was %s", err)
		}

		if actual != expected {
			t.Errorf("expected result to be %s not %s", expected, actual)
		}
	})
}
