package overlayfs

import (
	"bytes"
	"errors"
	"io"
	"io/fs"
	"os"
	"sort"
	"strings"
	"testing"
	"time"

	qt "github.com/frankban/quicktest"
	"github.com/spf13/afero"
	"golang.org/x/tools/txtar"
)

func TestAppend(t *testing.T) {
	c := qt.New(t)
	ofs1 := New(Options{Fss: []afero.Fs{basicFs("1", "1"), basicFs("2", "1")}})
	ofs2 := ofs1.Append(basicFs("3", "1"))
	c.Assert(ofs1.NumFilesystems(), qt.Equals, 2)
	c.Assert(ofs2.NumFilesystems(), qt.Equals, 3)
	c.Assert(readDirnames(c, ofs1, "mydir"), qt.DeepEquals, []string{"f1-1.txt", "f2-1.txt", "f1-2.txt", "f2-2.txt"})
	c.Assert(readDirnames(c, ofs2, "mydir"), qt.DeepEquals, []string{"f1-1.txt", "f2-1.txt", "f1-2.txt", "f2-2.txt", "f1-3.txt", "f2-3.txt"})
}

func TestEmpty(t *testing.T) {
	c := qt.New(t)
	ofs := New(Options{FirstWritable: true})
	c.Assert(ofs.NumFilesystems(), qt.Equals, 0)
	_, err := ofs.Stat("mydir/notfound.txt")
	c.Assert(err, qt.ErrorIs, fs.ErrNotExist)
	c.Assert(func() { ofs.Create("mydir/foo.txt") }, qt.PanicMatches, "overlayfs: there are no filesystems to write to")

	ofs = ofs.Append(basicFs("1", "1"))
	c.Assert(ofs.NumFilesystems(), qt.Equals, 1)
	_, err = ofs.Stat("mydir/f1-1.txt")
	c.Assert(err, qt.IsNil)
	f, err := ofs.Create("mydir/foo.txt")
	c.Assert(err, qt.IsNil)
	f.Close()
}

func TestFileystemIterator(t *testing.T) {
	c := qt.New(t)
	fs1, fs2 := basicFs("", "1"), basicFs("", "2")
	ofs := New(Options{Fss: []afero.Fs{fs1, fs2}})

	c.Assert(ofs.NumFilesystems(), qt.Equals, 2)
	c.Assert(ofs.Filesystem(0), qt.Equals, fs1)
	c.Assert(ofs.Filesystem(1), qt.Equals, fs2)
	c.Assert(ofs.Filesystem(2), qt.IsNil)
}

func TestReadOps(t *testing.T) {
	c := qt.New(t)
	fs1, fs2 := basicFs("1", "1"), basicFs("1", "2")
	ofs := New(Options{Fss: []afero.Fs{fs1, fs2}})

	c.Assert(ofs.Name(), qt.Equals, "overlayfs")

	// Open
	c.Assert(readFile(c, ofs, "mydir/f1-1.txt"), qt.Equals, "f1-1")

	// Stat
	fi, err := ofs.Stat("mydir/f1-1.txt")
	c.Assert(err, qt.IsNil)
	c.Assert(fi.Name(), qt.Equals, "f1-1.txt")
	_, err = ofs.Stat("mydir/notfound.txt")
	c.Assert(err, qt.ErrorIs, fs.ErrNotExist)

	// LstatIfPossible
	fi, _, err = ofs.LstatIfPossible("mydir/f2-1.txt")
	c.Assert(err, qt.IsNil)
	c.Assert(fi.Name(), qt.Equals, "f2-1.txt")
	_, _, err = ofs.LstatIfPossible("mydir/notfound.txt")
	c.Assert(err, qt.ErrorIs, fs.ErrNotExist)

}

func TestReadOpsErrors(t *testing.T) {
	c := qt.New(t)
	statErr := errors.New("stat error")
	fs1, fs2 := basicFs("1", "1"), &testFs{statErr: statErr}
	ofs := New(Options{Fss: []afero.Fs{fs1, fs2}})

	fi, err := ofs.Stat("mydir/f1-1.txt")
	c.Assert(err, qt.IsNil)
	c.Assert(fi.Name(), qt.Equals, "f1-1.txt")
	_, err = ofs.Stat("mydir/notfound.txt")
	c.Assert(err, qt.ErrorIs, statErr)

	// LstatIfPossible
	fi, _, err = ofs.LstatIfPossible("mydir/f2-1.txt")
	c.Assert(err, qt.IsNil)
	c.Assert(fi.Name(), qt.Equals, "f2-1.txt")
	_, _, err = ofs.LstatIfPossible("mydir/notfound.txt")
	c.Assert(err, qt.ErrorIs, statErr)

}

