// Copyright 2019 The gVisor 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 ext

import (
	"bytes"
	"math/rand"
	"testing"

	"github.com/google/go-cmp/cmp"
	"gvisor.dev/gvisor/pkg/binary"
	"gvisor.dev/gvisor/pkg/sentry/fsimpl/ext/disklayout"
)

// These consts are for mocking the block map tree.
const (
	mockBMBlkSize  = uint32(16)
	mockBMDiskSize = 2500
)

// TestBlockMapReader stress tests block map reader functionality. It performs
// random length reads from all possible positions in the block map structure.
func TestBlockMapReader(t *testing.T) {
	mockBMFile, want := blockMapSetUp(t)
	n := len(want)

	for from := 0; from < n; from++ {
		got := make([]byte, n-from)

		if read, err := mockBMFile.ReadAt(got, int64(from)); err != nil {
			t.Fatalf("file read operation from offset %d to %d only read %d bytes: %v", from, n, read, err)
		}

		if diff := cmp.Diff(got, want[from:]); diff != "" {
			t.Fatalf("file data from offset %d to %d mismatched (-want +got):\n%s", from, n, diff)
		}
	}
}

// blkNumGen is a number generator which gives block numbers for building the
// block map file on disk. It gives unique numbers in a random order which
// facilitates in creating an extremely fragmented filesystem.
type blkNumGen struct {
	nums []uint32
}

// newBlkNumGen is the blkNumGen constructor.
func newBlkNumGen() *blkNumGen {
	blkNums := &blkNumGen{}
	lim := mockBMDiskSize / mockBMBlkSize
	blkNums.nums = make([]uint32, lim)
	for i := uint32(0); i < lim; i++ {
		blkNums.nums[i] = i
	}

	rand.Shuffle(int(lim), func(i, j int) {
		blkNums.nums[i], blkNums.nums[j] = blkNums.nums[j], blkNums.nums[i]
	})
	return blkNums
}

// next returns the next random block number.
func (n *blkNumGen) next() uint32 {
	ret := n.nums[0]
	n.nums = n.nums[1:]
	return ret
}

// blockMapSetUp creates a mock disk and a block map file. It initializes the
// block map file with 12 direct block, 1 indirect block, 1 double indirect
// block and 1 triple indirect block (basically fill it till the rim). It
// initializes the disk to reflect the inode. Also returns the file data that
// the inode covers and that is written to disk.
func blockMapSetUp(t *testing.T) (*blockMapFile, []byte) {
	mockDisk := make([]byte, mockBMDiskSize)
	regFile := regularFile{
		inode: inode{
			diskInode: &disklayout.InodeNew{
				InodeOld: disklayout.InodeOld{
					SizeLo: getMockBMFileFize(),
				},
			},
			dev:     bytes.NewReader(mockDisk),
			blkSize: uint64(mockBMBlkSize),
		},
	}

	var fileData []byte
	blkNums := newBlkNumGen()
	var data []byte

	// Write the direct blocks.
	for i := 0; i < numDirectBlks; i++ {
		curBlkNum := blkNums.next()
		data = binary.Marshal(data, binary.LittleEndian, curBlkNum)
		fileData = append(fileData, writeFileDataToBlock(mockDisk, curBlkNum, 0, blkNums)...)
	}

	// Write to indirect block.
	indirectBlk := blkNums.next()
	data = binary.Marshal(data, binary.LittleEndian, indirectBlk)
	fileData = append(fileData, writeFileDataToBlock(mockDisk, indirectBlk, 1, blkNums)...)

	// Write to indirect block.
	doublyIndirectBlk := blkNums.next()
	data = binary.Marshal(data, binary.LittleEndian, doublyIndirectBlk)
	fileData = append(fileData, writeFileDataToBlock(mockDisk, doublyIndirectBlk, 2, blkNums)...)

	// Write to indirect block.
	triplyIndirectBlk := blkNums.next()
	data = binary.Marshal(data, binary.LittleEndian, triplyIndirectBlk)
	fileData = append(fileData, writeFileDataToBlock(mockDisk, triplyIndirectBlk, 3, blkNums)...)

	copy(regFile.inode.diskInode.Data(), data)

	mockFile, err := newBlockMapFile(regFile)
	if err != nil {
		t.Fatalf("newBlockMapFile failed: %v", err)
	}
	return mockFile, fileData
}

// writeFileDataToBlock writes random bytes to the block on disk.
func writeFileDataToBlock(disk []byte, blkNum uint32, height uint, blkNums *blkNumGen) []byte {
	if height == 0 {
		start := blkNum * mockBMBlkSize
		end := start + mockBMBlkSize
		rand.Read(disk[start:end])
		return disk[start:end]
	}

	var fileData []byte
	for off := blkNum * mockBMBlkSize; off < (blkNum+1)*mockBMBlkSize; off += 4 {
		curBlkNum := blkNums.next()
		copy(disk[off:off+4], binary.Marshal(nil, binary.LittleEndian, curBlkNum))
		fileData = append(fileData, writeFileDataToBlock(disk, curBlkNum, height-1, blkNums)...)
	}
	return fileData
}

// getMockBMFileFize gets the size of the mock block map file which is used for
// testing.
func getMockBMFileFize() uint32 {
	return uint32(numDirectBlks*getCoverage(uint64(mockBMBlkSize), 0) + getCoverage(uint64(mockBMBlkSize), 1) + getCoverage(uint64(mockBMBlkSize), 2) + getCoverage(uint64(mockBMBlkSize), 3))
}