package workspace

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"golang.org/x/sys/unix"

	iop "iop/proto/gen/iop"
)

func openedRuntime(t *testing.T) (*Runtime, string) {
	t.Helper()
	root := t.TempDir()
	rt, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = rt.Close() })
	if _, err := rt.Open(testRequestAuthority("request-1")); err != nil {
		t.Fatal(err)
	}
	return rt, root
}

func TestFileExecutorReadListWriteDelete(t *testing.T) {
	rt, root := openedRuntime(t)
	if err := os.WriteFile(filepath.Join(root, "input.txt"), []byte("hello"), 0600); err != nil {
		t.Fatal(err)
	}
	read := rt.Read("request-1", "input.txt")
	if read.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(read.Content) != "hello" {
		t.Fatalf("read=%+v", read)
	}
	list := rt.List("request-1", ".")
	if list.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || len(list.Entries) != 1 || list.Entries[0] != "input.txt\tfile" {
		t.Fatalf("list=%+v", list)
	}
	if write := rt.Write("request-1", "nested/output.txt", []byte("written")); write.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("write=%+v", write)
	}
	data, err := os.ReadFile(filepath.Join(root, "nested", "output.txt"))
	if err != nil || string(data) != "written" {
		t.Fatalf("output=%q err=%v", data, err)
	}
	if deleted := rt.Delete("request-1", "nested/output.txt"); deleted.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("delete=%+v", deleted)
	}
	if _, err := os.Stat(filepath.Join(root, "nested", "output.txt")); !os.IsNotExist(err) {
		t.Fatalf("deleted file remains: %v", err)
	}
}

func TestFileExecutorRejectsReservedSymlinkAndBounds(t *testing.T) {
	rt, root := openedRuntime(t)
	outside := filepath.Join(t.TempDir(), "outside.txt")
	if err := os.WriteFile(outside, []byte("outside"), 0600); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(outside, filepath.Join(root, "escape")); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(root, ".iop"), 0700); err != nil {
		t.Fatal(err)
	}
	for _, target := range []string{".iop", ".iop/job/request-2/x", "escape"} {
		if result := rt.Read("request-1", target); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
			t.Fatalf("read admitted %q: %+v", target, result)
		}
	}
	if err := os.WriteFile(filepath.Join(root, "large"), []byte(strings.Repeat("x", 65)), 0600); err != nil {
		t.Fatal(err)
	}
	if result := rt.Read("request-1", "large"); !result.Truncated || len(result.Content) != 64 {
		t.Fatalf("bounded read=%+v", result)
	}
	if result := rt.Write("request-1", "too-large", []byte(strings.Repeat("x", 65))); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("oversize write=%+v", result)
	}
	request, err := rt.Request("request-1")
	if err != nil {
		t.Fatal(err)
	}
	device := request.entry.device
	request.entry.device++
	if result := rt.Read("request-1", "large"); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("cross-filesystem read=%+v", result)
	}
	request.entry.device = device
}

func TestFileExecutorWriteFailurePreservesTarget(t *testing.T) {
	rt, root := openedRuntime(t)
	target := filepath.Join(root, "target.txt")
	if err := os.WriteFile(target, []byte("old"), 0600); err != nil {
		t.Fatal(err)
	}
	if result := rt.Write("request-1", "target.txt", []byte(strings.Repeat("x", 65))); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("write=%+v", result)
	}
	data, err := os.ReadFile(target)
	if err != nil || string(data) != "old" {
		t.Fatalf("target=%q err=%v", data, err)
	}
	request, err := rt.Request("request-1")
	if err != nil {
		t.Fatal(err)
	}
	request.entry.beforeRename = func() error { return errors.New("injected before rename") }
	if result := rt.Write("request-1", "target.txt", []byte("new")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("injected write=%+v", result)
	}
	request.entry.beforeRename = nil
	data, err = os.ReadFile(target)
	if err != nil || string(data) != "old" {
		t.Fatalf("target after injected failure=%q err=%v", data, err)
	}
	assertNoWriteTemps(t, root)
	if result := rt.Delete("request-1", "."); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("root delete=%+v", result)
	}
	if err := os.Mkdir(filepath.Join(root, "nonempty"), 0700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "nonempty", "child"), []byte("x"), 0600); err != nil {
		t.Fatal(err)
	}
	if result := rt.Delete("request-1", "nonempty"); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("recursive delete=%+v", result)
	}
}

