package workspace

import (
	"fmt"
	"os"
	"strings"
	"sync"
	"testing"

	iop "iop/proto/gen/iop"
)

func testWorkspaceConfig(root string) *iop.WorkspaceConfig {
	return &iop.WorkspaceConfig{
		Ref: "mac-workspace", Platform: "darwin", Root: root,
		Operations: []iop.WorkspaceOperation{
			iop.WorkspaceOperation_WORKSPACE_OPERATION_READ,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE,
		},
		MaxReadBytes: 64, MaxWriteBytes: 64, MaxOutputBytes: 64,
	}
}

func testRequestAuthority(requestID string) RequestAuthority {
	return RequestAuthority{
		RequestID: requestID, WorkspaceRef: "mac-workspace",
		Operations: []iop.WorkspaceOperation{
			iop.WorkspaceOperation_WORKSPACE_OPERATION_READ,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE,
		},
		MaxReadBytes: 64, MaxWriteBytes: 64, MaxOutputBytes: 64,
	}
}

func TestRuntimeCatalog(t *testing.T) {
	root := t.TempDir()
	for _, hostOS := range []string{"darwin", "linux"} {
		t.Run(hostOS+" catalog matches host", func(t *testing.T) {
			workspaceConfig := testWorkspaceConfig(root)
			workspaceConfig.Platform = hostOS
			runtime, err := NewRuntime([]*iop.WorkspaceConfig{workspaceConfig}, hostOS, nil)
			if err != nil {
				t.Fatalf("NewRuntime(valid): %v", err)
			}
			if err := runtime.Close(); err != nil {
				t.Fatalf("Close: %v", err)
			}
		})
	}
	for name, testCase := range map[string]struct {
		configs []*iop.WorkspaceConfig
		hostOS  string
	}{
		"cross-platform mismatch": {configs: []*iop.WorkspaceConfig{testWorkspaceConfig(root)}, hostOS: "linux"},
		"unsupported host":        {configs: []*iop.WorkspaceConfig{testWorkspaceConfig(root)}, hostOS: "windows"},
		"unknown catalog platform": {configs: func() []*iop.WorkspaceConfig {
			workspaceConfig := testWorkspaceConfig(root)
			workspaceConfig.Platform = "plan9"
			return []*iop.WorkspaceConfig{workspaceConfig}
		}(), hostOS: "darwin"},
		"missing": {configs: []*iop.WorkspaceConfig{testWorkspaceConfig(root + "/missing")}, hostOS: "darwin"},
		"root":    {configs: []*iop.WorkspaceConfig{testWorkspaceConfig("/")}, hostOS: "darwin"},
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := NewRuntime(testCase.configs, testCase.hostOS, nil); err == nil {
				t.Fatal("NewRuntime succeeded")
			}
		})
	}
	if runtime, err := NewRuntime(nil, "windows", nil); err != nil {
		t.Fatalf("empty catalog compatibility: %v", err)
	} else if err := runtime.Close(); err != nil {
		t.Fatalf("empty catalog Close: %v", err)
	}
	link := root + "-link"
	if err := os.Symlink(root, link); err != nil {
		t.Fatal(err)
	}
	if _, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(link)}, "darwin", nil); err == nil {
		t.Fatal("symlink root was admitted")
	}
	const sentinel = "workspace-root-sentinel-do-not-disclose"
	_, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root + "/" + sentinel)}, "darwin", nil)
	if err == nil || strings.Contains(err.Error(), sentinel) {
		t.Fatalf("startup error = %v", err)
	}

	readOnly := testWorkspaceConfig(root)
	readOnly.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}
	readOnly.MaxWriteBytes = 0
	readOnly.MaxOutputBytes = 0
	readRuntime, err := NewRuntime([]*iop.WorkspaceConfig{readOnly}, "darwin", nil)
	if err != nil {
		t.Fatalf("read-only catalog: %v", err)
	}
	_ = readRuntime.Close()

	commandOnly := testWorkspaceConfig(root)
	commandOnly.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND}
	commandOnly.Commands = []*iop.WorkspaceCommandConfig{{Id: "test", Executable: "/usr/bin/true"}}
	commandOnly.MaxReadBytes = 0
	commandOnly.MaxWriteBytes = 0
	commandOnly.MaxOutputBytes = 1
	commandOnly.MaxCommandTimeoutMs = 1
	commandRuntime, err := NewRuntime([]*iop.WorkspaceConfig{commandOnly}, "darwin", nil)
	if err != nil {
		t.Fatalf("command-only catalog: %v", err)
	}
	_ = commandRuntime.Close()
}

