package compactext4

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/Microsoft/hcsshim/ext4/internal/format"
	"github.com/Microsoft/hcsshim/internal/memory"
)

type testFile struct {
	Path        string
	File        *File
	Data        []byte
	DataSize    int64
	Link        string
	ExpectError bool
}

var (
	data []byte
	name string
)

func init() {
	data = make([]byte, BlockSize*2)
	for i := range data {
		data[i] = uint8(i)
	}

	nameb := make([]byte, 300)
	for i := range nameb {
		nameb[i] = byte('0' + i%10)
	}
	name = string(nameb)
}

type largeData struct {
	pos int64
}

func (d *largeData) Read(b []byte) (int, error) {
	p := d.pos
	var pb [8]byte
	for i := range b {
		binary.LittleEndian.PutUint64(pb[:], uint64(p+int64(i)))
		b[i] = pb[i%8]
	}
	d.pos += int64(len(b))
	return len(b), nil
}

func (tf *testFile) Reader() io.Reader {
	if tf.DataSize != 0 {
		return io.LimitReader(&largeData{}, tf.DataSize)
	}
	return bytes.NewReader(tf.Data)
}

func createTestFile(t *testing.T, w *Writer, tf testFile) {
	t.Helper()
	var err error
	if tf.File != nil {
		tf.File.Size = int64(len(tf.Data))
		if tf.File.Size == 0 {
			tf.File.Size = tf.DataSize
		}
		err = w.Create(tf.Path, tf.File)
	} else {
		err = w.Link(tf.Link, tf.Path)
	}
	if tf.ExpectError && err == nil {
		t.Errorf("%s: expected error", tf.Path)
	} else if !tf.ExpectError && err != nil {
		t.Error(err)
	} else {
		_, err := io.Copy(w, tf.Reader())
		if err != nil {
			t.Error(err)
		}
	}
}

func expectedMode(f *File) uint16 {
	switch f.Mode & format.TypeMask {
	case 0:
		return f.Mode | S_IFREG
	case S_IFLNK:
		return f.Mode | 0777
	default:
		return f.Mode
	}
}

func expectedSize(f *File) int64 {
	switch f.Mode & format.TypeMask {
	case 0, S_IFREG:
		return f.Size
	case S_IFLNK:
		return int64(len(f.Linkname))
	default:
		return 0
	}
}

func xattrsEqual(x1, x2 map[string][]byte) bool {
	if len(x1) != len(x2) {
		return false
	}
	for name, value := range x1 {
		if !bytes.Equal(x2[name], value) {
			return false
		}
	}
	return true
}

func fileEqual(f1, f2 *File) bool {
	return f1.Linkname == f2.Linkname &&
		expectedSize(f1) == expectedSize(f2) &&
		expectedMode(f1) == expectedMode(f2) &&
		f1.Uid == f2.Uid &&
		f1.Gid == f2.Gid &&
		f1.Atime.Equal(f2.Atime) &&
		f1.Ctime.Equal(f2.Ctime) &&
		f1.Mtime.Equal(f2.Mtime) &&
		f1.Crtime.Equal(f2.Crtime) &&
		f1.Devmajor == f2.Devmajor &&
		f1.Devminor == f2.Devminor &&
		xattrsEqual(f1.Xattrs, f2.Xattrs)
}