func TestOpenRecursive(t *testing.T) {
	c := qt.New(t)
	fs1, fs2 := basicFs("1", "1"), basicFs("1", "2")
	fs3, fs4 := basicFs("2", "3"), basicFs("1", "4")
	ofs2 := New(Options{Fss: []afero.Fs{fs1, fs2}})
	ofs3 := New(Options{Fss: []afero.Fs{ofs2, fs3, fs4}})
	ofs1 := New(Options{Fss: []afero.Fs{ofs3}})

	c.Assert(readFile(c, ofs1, "mydir/f1-1.txt"), qt.Equals, "f1-1")
	c.Assert(readFile(c, ofs1, "mydir/f1-2.txt"), qt.Equals, "f1-3")

}

func TestWriteOpsReadonly(t *testing.T) {
	c := qt.New(t)
	fs1, fs2 := basicFs("1", "1"), basicFs("1", "2")
	ofsReadOnly := New(Options{Fss: []afero.Fs{fs1, fs2}})

	_, err := ofsReadOnly.Create("mydir/foo.txt")
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	_, err = ofsReadOnly.OpenFile("mydir/foo.txt", os.O_CREATE, 0777)

	err = ofsReadOnly.Chmod("mydir/foo.txt", 0666)
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	err = ofsReadOnly.Chown("mydir/foo.txt", 1, 2)
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	err = ofsReadOnly.Chtimes("mydir/foo.txt", time.Now(), time.Now())
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	err = ofsReadOnly.Mkdir("mydir", 0777)
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	err = ofsReadOnly.MkdirAll("mydir", 0777)
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	err = ofsReadOnly.Remove("mydir")
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	err = ofsReadOnly.RemoveAll("mydir")
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)

	err = ofsReadOnly.Rename("a", "b")
	c.Assert(err, qt.ErrorIs, fs.ErrPermission)
}

func TestWriteOpsFirstWriteable(t *testing.T) {
	c := qt.New(t)
	fs1, fs2 := basicFs("1", "1"), basicFs("1", "2")
	ofs := New(Options{Fss: []afero.Fs{fs1, fs2}, FirstWritable: true})

	f, err := ofs.Create("mydir/foo.txt")
	c.Assert(err, qt.IsNil)
	f.Close()
}

func TestReadDir(t *testing.T) {
	c := qt.New(t)
	fs1, fs2 := basicFs("1", "1"), basicFs("1", "2")
	fs3, fs4 := basicFs("2", "3"), basicFs("1", "4")
	ofs2 := New(Options{Fss: []afero.Fs{fs1, fs2}})
	ofs1 := New(Options{Fss: []afero.Fs{ofs2, fs3, fs4}})

	dirnames := readDirnames(c, ofs1, "mydir")

	c.Assert(dirnames, qt.DeepEquals, []string{"f1-1.txt", "f2-1.txt", "f1-2.txt", "f2-2.txt"})

	ofsSingle := New(Options{Fss: []afero.Fs{basicFs("1", "1")}})

	dirnames = readDirnames(c, ofsSingle, "mydir")

	c.Assert(dirnames, qt.DeepEquals, []string{"f1-1.txt", "f2-1.txt"})
}

func TestReadDirN(t *testing.T) {
	c := qt.New(t)
	// 6 files.
	ofs := New(Options{Fss: []afero.Fs{basicFs("1", "1"), basicFs("2", "2"), basicFs("3", "3")}})

	d, _ := ofs.Open("mydir")

	for i := 0; i < 3; i++ {
		fis, err := d.Readdir(2)
		c.Assert(err, qt.IsNil)
		c.Assert(len(fis), qt.Equals, 2)
	}

	_, err := d.Readdir(1)
	c.Assert(err, qt.ErrorIs, io.EOF)
	c.Assert(d.Close(), qt.IsNil)

	d, _ = ofs.Open("mydir")
	fis, err := d.Readdir(32)
	c.Assert(err, qt.IsNil)
	c.Assert(len(fis), qt.Equals, 6)
	fis, err = d.Readdir(-1)
	c.Assert(len(fis), qt.Equals, 0)
	c.Assert(err, qt.ErrorIs, io.EOF)
	c.Assert(d.Close(), qt.IsNil)

	d, _ = ofs.Open("mydir")
	fis, err = d.Readdir(1)
	c.Assert(err, qt.IsNil)
	c.Assert(len(fis), qt.Equals, 1)
	fis, err = d.Readdir(4)
	c.Assert(len(fis), qt.Equals, 4)
	c.Assert(err, qt.IsNil)
	c.Assert(d.Close(), qt.IsNil)

	d, _ = ofs.Open("mydir")
	dirnames, err := d.Readdirnames(3)
	c.Assert(err, qt.IsNil)
	c.Assert(dirnames, qt.DeepEquals, []string{"f1-1.txt", "f2-1.txt", "f1-2.txt"})
	c.Assert(d.Close(), qt.IsNil)

	d, _ = ofs.Open("mydir")
	_, err = d.Readdir(-1)
	c.Assert(err, qt.IsNil)
	_, err = d.Readdir(-1)
	c.Assert(err, qt.ErrorIs, io.EOF)
	c.Assert(d.Close(), qt.IsNil)

}

