package cli

import (
	"context"
	"iop/apps/node/internal/runtime"
	"iop/packages/go/config"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"
)

func TestCLIOneshotExecutorCwd(t *testing.T) {
	c := New(config.CLIConf{
		Profiles: map[string]config.CLIProfileConf{
			"oneshot-pwd": {
				Command: "pwd",
			},
		},
	}, nil)
	tmpDir := t.TempDir()
	spec := runtime.ExecutionSpec{
		Target:    "oneshot-pwd",
		Workspace: tmpDir,
	}
	sink := &testSink{}
	err := c.Execute(context.Background(), spec, sink)
	if err != nil {
		t.Fatalf("Execute failed: %v", err)
	}

	var output string
	for _, ev := range sink.events {
		if ev.Type == runtime.EventTypeDelta {
			output += ev.Delta
		}
	}
	resolved, err := os.Readlink(tmpDir)
	if err != nil {
		resolved = tmpDir
	}
	if !strings.Contains(output, tmpDir) && !strings.Contains(output, resolved) {
		t.Errorf("expected output to contain workspace path %q, got: %q", tmpDir, output)
	}
}

func TestCLIPersistentNonTerminalCwd(t *testing.T) {
	c := New(config.CLIConf{
		Profiles: map[string]config.CLIProfileConf{
			"persistent-pwd": {
				Command:    "sh",
				Persistent: true,
			},
		},
	}, nil)
	tmpDir := t.TempDir()
	spec := runtime.ExecutionSpec{
		Target:      "persistent-pwd",
		Workspace:   tmpDir,
		SessionID:   "sess-cwd-nt",
		SessionMode: runtime.SessionModeCreateIfMissing,
		Input:       map[string]any{"prompt": "pwd\n"},
	}
	sink := &testSink{}
	err := c.Execute(context.Background(), spec, sink)
	if err != nil {
		t.Fatalf("Execute failed: %v", err)
	}
	defer func() {
		_ = c.Stop(context.Background())
	}()

	var output string
	success := false
	resolved, err := os.Readlink(tmpDir)
	if err != nil {
		resolved = tmpDir
	}
	// Wait up to 2 seconds for output
	for i := 0; i < 20; i++ {
		output = ""
		for _, ev := range sink.events {
			if ev.Type == runtime.EventTypeDelta {
				output += ev.Delta
			}
		}
		if strings.Contains(output, tmpDir) || strings.Contains(output, resolved) {
			success = true
			break
		}
		time.Sleep(100 * time.Millisecond)
	}
	if !success {
		t.Errorf("expected output to contain workspace path %q, got: %q", tmpDir, output)
	}
}

func TestCLIPersistentTerminalCwd(t *testing.T) {
	c := New(config.CLIConf{
		Profiles: map[string]config.CLIProfileConf{
			"persistent-term-pwd": {
				Command:    "sh",
				Persistent: true,
				Terminal:   true,
			},
		},
	}, nil)
	tmpDir := t.TempDir()
	spec := runtime.ExecutionSpec{
		Target:      "persistent-term-pwd",
		Workspace:   tmpDir,
		SessionID:   "sess-cwd-term",
		SessionMode: runtime.SessionModeCreateIfMissing,
		Input:       map[string]any{"prompt": "pwd"},
	}
	sink := &testSink{}
	err := c.Execute(context.Background(), spec, sink)
	if err != nil {
		t.Fatalf("Execute failed: %v", err)
	}
	defer func() {
		_ = c.Stop(context.Background())
	}()

	var output string
	success := false
	resolved, err := os.Readlink(tmpDir)
	if err != nil {
		resolved = tmpDir
	}
	// Wait up to 2 seconds for output
	for i := 0; i < 20; i++ {
		output = ""
		for _, ev := range sink.events {
			if ev.Type == runtime.EventTypeDelta {
				output += ev.Delta
			}
		}
		if strings.Contains(output, tmpDir) || strings.Contains(output, resolved) {
			success = true
			break
		}
		time.Sleep(100 * time.Millisecond)
	}
	if !success {
		t.Errorf("expected output to contain workspace path %q, got: %q", tmpDir, output)
	}
}

