// Copyright 2018 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 fsutil_test

import (
	"io"
	"syscall"
	"testing"

	"gvisor.googlesource.com/gvisor/pkg/sentry/context"
	"gvisor.googlesource.com/gvisor/pkg/sentry/context/contexttest"
	"gvisor.googlesource.com/gvisor/pkg/sentry/fs"
	"gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil"
	ramfstest "gvisor.googlesource.com/gvisor/pkg/sentry/fs/ramfs/test"
	"gvisor.googlesource.com/gvisor/pkg/sentry/usermem"
)

type testInodeOperations struct {
	fs.InodeOperations
	fs.InodeType
	FileSize int64
	writes   uint
	reads    uint
}

func (t *testInodeOperations) UnstableAttr(ctx context.Context, inode *fs.Inode) (fs.UnstableAttr, error) {
	return fs.UnstableAttr{Size: t.FileSize}, nil
}

// Check implements InodeOperations.Check.
func (t *testInodeOperations) Check(ctx context.Context, inode *fs.Inode, p fs.PermMask) bool {
	return fs.ContextCanAccessFile(ctx, inode, p)
}

func (t *testInodeOperations) DeprecatedPreadv(ctx context.Context, dst usermem.IOSequence, offset int64) (int64, error) {
	t.reads++
	return t.InodeOperations.DeprecatedPreadv(ctx, dst, offset)
}

func (t *testInodeOperations) DeprecatedPwritev(ctx context.Context, src usermem.IOSequence, offset int64) (int64, error) {
	t.writes++
	return t.InodeOperations.DeprecatedPwritev(ctx, src, offset)
}

// testHandle returns a handle for a test node.
//
// The size of the node is fixed at 20 bytes.
func testHandle(t *testing.T, flags fs.FileFlags, nt fs.InodeType) (*fs.File, *testInodeOperations) {
	ctx := contexttest.Context(t)
	m := fs.NewNonCachingMountSource(nil, fs.MountSourceFlags{})
	n := &testInodeOperations{
		InodeOperations: ramfstest.NewFile(ctx, fs.FilePermissions{User: fs.PermMask{Read: true, Write: true}}),
		FileSize:        20,
	}
	d := fs.NewDirent(fs.NewInode(n, m, fs.StableAttr{Type: nt}), "test")
	return fsutil.NewHandle(ctx, d, flags, d.Inode.HandleOps()), n
}

func TestHandleOps(t *testing.T) {
	h, n := testHandle(t, fs.FileFlags{Read: true, Write: true}, fs.RegularFile)
	defer h.DecRef()

	// Make sure a write request works.
	if n, err := h.Writev(contexttest.Context(t), usermem.BytesIOSequence([]byte("a"))); n != 1 || err != nil {
		t.Fatalf("Writev: got (%d, %v), wanted (1, nil)", n, err)
	}
	if n.writes != 1 {
		t.Errorf("found %d writes, expected 1", n.writes)
	}

	// Make sure a read request works.
	dst := make([]byte, 1)
	if n, err := h.Preadv(contexttest.Context(t), usermem.BytesIOSequence(dst), 0); n != 1 || (err != nil && err != io.EOF) {
		t.Errorf("Preadv: got (%d, %v), wanted (1, nil or EOF)", n, err)
	}
	if dst[0] != 'a' {
		t.Errorf("Preadv: read %q, wanted 'a'", dst[0])
	}
	if n.reads != 1 {
		t.Errorf("found %d reads, expected 1", n.reads)
	}
}

type seekTest struct {
	whence fs.SeekWhence
	offset int64
	result int64
	err    error
}

type seekSuite struct {
	nodeType fs.InodeType
	cases    []seekTest
}