func TestReadDirStable(t *testing.T) {
	c := qt.New(t)

	// 6 files.
	ofs := New(Options{Fss: []afero.Fs{basicFs("1", "1"), basicFs("2", "2"), basicFs("3", "3")}})
	d, _ := ofs.Open("mydir")
	fis1, err := d.Readdir(-1)
	c.Assert(err, qt.IsNil)
	d.Close()
	d, _ = ofs.Open("mydir")
	fis2, err := d.Readdir(2)
	c.Assert(err, qt.IsNil)
	c.Assert(d.Close(), qt.IsNil)
	c.Assert(fis1[0].Name(), qt.Equals, "f1-1.txt")
	c.Assert(fis2[0].Name(), qt.Equals, "f1-1.txt")
	sort.Slice(fis1, func(i, j int) bool { return fis1[i].Name() > fis1[j].Name() })
	sort.Slice(fis2, func(i, j int) bool { return fis2[i].Name() > fis2[j].Name() })
	checkFi := func() {
		c.Assert(fis1[0].Name(), qt.Equals, "f2-3.txt")
		c.Assert(fis2[0].Name(), qt.Equals, "f2-1.txt")
	}
	checkFi()
	for i := 0; i < 10; i++ {
		d, _ = ofs.Open("mydir")
		d.Readdir(-1)
		c.Assert(d.Close(), qt.IsNil)
	}
	checkFi()
}
func TestDirOps(t *testing.T) {
	c := qt.New(t)
	ofs := New(Options{Fss: []afero.Fs{basicFs("1", "1"), basicFs("2", "1")}})

	dir, err := ofs.Open("mydir")
	c.Assert(err, qt.IsNil)

	c.Assert(dir.Name(), qt.Equals, "mydir")
	_, err = dir.Stat()
	c.Assert(err, qt.IsNil)

	// Not supported.
	c.Assert(dir.Sync, qt.PanicMatches, `not supported`)

	c.Assert(func() { dir.Truncate(0) }, qt.PanicMatches, `not supported`)
	c.Assert(func() { dir.WriteString("asdf") }, qt.PanicMatches, `not supported`)
	c.Assert(func() { dir.Write(nil) }, qt.PanicMatches, `not supported`)
	c.Assert(func() { dir.WriteAt(nil, 21) }, qt.PanicMatches, `not supported`)
	c.Assert(func() { dir.Read(nil) }, qt.PanicMatches, `not supported`)
	c.Assert(func() { dir.ReadAt(nil, 21) }, qt.PanicMatches, `not supported`)
	c.Assert(func() { dir.Seek(1, 2) }, qt.PanicMatches, `not supported`)

	c.Assert(dir.Close(), qt.IsNil)
	_, err = dir.Stat()
	c.Assert(err, qt.ErrorIs, fs.ErrClosed)
}

func readDirnames(c *qt.C, fs afero.Fs, name string) []string {
	dir, err := fs.Open(name)
	c.Assert(err, qt.IsNil)
	defer dir.Close()

	dirnames, err := dir.Readdirnames(-1)
	c.Assert(err, qt.IsNil)
	return dirnames
}

func readFile(c *qt.C, fs afero.Fs, name string) string {
	c.Helper()
	f, err := fs.Open(name)
	c.Assert(err, qt.IsNil)
	defer f.Close()
	b, err := afero.ReadAll(f)
	c.Assert(err, qt.IsNil)
	return string(b)
}

func basicFs(idFilename, idContent string) afero.Fs {
	return fsFromTxtTar(
		strings.ReplaceAll(
			strings.ReplaceAll(`
