package graphtest // import "github.com/docker/docker/daemon/graphdriver/graphtest"

import (
	"bytes"
	"fmt"
	"io/fs"
	"math/rand"
	"os"
	"path/filepath"
	"sort"

	"github.com/docker/docker/daemon/graphdriver"
	"github.com/docker/docker/pkg/stringid"
	"github.com/moby/go-archive"
)

func randomContent(size int, seed int64) []byte {
	s := rand.NewSource(seed)
	content := make([]byte, size)

	for i := 0; i < len(content); i += 7 {
		val := s.Int63()
		for j := 0; i+j < len(content) && j < 7; j++ {
			content[i+j] = byte(val)
			val >>= 8
		}
	}

	return content
}

func addFiles(drv graphdriver.Driver, layer string, seed int64) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	if err := os.WriteFile(filepath.Join(root, "file-a"), randomContent(64, seed), 0o755); err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Join(root, "dir-b"), 0o755); err != nil {
		return err
	}
	if err := os.WriteFile(filepath.Join(root, "dir-b", "file-b"), randomContent(128, seed+1), 0o755); err != nil {
		return err
	}

	return os.WriteFile(filepath.Join(root, "file-c"), randomContent(128*128, seed+2), 0o755)
}

func checkFile(drv graphdriver.Driver, layer, filename string, content []byte) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	fileContent, err := os.ReadFile(filepath.Join(root, filename))
	if err != nil {
		return err
	}

	if !bytes.Equal(fileContent, content) {
		return fmt.Errorf("mismatched file content %v, expecting %v", fileContent, content)
	}

	return nil
}

func addFile(drv graphdriver.Driver, layer, filename string, content []byte) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	return os.WriteFile(filepath.Join(root, filename), content, 0o755)
}

func addDirectory(drv graphdriver.Driver, layer, dir string) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	return os.MkdirAll(filepath.Join(root, dir), 0o755)
}

func removeAll(drv graphdriver.Driver, layer string, names ...string) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	for _, filename := range names {
		if err := os.RemoveAll(filepath.Join(root, filename)); err != nil {
			return err
		}
	}
	return nil
}

func checkFileRemoved(drv graphdriver.Driver, layer, filename string) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	if _, err := os.Stat(filepath.Join(root, filename)); err == nil {
		return fmt.Errorf("file still exists: %s", filepath.Join(root, filename))
	} else if !os.IsNotExist(err) {
		return err
	}

	return nil
}

func addManyFiles(drv graphdriver.Driver, layer string, count int, seed int64) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	for i := 0; i < count; i += 100 {
		dir := filepath.Join(root, fmt.Sprintf("directory-%d", i))
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return err
		}
		for j := 0; i+j < count && j < 100; j++ {
			file := filepath.Join(dir, fmt.Sprintf("file-%d", i+j))
			if err := os.WriteFile(file, randomContent(64, seed+int64(i+j)), 0o755); err != nil {
				return err
			}
		}
	}

	return nil
}

func changeManyFiles(drv graphdriver.Driver, layer string, count int, seed int64) ([]archive.Change, error) {
	root, err := drv.Get(layer, "")
	if err != nil {
		return nil, err
	}
	defer drv.Put(layer)

	var changes []archive.Change
	for i := 0; i < count; i += 100 {
		archiveRoot := fmt.Sprintf("/directory-%d", i)
		if err := os.MkdirAll(filepath.Join(root, archiveRoot), 0o755); err != nil {
			return nil, err
		}
		for j := 0; i+j < count && j < 100; j++ {
			if j == 0 {
				changes = append(changes, archive.Change{
					Path: archiveRoot,
					Kind: archive.ChangeModify,
				})
			}
			var change archive.Change
			switch j % 3 {
			// Update file
			case 0:
				change.Path = filepath.Join(archiveRoot, fmt.Sprintf("file-%d", i+j))
				change.Kind = archive.ChangeModify
				if err := os.WriteFile(filepath.Join(root, change.Path), randomContent(64, seed+int64(i+j)), 0o755); err != nil {
					return nil, err
				}
			// Add file
			case 1:
				change.Path = filepath.Join(archiveRoot, fmt.Sprintf("file-%d-%d", seed, i+j))
				change.Kind = archive.ChangeAdd
				if err := os.WriteFile(filepath.Join(root, change.Path), randomContent(64, seed+int64(i+j)), 0o755); err != nil {
					return nil, err
				}
			// Remove file
			case 2:
				change.Path = filepath.Join(archiveRoot, fmt.Sprintf("file-%d", i+j))
				change.Kind = archive.ChangeDelete
				if err := os.Remove(filepath.Join(root, change.Path)); err != nil {
					return nil, err
				}
			}
			changes = append(changes, change)
		}
	}

	return changes, nil
}