func runTestsOnFiles(t *testing.T, testFiles []testFile, opts ...Option) {
	t.Helper()
	image := "testfs.img"
	imagef, err := os.Create(image)
	if err != nil {
		t.Fatal(err)
	}
	defer os.Remove(image)
	defer imagef.Close()

	w := NewWriter(imagef, opts...)
	for _, tf := range testFiles {
		createTestFile(t, w, tf)
		if !tf.ExpectError && tf.File != nil {
			f, err := w.Stat(tf.Path)
			if err != nil {
				if !strings.Contains(err.Error(), "cannot retrieve") {
					t.Error(err)
				}
			} else if !fileEqual(f, tf.File) {
				t.Errorf("%s: stat mismatch: %#v %#v", tf.Path, tf.File, f)
			}
		}
	}

	if t.Failed() {
		return
	}

	if err := w.Close(); err != nil {
		t.Fatal(err)
	}

	fsck(t, image)

	mountPath := "testmnt"

	if mountImage(t, image, mountPath) {
		defer unmountImage(t, mountPath)
		validated := make(map[string]*testFile)
		for i := range testFiles {
			tf := testFiles[len(testFiles)-i-1]
			if validated[tf.Link] != nil {
				// The link target was subsequently replaced. Find the
				// earlier instance.
				for j := range testFiles[:len(testFiles)-i-1] {
					otf := testFiles[j]
					if otf.Path == tf.Link && !otf.ExpectError {
						tf = otf
						break
					}
				}
			}
			if !tf.ExpectError && validated[tf.Path] == nil {
				verifyTestFile(t, mountPath, tf)
				validated[tf.Path] = &tf
			}
		}
	}
}

func TestBasic(t *testing.T) {
	now := time.Now()
	testFiles := []testFile{
		{Path: "empty", File: &File{Mode: 0644}},
		{Path: "small", File: &File{Mode: 0644}, Data: data[:40]},
		{Path: "time", File: &File{Atime: now, Ctime: now.Add(time.Second), Mtime: now.Add(time.Hour)}},
		{Path: "block_1", File: &File{Mode: 0644}, Data: data[:BlockSize]},
		{Path: "block_2", File: &File{Mode: 0644}, Data: data[:BlockSize*2]},
		{Path: "symlink", File: &File{Linkname: "block_1", Mode: format.S_IFLNK}},
		{Path: "symlink_59", File: &File{Linkname: name[:59], Mode: format.S_IFLNK}},
		{Path: "symlink_60", File: &File{Linkname: name[:60], Mode: format.S_IFLNK}},
		{Path: "symlink_120", File: &File{Linkname: name[:120], Mode: format.S_IFLNK}},
		{Path: "symlink_300", File: &File{Linkname: name[:300], Mode: format.S_IFLNK}},
		{Path: "dir", File: &File{Mode: format.S_IFDIR | 0755}},
		{Path: "dir/fifo", File: &File{Mode: format.S_IFIFO}},
		{Path: "dir/sock", File: &File{Mode: format.S_IFSOCK}},
		{Path: "dir/blk", File: &File{Mode: format.S_IFBLK, Devmajor: 0x5678, Devminor: 0x1234}},
		{Path: "dir/chr", File: &File{Mode: format.S_IFCHR, Devmajor: 0x5678, Devminor: 0x1234}},
		{Path: "dir/hard_link", Link: "small"},
	}

	runTestsOnFiles(t, testFiles)
}

func TestLargeDirectory(t *testing.T) {
	testFiles := []testFile{
		{Path: "bigdir", File: &File{Mode: format.S_IFDIR | 0755}},
	}
	for i := 0; i < 50000; i++ {
		testFiles = append(testFiles, testFile{
			Path: fmt.Sprintf("bigdir/%d", i), File: &File{Mode: 0644},
		})
	}

	runTestsOnFiles(t, testFiles)
}

func TestInlineData(t *testing.T) {
	testFiles := []testFile{
		{Path: "inline_30", File: &File{Mode: 0644}, Data: data[:30]},
		{Path: "inline_60", File: &File{Mode: 0644}, Data: data[:60]},
		{Path: "inline_120", File: &File{Mode: 0644}, Data: data[:120]},
		{Path: "inline_full", File: &File{Mode: 0644}, Data: data[:inlineDataSize]},
		{Path: "block_min", File: &File{Mode: 0644}, Data: data[:inlineDataSize+1]},
	}

	runTestsOnFiles(t, testFiles, InlineData)
}

func TestXattrs(t *testing.T) {
	testFiles := []testFile{
		{Path: "withsmallxattrs",
			File: &File{
				Mode: format.S_IFREG | 0644,
				Xattrs: map[string][]byte{
					"user.foo": []byte("test"),
					"user.bar": []byte("test2"),
				},
			},
		},
		{Path: "withlargexattrs",
			File: &File{
				Mode: format.S_IFREG | 0644,
				Xattrs: map[string][]byte{
					"user.foo": data[:100],
					"user.bar": data[:50],
				},
			},
		},
	}
	runTestsOnFiles(t, testFiles)
}

