package testutil

import (
	"fmt"
	"os"
	"path/filepath"

	"src.elv.sh/pkg/env"
	"src.elv.sh/pkg/must"
)

// TempDir creates a temporary directory for testing that will be removed
// after the test finishes. It is different from testing.TB.TempDir in that it
// resolves symlinks in the path of the directory.
//
// It panics if the test directory cannot be created or symlinks cannot be
// resolved. It is only suitable for use in tests.
func TempDir(c Cleanuper) string {
	dir, err := os.MkdirTemp("", "elvishtest.")
	if err != nil {
		panic(err)
	}
	dir, err = filepath.EvalSymlinks(dir)
	if err != nil {
		panic(err)
	}
	c.Cleanup(func() {
		err := os.RemoveAll(dir)
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to remove temp dir %s: %v\n", dir, err)
		}
	})
	return dir
}

// TempHome is equivalent to Setenv(c, env.HOME, TempDir(c))
func TempHome(c Cleanuper) string {
	return Setenv(c, env.HOME, TempDir(c))
}

// Chdir changes into a directory, and restores the original working directory
// when a test finishes. It returns the directory for easier chaining.
func Chdir(c Cleanuper, dir string) string {
	oldWd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	must.Chdir(dir)
	c.Cleanup(func() {
		must.Chdir(oldWd)
	})
	return dir
}

// InTempDir is equivalent to Chdir(c, TempDir(c)).
func InTempDir(c Cleanuper) string {
	return Chdir(c, TempDir(c))
}

// InTempHome is equivalent to Setenv(c, env.HOME, InTempDir(c))
func InTempHome(c Cleanuper) string {
	return Setenv(c, env.HOME, InTempDir(c))
}

// Dir describes the layout of a directory. The keys of the map represent
// filenames. Each value is either a string (for the content of a regular file
// with permission 0644), a File, or a Dir.
type Dir map[string]any

// File describes a file to create.
type File struct {
	Perm    os.FileMode
	Content string
}

// ApplyDir creates the given filesystem layout in the current directory.
func ApplyDir(dir Dir) {
	ApplyDirIn(dir, "")
}

// ApplyDirIn creates the given filesystem layout in a given directory.
func ApplyDirIn(dir Dir, root string) {
	for name, file := range dir {
		path := filepath.Join(root, name)
		switch file := file.(type) {
		case string:
			must.OK(os.WriteFile(path, []byte(file), 0644))
		case File:
			must.OK(os.WriteFile(path, []byte(file.Content), file.Perm))
		case Dir:
			must.OK(os.MkdirAll(path, 0755))
			ApplyDirIn(file, path)
		default:
			panic(fmt.Sprintf("file is neither string, Dir, or Symlink: %v", file))
		}
	}
}