func TestFileExecutorWriteRejectsUnsafeParentsWithoutEffects(t *testing.T) {
	rt, root := openedRuntime(t)
	outside := t.TempDir()
	if err := os.Symlink(outside, filepath.Join(root, "link")); err != nil {
		t.Fatal(err)
	}
	if result := rt.Write("request-1", "link/new/output.txt", []byte("bad")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("symlink-parent write=%+v", result)
	}
	if _, err := os.Stat(filepath.Join(outside, "new")); !os.IsNotExist(err) {
		t.Fatalf("rejected symlink write created outside parent: %v", err)
	}

	request, err := rt.Request("request-1")
	if err != nil {
		t.Fatal(err)
	}
	originalDevice := request.entry.device
	request.entry.device++
	if result := rt.Write("request-1", "mount-substitute/output.txt", []byte("bad")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("foreign-device write=%+v", result)
	}
	request.entry.device = originalDevice
	if _, err := os.Stat(filepath.Join(root, "mount-substitute")); !os.IsNotExist(err) {
		t.Fatalf("foreign-device rejection created parent: %v", err)
	}

	parent := filepath.Join(root, "parent")
	if err := os.Mkdir(parent, 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(parent, "target.txt"), []byte("old"), 0o600); err != nil {
		t.Fatal(err)
	}
	moved := filepath.Join(root, "parent-moved")
	request.entry.beforeRename = func() error {
		if err := os.Rename(parent, moved); err != nil {
			return err
		}
		return os.Mkdir(parent, 0o700)
	}
	if result := rt.Write("request-1", "parent/target.txt", []byte("new")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("replaced-parent write=%+v", result)
	}
	request.entry.beforeRename = nil
	data, err := os.ReadFile(filepath.Join(moved, "target.txt"))
	if err != nil || string(data) != "old" {
		t.Fatalf("moved target=%q err=%v", data, err)
	}
	entries, err := os.ReadDir(parent)
	if err != nil || len(entries) != 0 {
		t.Fatalf("replacement parent entries=%v err=%v", entries, err)
	}
	assertNoWriteTemps(t, root)
}

func TestFileExecutorBoundedDeterministicLargeList(t *testing.T) {
	root := t.TempDir()
	config := testWorkspaceConfig(root)
	config.MaxOutputBytes = 256
	rt, err := NewRuntime([]*iop.WorkspaceConfig{config}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = rt.Close() })
	authority := testRequestAuthority("request-1")
	authority.MaxOutputBytes = 256
	if _, err := rt.Open(authority); err != nil {
		t.Fatal(err)
	}
	for index := 0; index < maxListEntries+200; index++ {
		name := fmt.Sprintf("entry-%04d-with-bounded-name", index)
		if err := os.WriteFile(filepath.Join(root, name), nil, 0o600); err != nil {
			t.Fatal(err)
		}
	}
	first := rt.List("request-1", ".")
	second := rt.List("request-1", ".")
	if first.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || !first.Truncated || len(first.Entries) == 0 {
		t.Fatalf("first list=%+v", first)
	}
	if strings.Join(first.Entries, "\n") != strings.Join(second.Entries, "\n") || first.Entries[0] != "entry-0000-with-bounded-name\tfile" {
		t.Fatalf("list is not deterministic: first=%v second=%v", first.Entries, second.Entries)
	}
}

func TestFileExecutorRejectsSpecialFileAndRunsParallelRequests(t *testing.T) {
	rt, root := openedRuntime(t)
	fifo := filepath.Join(root, "special")
	if err := unix.Mkfifo(fifo, 0o600); err != nil {
		t.Fatal(err)
	}
	for _, result := range []Result{
		rt.Read("request-1", "special"),
		rt.Write("request-1", "special", []byte("bad")),
		rt.Delete("request-1", "special"),
	} {
		if result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
			t.Fatalf("special file operation succeeded: %+v", result)
		}
	}
	if err := os.Remove(fifo); err != nil {
		t.Fatal(err)
	}

	requestIDs := make([]string, 16)
	for index := 0; index < 16; index++ {
		requestID := fmt.Sprintf("request-%d", index+2)
		requestIDs[index] = requestID
		if _, err := rt.Open(testRequestAuthority(requestID)); err != nil {
			t.Fatal(err)
		}
	}
	var group sync.WaitGroup
	for index, requestID := range requestIDs {
		group.Add(1)
		go func(index int, requestID string) {
			defer group.Done()
			name := fmt.Sprintf("parallel/%02d.txt", index)
			if result := rt.Write(requestID, name, []byte(requestID)); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
				t.Errorf("write %s=%+v", requestID, result)
				return
			}
			if result := rt.Read(requestID, name); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(result.Content) != requestID {
				t.Errorf("read %s=%+v", requestID, result)
			}
		}(index, requestID)
	}
	group.Wait()
	for _, requestID := range requestIDs {
		group.Add(1)
		go func(requestID string) {
			defer group.Done()
			if result := rt.List(requestID, "parallel"); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
				t.Errorf("list %s=%+v", requestID, result)
			}
		}(requestID)
	}
	group.Wait()
	for index, requestID := range requestIDs {
		group.Add(1)
		go func(index int, requestID string) {
			defer group.Done()
			name := fmt.Sprintf("parallel/%02d.txt", index)
			if result := rt.Delete(requestID, name); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
				t.Errorf("delete %s=%+v", requestID, result)
			}
		}(index, requestID)
	}
	group.Wait()
}

func assertNoWriteTemps(t *testing.T, root string) {
	t.Helper()
	err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if strings.HasPrefix(entry.Name(), ".iop-write-") {
			t.Fatalf("temporary write artifact remains: %s", path)
		}
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
}