func TestReplace(t *testing.T) {
	testFiles := []testFile{
		{Path: "lost+found", ExpectError: true, File: &File{}}, // can't change type
		{Path: "lost+found", File: &File{Mode: format.S_IFDIR | 0777}},

		{Path: "dir", File: &File{Mode: format.S_IFDIR | 0777}},
		{Path: "dir/file", File: &File{}},
		{Path: "dir", File: &File{Mode: format.S_IFDIR | 0700}},

		{Path: "file", File: &File{}},
		{Path: "file", File: &File{Mode: 0600}},
		{Path: "file2", File: &File{}},
		{Path: "link", Link: "file2"},
		{Path: "file2", File: &File{Mode: 0600}},

		{Path: "nolinks", File: &File{}},
		{Path: "nolinks", ExpectError: true, Link: "file"}, // would orphan nolinks

		{Path: "onelink", File: &File{}},
		{Path: "onelink2", Link: "onelink"},
		{Path: "onelink", Link: "file"},

		{Path: "", ExpectError: true, File: &File{}},
		{Path: "", ExpectError: true, Link: "file"},
		{Path: "", File: &File{Mode: format.S_IFDIR | 0777}},

		{Path: "smallxattr", File: &File{Xattrs: map[string][]byte{"user.foo": data[:4]}}},
		{Path: "smallxattr", File: &File{Xattrs: map[string][]byte{"user.foo": data[:8]}}},

		{Path: "smallxattr_delete", File: &File{Xattrs: map[string][]byte{"user.foo": data[:4]}}},
		{Path: "smallxattr_delete", File: &File{}},

		{Path: "largexattr", File: &File{Xattrs: map[string][]byte{"user.small": data[:8], "user.foo": data[:200]}}},
		{Path: "largexattr", File: &File{Xattrs: map[string][]byte{"user.small": data[:12], "user.foo": data[:400]}}},

		{Path: "largexattr", File: &File{Xattrs: map[string][]byte{"user.foo": data[:200]}}},
		{Path: "largexattr_delete", File: &File{}},
	}
	runTestsOnFiles(t, testFiles)
}

func TestTime(t *testing.T) {
	now := time.Now()
	now2 := fsTimeToTime(timeToFsTime(now))
	if now.UnixNano() != now2.UnixNano() {
		t.Fatalf("%s != %s", now, now2)
	}
}

func TestLargeFile(t *testing.T) {
	testFiles := []testFile{
		{Path: "small", File: &File{}, DataSize: memory.MiB},        // can't change type
		{Path: "medium", File: &File{}, DataSize: 200 * memory.MiB}, // can't change type
		{Path: "large", File: &File{}, DataSize: 600 * memory.MiB},  // can't change type
	}
	runTestsOnFiles(t, testFiles)
}

func TestFileLinkLimit(t *testing.T) {
	testFiles := []testFile{
		{Path: "file", File: &File{}},
	}
	for i := 0; i < format.MaxLinks; i++ {
		testFiles = append(testFiles, testFile{Path: fmt.Sprintf("link%d", i), Link: "file"})
	}
	testFiles[len(testFiles)-1].ExpectError = true
	runTestsOnFiles(t, testFiles)
}

func TestDirLinkLimit(t *testing.T) {
	testFiles := []testFile{
		{Path: "dir", File: &File{Mode: S_IFDIR}},
	}
	for i := 0; i < format.MaxLinks-1; i++ {
		testFiles = append(testFiles, testFile{Path: fmt.Sprintf("dir/%d", i), File: &File{Mode: S_IFDIR}})
	}
	testFiles[len(testFiles)-1].ExpectError = true
	runTestsOnFiles(t, testFiles)
}

func TestLargeDisk(t *testing.T) {
	testFiles := []testFile{
		{Path: "file", File: &File{}},
	}
	runTestsOnFiles(t, testFiles, MaximumDiskSize(maxMaxDiskSize))
}
