// Copyright 2017 Google Inc.
//
// 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 integration

import (
	"os"
	"path/filepath"
	"reflect"
	"syscall"
	"testing"

	"github.com/bazelbuild/sandboxfs/integration/utils"
)

func TestNesting_ScaffoldIntermediateComponents(t *testing.T) {
	state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/1/2/3/4/5:%ROOT%/subdir")
	defer state.TearDown(t)

	utils.MustWriteFile(t, state.RootPath("subdir", "file"), 0644, "some contents")

	if err := utils.DirEquals(state.RootPath("subdir"), state.MountPath("1/2/3/4/5")); err != nil {
		t.Error(err)
	}
	if err := utils.FileEquals(state.MountPath("1/2/3/4/5", "file"), "some contents"); err != nil {
		t.Error(err)
	}

	utils.MustMkdirAll(t, state.TempPath("golden/1/2/3/4/5"), 0755)
	for _, dir := range []string{"1/2/3/4", "1/2/3", "1/2", "1"} {
		goldenDir := state.TempPath("golden", dir)
		if err := os.Chmod(goldenDir, 0555); err != nil {
			t.Errorf("Failed to set golden dir permissions to 0555 to match scaffold dir expectations: %v", err)
		}
		defer os.Chmod(goldenDir, 0755) // To allow cleanup in tearDown to succeed.

		scaffoldDir := state.MountPath(dir)
		if err := utils.DirEquals(goldenDir, scaffoldDir); err != nil {
			t.Error(err)
		}
	}
}

func TestNesting_ScaffoldIntermediateComponentsAreImmutable(t *testing.T) {
	// Scaffold directories have mode 0555 to signal that they are read-only.  The mode alone
	// prevents unprivileged users from writing to those directories, but the mode has no effect
	// on root accesses.  Therefore, run the test as root to bypass permission checks and
	// attempt real writes.
	root := utils.RequireRoot(t, "Requires root privileges to write to directories with mode 0555")

	state := utils.MountSetupWithUser(t, root, "--mapping=ro:/:%ROOT%", "--mapping=rw:/1/2/3:%ROOT%/subdir")
	defer state.TearDown(t)

	for _, dir := range []string{"1/foo", "1/2/foo"} {
		err := os.Mkdir(state.MountPath(dir), 0755)
		pathErr, ok := err.(*os.PathError)
		if !ok || pathErr.Err != syscall.EPERM {
			t.Errorf("Want Mkdir to fail inside scaffold directory %s with %v; got %v (%v)", dir, syscall.EPERM, err, reflect.TypeOf(err))
		}
	}
	if err := os.Mkdir(state.MountPath("1/2/3/foo"), 0755); err != nil {
		t.Errorf("Want Mkdir to succeed inside non-scaffold directory; got %v", err)
	}
}

func TestNesting_ReadWriteWithinReadOnly(t *testing.T) {
	state := utils.MountSetup(t, "--mapping=rw:/:%ROOT%", "--mapping=ro:/ro:%ROOT%/one/two", "--mapping=rw:/ro/rw:%ROOT%")
	defer state.TearDown(t)

	if err := os.MkdirAll(state.MountPath("ro/hello"), 0755); err == nil {
		t.Errorf("Mkdir succeeded in read-only mapping")
	}
	if err := os.MkdirAll(state.MountPath("ro/rw/hello"), 0755); err != nil {
		t.Errorf("Mkdir failed in read-write mapping: %v", err)
	}
}

func TestNesting_SameTarget(t *testing.T) {
	state := utils.MountSetup(t, "--node_cache", "--mapping=ro:/:%ROOT%", "--mapping=rw:/dir1:%ROOT%/same", "--mapping=rw:/dir2/dir3/dir4:%ROOT%/same")
	defer state.TearDown(t)

	utils.MustWriteFile(t, state.MountPath("dir1/file"), 0644, "old contents")
	utils.MustWriteFile(t, state.MountPath("dir2/dir3/dir4/file"), 0644, "new contents")

	externalDir := state.RootPath("same")
	if err := utils.FileEquals(filepath.Join(externalDir, "file"), "new contents"); err != nil {
		t.Error(err)
	}
	for _, dir := range []string{"/dir1", "/dir2/dir3/dir4"} {
		internalDir := state.MountPath(dir)
		if err := utils.DirEquals(externalDir, internalDir); err != nil {
			t.Error(err)
		}
	}

	// We share the same internal representation for different mappings that point to the same
	// underlying file, which means that we can assume content changes through a mapping will be
	// reflected on the other mapping.  This is independent of how the kernel caches work or
	// when content invalidations happen.
	if err := utils.FileEquals(state.MountPath("dir1/file"), "new contents"); err != nil {
		t.Error(err)
	}
	if err := utils.FileEquals(state.MountPath("dir2/dir3/dir4/file"), "new contents"); err != nil {
		t.Error(err)
	}
}

func TestNesting_PreserveSymlinks(t *testing.T) {
	state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/dir1/dir2:%ROOT%")
	defer state.TearDown(t)

	utils.MustWriteFile(t, state.RootPath("file"), 0644, "file in root directory")
	utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
	if err := os.Symlink("..", state.RootPath("dir/up")); err != nil {
		t.Fatalf("Failed to create test symlink: %v", err)
	}

	if err := utils.FileEquals(state.MountPath("dir/up/file"), "file in root directory"); err != nil {
		t.Error(err)
	}
	if err := utils.FileEquals(state.MountPath("dir1/dir2/dir/up/file"), "file in root directory"); err != nil {
		t.Error(err)
	}
}