func TestCLIWorkspacePreflightFailures(t *testing.T) {
	// 1. prepareWorkspaceDir helper tests
	t.Run("Helper", func(t *testing.T) {
		// Blank workspace
		dir, err := prepareWorkspaceDir("")
		if err != nil || dir != "" {
			t.Errorf("expected empty dir and nil error for blank workspace, got dir=%q, err=%v", dir, err)
		}
		dir, err = prepareWorkspaceDir("   ")
		if err != nil || dir != "" {
			t.Errorf("expected empty dir and nil error for whitespace workspace, got dir=%q, err=%v", dir, err)
		}

		// Not found directory
		nonExistentPath := "/nonexistent/path/for/workspace/test"
		dir, err = prepareWorkspaceDir(nonExistentPath)
		if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace not found") {
			t.Errorf("expected 'workspace not found' error, got dir=%q, err=%v", dir, err)
		}

		// File is not a directory
		tmpFile, err := os.CreateTemp("", "test-workspace-file-*")
		if err != nil {
			t.Fatalf("failed to create temp file: %v", err)
		}
		defer os.Remove(tmpFile.Name())
		_ = tmpFile.Close()

		dir, err = prepareWorkspaceDir(tmpFile.Name())
		if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace is not a directory") {
			t.Errorf("expected 'workspace is not a directory' error, got dir=%q, err=%v", dir, err)
		}

		// Inaccessible directory
		if os.Geteuid() != 0 {
			// Create a directory and make it inaccessible
			inaccessibleDir := t.TempDir() + "/inaccessible"
			if err := os.Mkdir(inaccessibleDir, 0000); err != nil {
				t.Fatalf("failed to create inaccessible dir: %v", err)
			}
			dir, err = prepareWorkspaceDir(inaccessibleDir)
			if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace inaccessible") {
				t.Errorf("expected 'workspace inaccessible' error, got dir=%q, err=%v", dir, err)
			}
			// Restore permission for cleanup
			_ = os.Chmod(inaccessibleDir, 0700)
		} else {
			t.Log("running as root; skipping inaccessible workspace helper test")
		}
	})

	// 2. Execute path preflight error propagation
	t.Run("ExecuteOneshot", func(t *testing.T) {
		c := New(config.CLIConf{
			Profiles: map[string]config.CLIProfileConf{
				"oneshot-pwd": {
					Command: "pwd",
				},
			},
		}, nil)

		spec := runtime.ExecutionSpec{
			RunID:     "oneshot-run-id",
			Target:    "oneshot-pwd",
			Workspace: "/nonexistent/path/for/workspace/test",
		}
		sink := &testSink{}
		err := c.Execute(context.Background(), spec, sink)
		if err == nil {
			t.Fatal("expected execute to fail for nonexistent workspace")
		}
		if !strings.Contains(err.Error(), "cli adapter: workspace not found") {
			t.Errorf("expected 'workspace not found' error, got: %v", err)
		}

		// Verify sink EventTypeError event
		hasErrorEvent := false
		for _, ev := range sink.events {
			if ev.Type == runtime.EventTypeError {
				hasErrorEvent = true
				if ev.RunID != spec.RunID {
					t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
				}
				if !strings.Contains(ev.Error, "cli adapter: workspace not found") {
					t.Errorf("expected EventTypeError Error to contain 'workspace not found', got %q", ev.Error)
				}
			}
		}
		if !hasErrorEvent {
			t.Errorf("expected sink to contain EventTypeError event, but it did not")
		}
	})

	t.Run("ExecutePersistent", func(t *testing.T) {
		c := New(config.CLIConf{
			Profiles: map[string]config.CLIProfileConf{
				"persistent-pwd": {
					Command:    "sh",
					Persistent: true,
				},
			},
		}, nil)

		spec := runtime.ExecutionSpec{
			RunID:       "persistent-run-id",
			Target:      "persistent-pwd",
			Workspace:   "/nonexistent/path/for/workspace/test",
			SessionID:   "sess-preflight-fail",
			SessionMode: runtime.SessionModeCreateIfMissing,
			Input:       map[string]any{"prompt": "pwd\n"},
		}
		sink := &testSink{}
		err := c.Execute(context.Background(), spec, sink)
		if err == nil {
			t.Fatal("expected execute to fail for nonexistent workspace")
		}
		if !strings.Contains(err.Error(), "cli adapter: workspace not found") {
			t.Errorf("expected 'workspace not found' error, got: %v", err)
		}

		// Verify sink EventTypeError event
		hasErrorEvent := false
		for _, ev := range sink.events {
			if ev.Type == runtime.EventTypeError {
				hasErrorEvent = true
				if ev.RunID != spec.RunID {
					t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
				}
				if !strings.Contains(ev.Error, "cli adapter: workspace not found") {
					t.Errorf("expected EventTypeError Error to contain 'workspace not found', got %q", ev.Error)
				}
			}
		}
		if !hasErrorEvent {
			t.Errorf("expected sink to contain EventTypeError event, but it did not")
		}
	})

	t.Run("ExecuteCodexAppServer", func(t *testing.T) {
		c := New(config.CLIConf{
			Profiles: map[string]config.CLIProfileConf{
				"codex-app": {
					Mode:       modeCodexAppServer,
					Command:    "codex",
					Persistent: true,
					Args:       []string{"--app-server"},
				},
			},
		}, nil)

		spec := runtime.ExecutionSpec{
			RunID:       "codex-run-id",
			Target:      "codex-app",
			Workspace:   "/nonexistent/path/for/workspace/test",
			SessionID:   "sess-codex-preflight-fail",
			SessionMode: runtime.SessionModeCreateIfMissing,
			Input:       map[string]any{"prompt": "some prompt"},
		}
		sink := &testSink{}
		err := c.Execute(context.Background(), spec, sink)
		if err == nil {
			t.Fatal("expected execute to fail for nonexistent workspace")
		}
		if !strings.Contains(err.Error(), "cli adapter: start codex app-server") ||
			!strings.Contains(err.Error(), "cli adapter: workspace not found") {
			t.Errorf("expected 'start codex app-server' and 'workspace not found' errors, got: %v", err)
		}

		// Verify sink EventTypeError event
		hasErrorEvent := false
		for _, ev := range sink.events {
			if ev.Type == runtime.EventTypeError {
				hasErrorEvent = true
				if ev.RunID != spec.RunID {
					t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
				}
				if !strings.Contains(ev.Error, "cli adapter: start codex app-server") ||
					!strings.Contains(ev.Error, "cli adapter: workspace not found") {
					t.Errorf("expected EventTypeError Error to contain 'start codex app-server' and 'workspace not found', got %q", ev.Error)
				}
			}
		}
		if !hasErrorEvent {
			t.Errorf("expected sink to contain EventTypeError event, but it did not")
		}
	})

	t.Run("ExecuteOpencodeSSE", func(t *testing.T) {
		c := New(config.CLIConf{
			Profiles: map[string]config.CLIProfileConf{
				"opencode-sse-target": {
					Mode:       modeOpencodeSSE,
					Command:    "opencode",
					Persistent: true,
					Args:       []string{"serve"}, // triggers opencode-sse mode
				},
			},
		}, nil)

		spec := runtime.ExecutionSpec{
			RunID:       "opencode-run-id",
			Target:      "opencode-sse-target",
			Workspace:   "/nonexistent/path/for/workspace/test",
			SessionID:   "sess-opencode-preflight-fail",
			SessionMode: runtime.SessionModeCreateIfMissing,
			Input:       map[string]any{"prompt": "some prompt"},
		}
		sink := &testSink{}
		err := c.Execute(context.Background(), spec, sink)
		if err == nil {
			t.Fatal("expected execute to fail for nonexistent workspace")
		}
		if !strings.Contains(err.Error(), "cli adapter: start opencode server") ||
			!strings.Contains(err.Error(), "cli adapter: workspace not found") {
			t.Errorf("expected 'start opencode server' and 'workspace not found' errors, got: %v", err)
		}

		// Verify sink EventTypeError event
		hasErrorEvent := false
		for _, ev := range sink.events {
			if ev.Type == runtime.EventTypeError {
				hasErrorEvent = true
				if ev.RunID != spec.RunID {
					t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
				}
				if !strings.Contains(ev.Error, "cli adapter: start opencode server") ||
					!strings.Contains(ev.Error, "cli adapter: workspace not found") {
					t.Errorf("expected EventTypeError Error to contain 'start opencode server' and 'workspace not found', got %q", ev.Error)
				}
			}
		}
		if !hasErrorEvent {
			t.Errorf("expected sink to contain EventTypeError event, but it did not")
		}
	})
}