func TestRuntimeOpen(t *testing.T) {
	rt, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(t.TempDir())}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = rt.Close() })
	authority := testRequestAuthority("request-1")
	first, err := rt.Open(authority)
	if err != nil {
		t.Fatal(err)
	}
	if first.internalPrefix != ".iop/job/request-1" {
		t.Fatalf("prefix=%q", first.internalPrefix)
	}
	if _, err := rt.Open(authority); err != nil {
		t.Fatalf("idempotent open: %v", err)
	}
	conflict := authority
	conflict.MaxReadBytes = 32
	if _, err := rt.Open(conflict); err != ErrRequestConflict {
		t.Fatalf("conflict=%v", err)
	}
	invalid := authority
	invalid.RequestID = "bad/id"
	if _, err := rt.Open(invalid); err != ErrInvalidRequest {
		t.Fatalf("invalid=%v", err)
	}
	for name, mutate := range map[string]func(*RequestAuthority){
		"operation widening": func(value *RequestAuthority) {
			value.Operations = append(value.Operations, iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND)
		},
		"read limit widening": func(value *RequestAuthority) { value.MaxReadBytes = 65 },
		"disabled limit": func(value *RequestAuthority) {
			value.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}
			value.MaxWriteBytes = 1
			value.MaxOutputBytes = 0
		},
		"unknown command": func(value *RequestAuthority) {
			value.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND}
			value.CommandIDs = []string{"missing"}
			value.MaxReadBytes, value.MaxWriteBytes, value.MaxOutputBytes, value.MaxCommandTimeoutMS = 0, 0, 1, 1
		},
	} {
		t.Run(name, func(t *testing.T) {
			candidate := testRequestAuthority("request-" + strings.ReplaceAll(name, " ", "-"))
			mutate(&candidate)
			if _, err := rt.Open(candidate); err != ErrInvalidRequest {
				t.Fatalf("Open = %v", err)
			}
		})
	}

	lowered := RequestAuthority{
		RequestID: "request-lowered", WorkspaceRef: "mac-workspace",
		Operations:   []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ},
		MaxReadBytes: 32,
	}
	request, err := rt.Open(lowered)
	if err != nil {
		t.Fatalf("lowered read-only authority: %v", err)
	}
	lowered.Operations[0] = iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE
	lowered.MaxReadBytes = 1
	if _, ok := request.operations[iop.WorkspaceOperation_WORKSPACE_OPERATION_READ]; !ok || request.maxRead != 32 {
		t.Fatalf("request authority changed through caller mutation: %+v", request)
	}
	if _, err := first.internalPath(".iop/job/request-2/plan.md"); err == nil {
		t.Fatal("sibling internal path admitted")
	}
	if _, err := first.internalPath(".iop/job/request-1/plan.md"); err != nil {
		t.Fatalf("owned internal path: %v", err)
	}
}

func TestRuntimeCloseAndConcurrentIsolation(t *testing.T) {
	root := t.TempDir()
	rt, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	var group sync.WaitGroup
	for i := 0; i < 32; i++ {
		group.Add(1)
		go func(index int) {
			defer group.Done()
			id := fmt.Sprintf("request-%d", index)
			authority := testRequestAuthority(id)
			if _, err := rt.Open(authority); err != nil {
				t.Errorf("Open(%s): %v", id, err)
			}
		}(i)
	}
	group.Wait()
	if err := rt.Close(); err != nil {
		t.Fatal(err)
	}
	if _, err := rt.Request("request-a"); err != ErrClosed {
		t.Fatalf("Request after close=%v", err)
	}
	if err := rt.Close(); err != nil {
		t.Fatalf("second close: %v", err)
	}
}

func TestRuntimeOpenCommandAuthority(t *testing.T) {
	config := testWorkspaceConfig(t.TempDir())
	config.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND}
	config.Commands = []*iop.WorkspaceCommandConfig{
		{Id: "format", Executable: "/usr/bin/true"},
		{Id: "test", Executable: "/usr/bin/true"},
	}
	config.MaxReadBytes = 0
	config.MaxWriteBytes = 0
	config.MaxOutputBytes = 64
	config.MaxCommandTimeoutMs = 1000
	rt, err := NewRuntime([]*iop.WorkspaceConfig{config}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = rt.Close() })
	authority := RequestAuthority{
		RequestID: "request-command", WorkspaceRef: "mac-workspace",
		Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
		CommandIDs: []string{"test"}, MaxOutputBytes: 32, MaxCommandTimeoutMS: 500,
	}
	request, err := rt.Open(authority)
	if err != nil {
		t.Fatalf("command-only open: %v", err)
	}
	authority.CommandIDs[0] = "format"
	if len(request.commandIDs) != 1 || request.commandIDs[0] != "test" || request.maxCommandTimeout != 500 {
		t.Fatalf("command authority was not copied: %+v", request)
	}
	for name, mutate := range map[string]func(*RequestAuthority){
		"unknown command":  func(value *RequestAuthority) { value.CommandIDs = []string{"unknown"} },
		"missing command":  func(value *RequestAuthority) { value.CommandIDs = nil },
		"output widening":  func(value *RequestAuthority) { value.MaxOutputBytes = 65 },
		"timeout widening": func(value *RequestAuthority) { value.MaxCommandTimeoutMS = 1001 },
	} {
		t.Run(name, func(t *testing.T) {
			candidate := RequestAuthority{
				RequestID: "request-" + strings.ReplaceAll(name, " ", "-"), WorkspaceRef: "mac-workspace",
				Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
				CommandIDs: []string{"test"}, MaxOutputBytes: 32, MaxCommandTimeoutMS: 500,
			}
			mutate(&candidate)
			if _, err := rt.Open(candidate); err != ErrInvalidRequest {
				t.Fatalf("Open = %v", err)
			}
		})
	}
}