func checkManyFiles(drv graphdriver.Driver, layer string, count int, seed int64) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	for i := 0; i < count; i += 100 {
		dir := filepath.Join(root, fmt.Sprintf("directory-%d", i))
		for j := 0; i+j < count && j < 100; j++ {
			file := filepath.Join(dir, fmt.Sprintf("file-%d", i+j))
			fileContent, err := os.ReadFile(file)
			if err != nil {
				return err
			}

			content := randomContent(64, seed+int64(i+j))

			if !bytes.Equal(fileContent, content) {
				return fmt.Errorf("mismatched file content %v, expecting %v", fileContent, content)
			}
		}
	}

	return nil
}

type changeList []archive.Change

func (c changeList) Less(i, j int) bool {
	if c[i].Path == c[j].Path {
		return c[i].Kind < c[j].Kind
	}
	return c[i].Path < c[j].Path
}
func (c changeList) Len() int      { return len(c) }
func (c changeList) Swap(i, j int) { c[j], c[i] = c[i], c[j] }

func checkChanges(expected, actual []archive.Change) error {
	if len(expected) != len(actual) {
		return fmt.Errorf("unexpected number of changes, expected %d, got %d", len(expected), len(actual))
	}
	sort.Sort(changeList(expected))
	sort.Sort(changeList(actual))

	for i := range expected {
		if expected[i] != actual[i] {
			return fmt.Errorf("unexpected change, expecting %v, got %v", expected[i], actual[i])
		}
	}

	return nil
}

func addLayerFiles(drv graphdriver.Driver, layer, parent string, i int) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	if err := os.WriteFile(filepath.Join(root, "top-id"), []byte(layer), 0o755); err != nil {
		return err
	}
	layerDir := filepath.Join(root, fmt.Sprintf("layer-%d", i))
	if err := os.MkdirAll(layerDir, 0o755); err != nil {
		return err
	}
	if err := os.WriteFile(filepath.Join(layerDir, "layer-id"), []byte(layer), 0o755); err != nil {
		return err
	}
	return os.WriteFile(filepath.Join(layerDir, "parent-id"), []byte(parent), 0o755)
}

func addManyLayers(drv graphdriver.Driver, baseLayer string, count int) (string, error) {
	lastLayer := baseLayer
	for i := 1; i <= count; i++ {
		nextLayer := stringid.GenerateRandomID()
		if err := drv.Create(nextLayer, lastLayer, nil); err != nil {
			return "", err
		}
		if err := addLayerFiles(drv, nextLayer, lastLayer, i); err != nil {
			return "", err
		}

		lastLayer = nextLayer
	}
	return lastLayer, nil
}

func checkManyLayers(drv graphdriver.Driver, layer string, count int) error {
	root, err := drv.Get(layer, "")
	if err != nil {
		return err
	}
	defer drv.Put(layer)

	layerIDBytes, err := os.ReadFile(filepath.Join(root, "top-id"))
	if err != nil {
		return err
	}

	if !bytes.Equal(layerIDBytes, []byte(layer)) {
		return fmt.Errorf("mismatched file content %v, expecting %v", layerIDBytes, []byte(layer))
	}

	for i := count; i > 0; i-- {
		layerDir := filepath.Join(root, fmt.Sprintf("layer-%d", i))

		thisLayerIDBytes, err := os.ReadFile(filepath.Join(layerDir, "layer-id"))
		if err != nil {
			return err
		}
		if !bytes.Equal(thisLayerIDBytes, layerIDBytes) {
			return fmt.Errorf("mismatched file content %v, expecting %v", thisLayerIDBytes, layerIDBytes)
		}
		layerIDBytes, err = os.ReadFile(filepath.Join(layerDir, "parent-id"))
		if err != nil {
			return err
		}
	}
	return nil
}

// readDir reads a directory just like os.ReadDir()
// then hides specific files (currently "lost+found")
// so the tests don't "see" it
func readDir(dir string) ([]fs.DirEntry, error) {
	a, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}

	b := a[:0]
	for _, x := range a {
		if x.Name() != "lost+found" { // ext4 always have this dir
			b = append(b, x)
		}
	}

	return b, nil
}