// FIXME: This is currently missing fs.SeekEnd tests due to the
// fact that NullInodeOperations returns an error on stat.
func TestHandleSeek(t *testing.T) {
	ts := []seekSuite{
		{
			nodeType: fs.RegularFile,
			cases: []seekTest{
				{fs.SeekSet, 0, 0, nil},
				{fs.SeekSet, 10, 10, nil},
				{fs.SeekSet, -5, 10, syscall.EINVAL},
				{fs.SeekCurrent, -1, 9, nil},
				{fs.SeekCurrent, 2, 11, nil},
				{fs.SeekCurrent, -12, 11, syscall.EINVAL},
				{fs.SeekEnd, -1, 19, nil},
				{fs.SeekEnd, 0, 20, nil},
				{fs.SeekEnd, 2, 22, nil},
			},
		},
		{
			nodeType: fs.Directory,
			cases: []seekTest{
				{fs.SeekSet, 0, 0, nil},
				{fs.SeekSet, 10, 0, syscall.EINVAL},
				{fs.SeekSet, -5, 0, syscall.EINVAL},
				{fs.SeekCurrent, 0, 0, nil},
				{fs.SeekCurrent, 11, 0, syscall.EINVAL},
				{fs.SeekCurrent, -6, 0, syscall.EINVAL},
				{fs.SeekEnd, 0, 0, syscall.EINVAL},
				{fs.SeekEnd, -1, 0, syscall.EINVAL},
				{fs.SeekEnd, 2, 0, syscall.EINVAL},
			},
		},
		{
			nodeType: fs.Symlink,
			cases: []seekTest{
				{fs.SeekSet, 5, 0, syscall.EINVAL},
				{fs.SeekSet, -5, 0, syscall.EINVAL},
				{fs.SeekSet, 0, 0, syscall.EINVAL},
				{fs.SeekCurrent, 5, 0, syscall.EINVAL},
				{fs.SeekCurrent, -5, 0, syscall.EINVAL},
				{fs.SeekCurrent, 0, 0, syscall.EINVAL},
				{fs.SeekEnd, 5, 0, syscall.EINVAL},
				{fs.SeekEnd, -5, 0, syscall.EINVAL},
				{fs.SeekEnd, 0, 0, syscall.EINVAL},
			},
		},
		{
			nodeType: fs.Pipe,
			cases: []seekTest{
				{fs.SeekSet, 5, 0, syscall.ESPIPE},
				{fs.SeekSet, -5, 0, syscall.ESPIPE},
				{fs.SeekSet, 0, 0, syscall.ESPIPE},
				{fs.SeekCurrent, 5, 0, syscall.ESPIPE},
				{fs.SeekCurrent, -5, 0, syscall.ESPIPE},
				{fs.SeekCurrent, 0, 0, syscall.ESPIPE},
				{fs.SeekEnd, 5, 0, syscall.ESPIPE},
				{fs.SeekEnd, -5, 0, syscall.ESPIPE},
				{fs.SeekEnd, 0, 0, syscall.ESPIPE},
			},
		},
		{
			nodeType: fs.Socket,
			cases: []seekTest{
				{fs.SeekSet, 5, 0, syscall.ESPIPE},
				{fs.SeekSet, -5, 0, syscall.ESPIPE},
				{fs.SeekSet, 0, 0, syscall.ESPIPE},
				{fs.SeekCurrent, 5, 0, syscall.ESPIPE},
				{fs.SeekCurrent, -5, 0, syscall.ESPIPE},
				{fs.SeekCurrent, 0, 0, syscall.ESPIPE},
				{fs.SeekEnd, 5, 0, syscall.ESPIPE},
				{fs.SeekEnd, -5, 0, syscall.ESPIPE},
				{fs.SeekEnd, 0, 0, syscall.ESPIPE},
			},
		},
		{
			nodeType: fs.CharacterDevice,
			cases: []seekTest{
				{fs.SeekSet, 5, 0, nil},
				{fs.SeekSet, -5, 0, nil},
				{fs.SeekSet, 0, 0, nil},
				{fs.SeekCurrent, 5, 0, nil},
				{fs.SeekCurrent, -5, 0, nil},
				{fs.SeekCurrent, 0, 0, nil},
				{fs.SeekEnd, 5, 0, nil},
				{fs.SeekEnd, -5, 0, nil},
				{fs.SeekEnd, 0, 0, nil},
			},
		},
		{
			nodeType: fs.BlockDevice,
			cases: []seekTest{
				{fs.SeekSet, 0, 0, nil},
				{fs.SeekSet, 10, 10, nil},
				{fs.SeekSet, -5, 10, syscall.EINVAL},
				{fs.SeekCurrent, -1, 9, nil},
				{fs.SeekCurrent, 2, 11, nil},
				{fs.SeekCurrent, -12, 11, syscall.EINVAL},
				{fs.SeekEnd, -1, 19, nil},
				{fs.SeekEnd, 0, 20, nil},
				{fs.SeekEnd, 2, 22, nil},
			},
		},
	}

	for _, s := range ts {
		h, _ := testHandle(t, fs.FileFlags{Read: true, Write: true}, s.nodeType)
		defer h.DecRef()

		for _, c := range s.cases {
			// Try the given seek.
			offset, err := h.Seek(contexttest.Context(t), c.whence, c.offset)
			if err != c.err {
				t.Errorf("seek(%s, %d) on %s had unexpected error: expected %v, got %v", c.whence, c.offset, s.nodeType, c.err, err)
			}
			if err == nil && offset != c.result {
				t.Errorf("seek(%s, %d) on %s had bad result: expected %v, got %v", c.whence, c.offset, s.nodeType, c.result, offset)
			}
		}
	}
}