func TestCLIProcessExitFailureRemainsDistinct(t *testing.T) {
	c := New(config.CLIConf{
		Profiles: map[string]config.CLIProfileConf{
			"fail-oneshot": {
				Command: "sh",
				Args:    []string{"-c", "exit 7"},
			},
		},
	}, nil)

	tmpDir := t.TempDir()
	spec := runtime.ExecutionSpec{
		Target:    "fail-oneshot",
		Workspace: tmpDir,
	}
	sink := &testSink{}
	err := c.Execute(context.Background(), spec, sink)
	if err == nil {
		t.Fatal("expected command execution to fail")
	}

	errStr := err.Error()
	// Command failure should NOT look like a workspace error.
	if strings.Contains(errStr, "workspace not found") ||
		strings.Contains(errStr, "workspace is not a directory") ||
		strings.Contains(errStr, "workspace inaccessible") {
		t.Errorf("process exit failure should be distinct from workspace preflight error, got: %q", errStr)
	}
	if !strings.Contains(errStr, "exit status 7") && !strings.Contains(errStr, "exit code 7") {
		t.Errorf("expected error to mention exit status/code 7, got: %q", errStr)
	}
}

// TestBuildCmdAppliesEnvAndCwd locks the pre-refactor dir-before-env order
// for the context one-shot lifecycle: workspace is applied before env, so
// exec.Cmd.Environ() picks up PWD=<workspace> alongside the custom env.
func TestBuildCmdAppliesEnvAndCwd(t *testing.T) {
	tmpDir := t.TempDir()

	subDir := tmpDir + "/sub"
	if err := os.Mkdir(subDir, 0755); err != nil {
		t.Fatalf("failed to create sub dir: %v", err)
	}

	// 1. Happy path: valid command, valid workspace, valid env
	cmd, err := buildCmd(context.Background(), "echo", []string{"hello"}, []string{"FOO=bar"}, subDir)
	if err != nil {
		t.Fatalf("buildCmd failed: %v", err)
	}

	wantCmd := exec.CommandContext(context.Background(), "echo", "hello")
	wantCmd.Dir = subDir
	wantCmd.Env = append(wantCmd.Environ(), "FOO=bar")

	if cmd.Dir != wantCmd.Dir {
		t.Errorf("expected cmd.Dir to be %q, got %q", wantCmd.Dir, cmd.Dir)
	}
	if cmd.Path != wantCmd.Path {
		t.Errorf("expected cmd.Path %q, got %q", wantCmd.Path, cmd.Path)
	}
	if len(cmd.Args) != len(wantCmd.Args) {
		t.Fatalf("expected cmd.Args %v, got %v", wantCmd.Args, cmd.Args)
	}
	for i := range wantCmd.Args {
		if cmd.Args[i] != wantCmd.Args[i] {
			t.Errorf("expected cmd.Args %v, got %v", wantCmd.Args, cmd.Args)
			break
		}
	}
	assertEnvEqual(t, wantCmd.Env, cmd.Env)

	// 2. Invalid workspace path
	_, err = buildCmd(context.Background(), "echo", []string{"hello"}, nil, "/nonexistent/workspace/path")
	if err == nil {
		t.Error("expected buildCmd to fail with invalid workspace")
	}
	if !strings.Contains(err.Error(), "cli adapter: workspace not found") {
		t.Errorf("expected 'workspace not found' error, got %v", err)
	}
}

// TestBuildCmdWithoutContextAppliesEnvAndCwd locks the pre-refactor
// env-before-dir order for the codex app-server, opencode local server, and
// non-terminal persistent lifecycles: env is applied before workspace, so
// exec.Cmd.Environ() keeps the parent PWD instead of being overridden to
// match workspace.
func TestBuildCmdWithoutContextAppliesEnvAndCwd(t *testing.T) {
	tmpDir := t.TempDir()

	subDir := tmpDir + "/sub"
	if err := os.Mkdir(subDir, 0755); err != nil {
		t.Fatalf("failed to create sub dir: %v", err)
	}

	// 1. Happy path: valid command, valid workspace, valid env
	cmd, err := buildCmdWithoutContext("echo", []string{"hello"}, []string{"FOO=bar"}, subDir)
	if err != nil {
		t.Fatalf("buildCmdWithoutContext failed: %v", err)
	}

	wantCmd := exec.Command("echo", "hello")
	wantCmd.Env = append(wantCmd.Environ(), "FOO=bar")
	wantCmd.Dir = subDir

	if cmd.Dir != wantCmd.Dir {
		t.Errorf("expected cmd.Dir to be %q, got %q", wantCmd.Dir, cmd.Dir)
	}
	if cmd.Path != wantCmd.Path {
		t.Errorf("expected cmd.Path %q, got %q", wantCmd.Path, cmd.Path)
	}
	if len(cmd.Args) != len(wantCmd.Args) {
		t.Fatalf("expected cmd.Args %v, got %v", wantCmd.Args, cmd.Args)
	}
	for i := range wantCmd.Args {
		if cmd.Args[i] != wantCmd.Args[i] {
			t.Errorf("expected cmd.Args %v, got %v", wantCmd.Args, cmd.Args)
			break
		}
	}
	assertEnvEqual(t, wantCmd.Env, cmd.Env)

	// cmd.Env must NOT carry PWD=<workspace>: env is computed before Dir is
	// set, so PWD (if present at all) must still match the parent's PWD.
	for _, e := range cmd.Env {
		if e == "PWD="+subDir {
			t.Errorf("expected cmd.Env to not override PWD to workspace %q, got env %v", subDir, cmd.Env)
		}
	}

	// 2. Invalid workspace path
	_, err = buildCmdWithoutContext("echo", []string{"hello"}, nil, "/nonexistent/workspace/path")
	if err == nil {
		t.Error("expected buildCmdWithoutContext to fail with invalid workspace")
	}
	if !strings.Contains(err.Error(), "cli adapter: workspace not found") {
		t.Errorf("expected 'workspace not found' error, got %v", err)
	}
}

func assertEnvEqual(t *testing.T, want, got []string) {
	t.Helper()
	if len(want) != len(got) {
		t.Errorf("expected cmd.Env length %d, got %d\nwant: %v\ngot:  %v", len(want), len(got), want, got)
		return
	}
	for i := range want {
		if want[i] != got[i] {
			t.Errorf("expected cmd.Env[%d] to be %q, got %q\nwant: %v\ngot:  %v", i, want[i], got[i], want, got)
		}
	}
}
