package workspace_test

import (
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/seznam/jailoc/internal/config"
	"github.com/seznam/jailoc/internal/workspace"
)

func TestResolveValidWorkspace(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/home/user/projects"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	if resolved.Name != "default" {
		t.Fatalf("name mismatch: got %q", resolved.Name)
	}
	if resolved.Port != 4096 {
		t.Fatalf("port mismatch: got %d", resolved.Port)
	}
	if len(resolved.Paths) != 1 || resolved.Paths[0] != "/home/user/projects" {
		t.Fatalf("paths mismatch: got %#v", resolved.Paths)
	}
}

func TestResolveNonexistentWorkspace(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{},
	}

	_, err := workspace.Resolve(cfg, "nonexistent")
	if err == nil {
		t.Fatal("expected error for nonexistent workspace")
	}
	if !strings.Contains(err.Error(), "nonexistent") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestResolveFromCWDMatches(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/home/user/projects"},
			},
		},
	}

	resolved, matchedPath, err := workspace.ResolveFromCWD(cfg, "/home/user/projects/foo")
	if err != nil {
		t.Fatalf("ResolveFromCWD returned error: %v", err)
	}
	if resolved.Name != "default" {
		t.Fatalf("workspace mismatch: got %q", resolved.Name)
	}
	if matchedPath != "/home/user/projects" {
		t.Fatalf("matched path mismatch: got %q", matchedPath)
	}
}

func TestResolveFromCWDNoMatch(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/home/user/projects"},
			},
		},
	}

	_, _, err := workspace.ResolveFromCWD(cfg, "/home/user/unrelated")
	if err == nil {
		t.Fatal("expected no match error")
	}
	if !strings.Contains(err.Error(), "no matching workspace") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestResolveFromCWD(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name            string
		cfg             *config.Config
		cwd             string
		wantWorkspace   string
		wantMatchedPath string
		wantErrContains string
	}{
		{
			name: "overlapping paths uses longest prefix",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"broad": {
						Paths: []string{"/a/b"},
					},
					"specific": {
						Paths: []string{"/a/b/c"},
					},
				},
			},
			cwd:             "/a/b/c/d",
			wantWorkspace:   "specific",
			wantMatchedPath: "/a/b/c",
		},
		{
			name: "multiple paths per workspace picks longest matched path",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"multi": {
						Paths: []string{"/a/b", "/a/b/c/d"},
					},
				},
			},
			cwd:             "/a/b/c/d/e",
			wantWorkspace:   "multi",
			wantMatchedPath: "/a/b/c/d",
		},
		{
			name: "equal length tiebreaker uses alphabetical workspace name",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"beta": {
						Paths: []string{"/a/b"},
					},
					"alpha": {
						Paths: []string{"/a/b"},
					},
				},
			},
			cwd:             "/a/b/c",
			wantWorkspace:   "alpha",
			wantMatchedPath: "/a/b",
		},
		{
			name: "segment count wins over string length",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"long-name": {
						Paths: []string{"/a/bb"},
					},
					"deep": {
						Paths: []string{"/a/b/c"},
					},
				},
			},
			cwd:             "/a/b/c/d",
			wantWorkspace:   "deep",
			wantMatchedPath: "/a/b/c",
		},
		{
			name: "single match no overlap unchanged",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"alpha": {
						Paths: []string{"/x/y"},
					},
					"beta": {
						Paths: []string{"/a/b"},
					},
				},
			},
			cwd:             "/x/y/z",
			wantWorkspace:   "alpha",
			wantMatchedPath: "/x/y",
		},
		{
			name: "no-match",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"default": {
						Paths: []string{"/home/user/projects"},
					},
				},
			},
			cwd:             "/unrelated/path",
			wantErrContains: "no matching workspace",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			resolved, matchedPath, err := workspace.ResolveFromCWD(tt.cfg, tt.cwd)

			if tt.wantErrContains != "" {
				if err == nil {
					t.Fatalf("expected error containing %q", tt.wantErrContains)
				}
				if !strings.Contains(err.Error(), tt.wantErrContains) {
					t.Fatalf("unexpected error: %v", err)
				}
				return
			}

			if err != nil {
				t.Fatalf("ResolveFromCWD returned error: %v", err)
			}
			if resolved.Name != tt.wantWorkspace {
				t.Fatalf("workspace mismatch: got %q want %q", resolved.Name, tt.wantWorkspace)
			}
			if matchedPath != tt.wantMatchedPath {
				t.Fatalf("matched path mismatch: got %q want %q", matchedPath, tt.wantMatchedPath)
			}
		})
	}
}

func TestPortAllocationAlphabetical(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"zebra": {},
			"alpha": {},
		},
	}

	if got := workspace.PortForWorkspace(cfg, "alpha"); got != 4096 {
		t.Fatalf("alpha port mismatch: got %d", got)
	}
	if got := workspace.PortForWorkspace(cfg, "zebra"); got != 4097 {
		t.Fatalf("zebra port mismatch: got %d", got)
	}
}

func TestPortAllocationSingleDefault(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {},
		},
	}

	if got := workspace.PortForWorkspace(cfg, "default"); got != 4096 {
		t.Fatalf("default port mismatch: got %d", got)
	}
}

func TestTildeExpansionInPaths(t *testing.T) {
	t.Parallel()

	home, err := os.UserHomeDir()
	if err != nil {
		t.Fatalf("UserHomeDir failed: %v", err)
	}
	want, err := filepath.Abs(filepath.Join(home, "projects"))
	if err != nil {
		t.Fatalf("Abs failed: %v", err)
	}

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"~/projects"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if len(resolved.Paths) != 1 || resolved.Paths[0] != want {
		t.Fatalf("tilde expansion mismatch: got %#v want %q", resolved.Paths, want)
	}
}

func TestPathWithSpaces(t *testing.T) {
	t.Parallel()

	home, err := os.UserHomeDir()
	if err != nil {
		t.Fatalf("UserHomeDir failed: %v", err)
	}
	want, err := filepath.Abs(filepath.Join(home, "My Projects", "foo"))
	if err != nil {
		t.Fatalf("Abs failed: %v", err)
	}

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"~/My Projects/foo"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if len(resolved.Paths) != 1 || resolved.Paths[0] != want {
		t.Fatalf("space path expansion mismatch: got %#v want %q", resolved.Paths, want)
	}
}

func TestBuildContextExpanded(t *testing.T) {
	t.Parallel()

	home, err := os.UserHomeDir()
	if err != nil {
		t.Fatalf("UserHomeDir failed: %v", err)
	}
	want, err := filepath.Abs(filepath.Join(home, "my-build"))
	if err != nil {
		t.Fatalf("Abs failed: %v", err)
	}

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				BuildContext: "~/my-build",
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if resolved.BuildContext != want {
		t.Fatalf("build context mismatch: got %q want %q", resolved.BuildContext, want)
	}
}

func TestBuildContextEmpty(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				BuildContext: "",
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if resolved.BuildContext != "" {
		t.Fatalf("expected empty build context, got %q", resolved.BuildContext)
	}
}

func TestCWDSubdirectoryMatches(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/home/user/projects"},
			},
		},
	}

	resolved, _, err := workspace.ResolveFromCWD(cfg, "/home/user/projects/deep/subdir")
	if err != nil {
		t.Fatalf("ResolveFromCWD returned error: %v", err)
	}
	if resolved.Name != "default" {
		t.Fatalf("workspace mismatch: got %q", resolved.Name)
	}
}

func TestCWDDoesNotMatchPrefix(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/home/user/projects"},
			},
		},
	}

	_, _, err := workspace.ResolveFromCWD(cfg, "/home/user/projectsother")
	if err == nil {
		t.Fatal("expected no match error for prefix-only path")
	}
}

func TestMatchesCWD(t *testing.T) {
	t.Parallel()

	ws := &workspace.Resolved{Paths: []string{"/home/user/projects"}}
	if !workspace.MatchesCWD(ws, "/home/user/projects/x") {
		t.Fatal("expected subdirectory to match")
	}
	if workspace.MatchesCWD(ws, "/home/user/projectsother") {
		t.Fatal("expected prefix-only path not to match")
	}
	if !workspace.MatchesCWD(ws, "/home/user/projects") {
		t.Fatal("expected exact path to match")
	}
}

func TestPortForWorkspaceUnknown(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"alpha": {},
			"beta":  {},
		},
	}

	got := workspace.PortForWorkspace(cfg, "unknown-workspace")
	if got != -1 {
		t.Fatalf("expected -1 for unknown workspace, got %d", got)
	}
}

func TestResolveNilConfig(t *testing.T) {
	t.Parallel()

	_, err := workspace.Resolve(nil, "somename")
	if err == nil {
		t.Fatal("expected error for nil config")
	}
	if !strings.Contains(err.Error(), "not found") {
		t.Fatalf("unexpected error message: %v", err)
	}
}

func TestMatchesCWDNilWorkspace(t *testing.T) {
	t.Parallel()

	got := workspace.MatchesCWD(nil, "/some/path")
	if got {
		t.Fatal("expected false for nil workspace")
	}
}

func TestDockerfileSet(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Dockerfile: "https://example.com/Dockerfile",
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if resolved.Dockerfile != "https://example.com/Dockerfile" {
		t.Fatalf("dockerfile mismatch: got %q", resolved.Dockerfile)
	}
}

func TestDockerfileEmpty(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Dockerfile: "",
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if resolved.Dockerfile != "" {
		t.Fatalf("expected empty dockerfile, got %q", resolved.Dockerfile)
	}
}

func TestResolveEnvGlobalOnly(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Defaults: config.Defaults{
			Env: []string{"GLOBAL_A=1", "GLOBAL_B=2"},
		},
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/tmp"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	if len(resolved.Env) != 2 {
		t.Fatalf("env length mismatch: got %#v", resolved.Env)
	}
	if resolved.Env[0] != "GLOBAL_A=1" || resolved.Env[1] != "GLOBAL_B=2" {
		t.Fatalf("env mismatch: got %#v", resolved.Env)
	}
}

func TestResolveEnvWorkspaceOnly(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/tmp"},
				Env:   []string{"WS_A=1", "WS_B=2"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	if len(resolved.Env) != 2 {
		t.Fatalf("env length mismatch: got %#v", resolved.Env)
	}
	if resolved.Env[0] != "WS_A=1" || resolved.Env[1] != "WS_B=2" {
		t.Fatalf("env mismatch: got %#v", resolved.Env)
	}
}

func TestResolveEnvMergeOverride(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Defaults: config.Defaults{
			Env: []string{"SHARED=global", "GLOBAL_ONLY=1"},
		},
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/tmp"},
				Env:   []string{"SHARED=workspace", "WS_ONLY=1"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	want := []string{"SHARED=workspace", "GLOBAL_ONLY=1", "WS_ONLY=1"}
	if len(resolved.Env) != len(want) {
		t.Fatalf("env length mismatch: got %#v want %#v", resolved.Env, want)
	}
	for i := range want {
		if resolved.Env[i] != want[i] {
			t.Fatalf("env mismatch at %d: got %#v want %#v", i, resolved.Env, want)
		}
	}
}

func TestResolveEnvFile(t *testing.T) {
	t.Parallel()

	envFile, err := os.CreateTemp("", "jailoc-default-*.env")
	if err != nil {
		t.Fatalf("CreateTemp failed: %v", err)
	}
	envFilePath := envFile.Name()
	t.Cleanup(func() { _ = os.Remove(envFilePath) })

	if err := os.WriteFile(envFilePath, []byte("FROM_FILE=ok\nOTHER=two\n"), 0o600); err != nil { //nolint:gosec // test temp file
		t.Fatalf("WriteFile failed: %v", err)
	}

	cfg := &config.Config{
		Defaults: config.Defaults{
			EnvFile: []string{envFilePath},
		},
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/tmp"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	want := []string{"FROM_FILE=ok", "OTHER=two"}
	if len(resolved.Env) != len(want) {
		t.Fatalf("env length mismatch: got %#v want %#v", resolved.Env, want)
	}
	for i := range want {
		if resolved.Env[i] != want[i] {
			t.Fatalf("env mismatch at %d: got %#v want %#v", i, resolved.Env, want)
		}
	}
}

func TestResolveEnvFullPipeline(t *testing.T) {
	t.Parallel()

	globalFile, err := os.CreateTemp("", "jailoc-global-*.env")
	if err != nil {
		t.Fatalf("CreateTemp global env failed: %v", err)
	}
	globalFilePath := globalFile.Name()
	t.Cleanup(func() { _ = os.Remove(globalFilePath) })

	workspaceFile, err := os.CreateTemp("", "jailoc-workspace-*.env")
	if err != nil {
		t.Fatalf("CreateTemp workspace env failed: %v", err)
	}
	workspaceFilePath := workspaceFile.Name()
	t.Cleanup(func() { _ = os.Remove(workspaceFilePath) })

	if err := os.WriteFile(globalFilePath, []byte("B=global-file\nC=global-file\n"), 0o600); err != nil { //nolint:gosec // test temp file
		t.Fatalf("WriteFile global env failed: %v", err)
	}
	if err := os.WriteFile(workspaceFilePath, []byte("C=workspace-file\nD=workspace-file\n"), 0o600); err != nil { //nolint:gosec // test temp file
		t.Fatalf("WriteFile workspace env failed: %v", err)
	}

	cfg := &config.Config{
		Defaults: config.Defaults{
			Env:     []string{"A=global", "B=global-inline"},
			EnvFile: []string{globalFilePath},
		},
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths:   []string{"/tmp"},
				EnvFile: []string{workspaceFilePath},
				Env:     []string{"D=workspace-inline", "A=workspace-inline"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	want := []string{"A=workspace-inline", "B=global-file", "C=workspace-file", "D=workspace-inline"}
	if len(resolved.Env) != len(want) {
		t.Fatalf("env length mismatch: got %#v want %#v", resolved.Env, want)
	}
	for i := range want {
		if resolved.Env[i] != want[i] {
			t.Fatalf("env mismatch at %d: got %#v want %#v", i, resolved.Env, want)
		}
	}
}

func TestResolveEnvFileError(t *testing.T) {
	t.Parallel()

	missingPath := filepath.Join("/tmp", "jailoc-missing-env-file-do-not-create.env")

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths:   []string{"/tmp"},
				EnvFile: []string{missingPath},
			},
		},
	}

	_, err := workspace.Resolve(cfg, "default")
	if err == nil {
		t.Fatal("expected error for missing env_file")
	}
	if !strings.Contains(err.Error(), "resolving env for workspace default") {
		t.Fatalf("unexpected error context: %v", err)
	}
	if !strings.Contains(err.Error(), missingPath) {
		t.Fatalf("expected missing path in error, got: %v", err)
	}
}

func TestResolveEnvFileDedupPaths(t *testing.T) {
	t.Parallel()

	envFile, err := os.CreateTemp("", "jailoc-shared-*.env")
	if err != nil {
		t.Fatalf("CreateTemp failed: %v", err)
	}
	t.Cleanup(func() { _ = os.Remove(envFile.Name()) }) //nolint:gosec // cleaning up temp file created in this test

	if err := os.WriteFile(envFile.Name(), []byte("SHARED=value\n"), 0o600); err != nil { //nolint:gosec // writing to temp file created in this test
		t.Fatalf("WriteFile failed: %v", err)
	}

	cfg := &config.Config{
		Defaults: config.Defaults{
			EnvFile: []string{envFile.Name()},
		},
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths:   []string{"/tmp"},
				EnvFile: []string{envFile.Name()},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	want := []string{"SHARED=value"}
	if len(resolved.Env) != len(want) {
		t.Fatalf("env length mismatch: got %#v want %#v", resolved.Env, want)
	}
	for i := range want {
		if resolved.Env[i] != want[i] {
			t.Fatalf("env mismatch at %d: got %#v want %#v", i, resolved.Env, want)
		}
	}
}

func TestResolveSSHGitFieldsFromDefaults(t *testing.T) {
	t.Parallel()

	boolTrue := true
	cfg := &config.Config{
		Defaults: config.Defaults{
			SSHAuthSock: true,
			GitConfig:   &boolTrue,
		},
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/tmp"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	if !resolved.SSHAuthSock {
		t.Fatal("expected SSHAuthSock = true from defaults")
	}
	if !resolved.GitConfig {
		t.Fatal("expected GitConfig = true from defaults")
	}
}

func TestResolveSSHGitFieldsDefaultsFalse(t *testing.T) {
	t.Parallel()

	boolFalse := false
	cfg := &config.Config{
		Defaults: config.Defaults{
			GitConfig: &boolFalse,
		},
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths: []string{"/tmp"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	if resolved.SSHAuthSock {
		t.Fatal("expected SSHAuthSock = false by default")
	}
	if resolved.GitConfig {
		t.Fatal("expected GitConfig = false when explicitly set false")
	}
}

func TestResolveSSHGitFieldsWorkspaceOverride(t *testing.T) {
	t.Parallel()

	boolTrue := true
	boolFalse := false

	tests := []struct {
		name         string
		defaultSSH   bool
		workspaceSSH *bool
		expectedSSH  bool
		defaultGit   *bool
		workspaceGit *bool
		expectedGit  bool
	}{
		{
			name:         "workspace overrides defaults true to false",
			defaultSSH:   true,
			workspaceSSH: &boolFalse,
			expectedSSH:  false,
			defaultGit:   &boolTrue,
			workspaceGit: &boolFalse,
			expectedGit:  false,
		},
		{
			name:         "workspace overrides defaults false to true",
			defaultSSH:   false,
			workspaceSSH: &boolTrue,
			expectedSSH:  true,
			defaultGit:   &boolFalse,
			workspaceGit: &boolTrue,
			expectedGit:  true,
		},
		{
			name:         "workspace nil inherits defaults",
			defaultSSH:   true,
			workspaceSSH: nil,
			expectedSSH:  true,
			defaultGit:   &boolFalse,
			workspaceGit: nil,
			expectedGit:  false,
		},
		{
			name:         "git_config defaults to true when both nil",
			defaultSSH:   false,
			workspaceSSH: nil,
			expectedSSH:  false,
			defaultGit:   nil,
			workspaceGit: nil,
			expectedGit:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			cfg := &config.Config{
				Defaults: config.Defaults{
					SSHAuthSock: tt.defaultSSH,
					GitConfig:   tt.defaultGit,
				},
				Workspaces: map[string]config.Workspace{
					"test": {
						Paths:       []string{"/tmp"},
						SSHAuthSock: tt.workspaceSSH,
						GitConfig:   tt.workspaceGit,
					},
				},
			}

			resolved, err := workspace.Resolve(cfg, "test")
			if err != nil {
				t.Fatalf("Resolve returned error: %v", err)
			}

			if resolved.SSHAuthSock != tt.expectedSSH {
				t.Fatalf("SSHAuthSock = %v, want %v", resolved.SSHAuthSock, tt.expectedSSH)
			}
			if resolved.GitConfig != tt.expectedGit {
				t.Fatalf("GitConfig = %v, want %v", resolved.GitConfig, tt.expectedGit)
			}
		})
	}
}

func TestResolveExposePortDefaultsTrue(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"test": {Paths: []string{"/tmp"}},
		},
	}

	resolved, err := workspace.Resolve(cfg, "test")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if !resolved.ExposePort {
		t.Fatal("expected ExposePort = true by default")
	}
}

func TestResolveExposePortWorkspaceOverride(t *testing.T) {
	t.Parallel()

	boolFalse := false
	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"test": {
				Paths:      []string{"/tmp"},
				ExposePort: &boolFalse,
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "test")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if resolved.ExposePort {
		t.Fatal("expected ExposePort = false when workspace sets it false")
	}
}

func TestResolveExposePortDefaultsOverride(t *testing.T) {
	t.Parallel()

	boolFalse := false
	cfg := &config.Config{
		Defaults: config.Defaults{
			ExposePort: &boolFalse,
		},
		Workspaces: map[string]config.Workspace{
			"test": {Paths: []string{"/tmp"}},
		},
	}

	resolved, err := workspace.Resolve(cfg, "test")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if resolved.ExposePort {
		t.Fatal("expected ExposePort = false when defaults set it false and workspace leaves it unset")
	}
}

func TestResolveExposePortWorkspaceOverridesDefaults(t *testing.T) {
	t.Parallel()

	boolFalse := false
	boolTrue := true
	cfg := &config.Config{
		Defaults: config.Defaults{
			ExposePort: &boolFalse,
		},
		Workspaces: map[string]config.Workspace{
			"test": {
				Paths:      []string{"/tmp"},
				ExposePort: &boolTrue,
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "test")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}
	if !resolved.ExposePort {
		t.Fatal("expected ExposePort = true when workspace sets it true and defaults set it false")
	}
}

func TestResolveImagePropagation(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name              string
		workspaceImage    string
		defaultsImage     string
		expectedImage     string
		expectedImageDesc string
	}{
		{
			name:              "workspace_image_set",
			workspaceImage:    "ws:1",
			defaultsImage:     "default:2",
			expectedImage:     "ws:1",
			expectedImageDesc: "raw workspace value",
		},
		{
			name:              "workspace_image_empty_defaults_set",
			workspaceImage:    "",
			defaultsImage:     "default:2",
			expectedImage:     "",
			expectedImageDesc: "empty (defaults NOT applied in Resolve)",
		},
		{
			name:              "both_empty",
			workspaceImage:    "",
			defaultsImage:     "",
			expectedImage:     "",
			expectedImageDesc: "both empty",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			cfg := &config.Config{
				Defaults: config.Defaults{
					Image: tt.defaultsImage,
				},
				Workspaces: map[string]config.Workspace{
					"test": {
						Paths: []string{"/tmp"},
						Image: tt.workspaceImage,
					},
				},
			}

			resolved, err := workspace.Resolve(cfg, "test")
			if err != nil {
				t.Fatalf("Resolve returned error: %v", err)
			}

			if resolved.Image != tt.expectedImage {
				t.Fatalf("image mismatch (%s): got %q want %q", tt.expectedImageDesc, resolved.Image, tt.expectedImage)
			}
		})
	}
}

func TestResolveErrorMessages(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		cfg         *config.Config
		workspace   string
		wantSubstrs []string
	}{
		{
			name:        "nil config includes workspace name",
			cfg:         nil,
			workspace:   "myws",
			wantSubstrs: []string{"myws", "not found"},
		},
		{
			name: "nonexistent workspace includes name",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"other": {Paths: []string{"/data"}},
				},
			},
			workspace:   "missing",
			wantSubstrs: []string{"missing", "not found"},
		},
		{
			name: "empty workspaces map includes name",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{},
			},
			workspace:   "default",
			wantSubstrs: []string{"default", "not found"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			_, err := workspace.Resolve(tt.cfg, tt.workspace)
			if err == nil {
				t.Fatal("expected error, got nil")
			}

			msg := err.Error()
			for _, sub := range tt.wantSubstrs {
				if !strings.Contains(msg, sub) {
					t.Errorf("error %q missing expected substring %q", msg, sub)
				}
			}
		})
	}
}

func TestResolveFromCWDErrorMessages(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		cfg         *config.Config
		cwd         string
		wantSubstrs []string
	}{
		{
			name: "no matching workspace includes directory",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"default": {Paths: []string{"/home/user/projects"}},
				},
			},
			cwd:         "/unrelated/path",
			wantSubstrs: []string{"/unrelated/path", "no matching workspace"},
		},
		{
			name: "empty workspaces map includes directory",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{},
			},
			cwd:         "/some/path",
			wantSubstrs: []string{"/some/path", "no matching workspace"},
		},
		{
			name: "all workspaces have empty paths includes directory",
			cfg: &config.Config{
				Workspaces: map[string]config.Workspace{
					"default": {Paths: []string{}},
					"other":   {Paths: []string{}},
				},
			},
			cwd:         "/anywhere",
			wantSubstrs: []string{"/anywhere", "no matching workspace"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			_, _, err := workspace.ResolveFromCWD(tt.cfg, tt.cwd)
			if err == nil {
				t.Fatal("expected error, got nil")
			}

			msg := err.Error()
			for _, sub := range tt.wantSubstrs {
				if !strings.Contains(msg, sub) {
					t.Errorf("error %q missing expected substring %q", msg, sub)
				}
			}
		})
	}
}

func TestResolveCPUMemory(t *testing.T) {
	t.Parallel()

	f64 := func(v float64) *float64 { return &v }
	str := func(v string) *string { return &v }

	tests := []struct {
		name       string
		defCPU     *float64
		defMemory  *string
		wsCPU      *float64
		wsMemory   *string
		wantCPU    float64
		wantMemory string
	}{
		{"workspace overrides defaults cpu", f64(1.0), nil, f64(4.0), nil, 4.0, "4g"},
		{"workspace overrides defaults memory", nil, str("8g"), nil, str("16g"), 2.0, "16g"},
		{"defaults used for cpu when workspace unset", f64(1.0), nil, nil, nil, 1.0, "4g"},
		{"defaults used for memory when workspace unset", nil, str("8g"), nil, nil, 2.0, "8g"},
		{"fallback cpu when both unset", nil, nil, nil, nil, 2.0, "4g"},
		{"fallback memory when both unset", nil, nil, nil, nil, 2.0, "4g"},
		{"workspace cpu without defaults", nil, nil, f64(0.5), nil, 0.5, "4g"},
		{"workspace memory without defaults", nil, nil, nil, str("512m"), 2.0, "512m"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			cfg := &config.Config{
				Defaults: config.Defaults{
					CPU:    tt.defCPU,
					Memory: tt.defMemory,
				},
				Workspaces: map[string]config.Workspace{
					"test": {
						Paths:  []string{"/data"},
						CPU:    tt.wsCPU,
						Memory: tt.wsMemory,
					},
				},
			}

			resolved, err := workspace.Resolve(cfg, "test")
			if err != nil {
				t.Fatalf("Resolve failed: %v", err)
			}

			if resolved.CPU != tt.wantCPU {
				t.Errorf("CPU: got %v, want %v", resolved.CPU, tt.wantCPU)
			}
			if resolved.Memory != tt.wantMemory {
				t.Errorf("Memory: got %q, want %q", resolved.Memory, tt.wantMemory)
			}
		})
	}
}

func TestResolveMountsDefault(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {Paths: []string{"/tmp"}},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	want := []string{
		filepath.Join(home, ".config", "opencode") + ":/home/agent/.config/opencode:rw",
		filepath.Join(home, ".opencode") + ":/home/agent/.opencode:rw",
		filepath.Join(home, ".claude", "transcripts") + ":/home/agent/.claude/transcripts:rw",
		filepath.Join(home, ".agents") + ":/home/agent/.agents:ro",
	}

	if !reflect.DeepEqual(resolved.Mounts, want) {
		t.Fatalf("mounts mismatch: got %#v want %#v", resolved.Mounts, want)
	}
}

func TestResolveMountsOverride(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &config.Config{
		Defaults: config.Defaults{
			Mounts: []string{"~/custom-opencode:/home/agent/.config/opencode:rw"},
		},
		Workspaces: map[string]config.Workspace{
			"default": {Paths: []string{"/tmp"}},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	want := []string{
		filepath.Join(home, "custom-opencode") + ":/home/agent/.config/opencode:rw",
		filepath.Join(home, ".opencode") + ":/home/agent/.opencode:rw",
		filepath.Join(home, ".claude", "transcripts") + ":/home/agent/.claude/transcripts:rw",
		filepath.Join(home, ".agents") + ":/home/agent/.agents:ro",
	}

	if !reflect.DeepEqual(resolved.Mounts, want) {
		t.Fatalf("mounts mismatch: got %#v want %#v", resolved.Mounts, want)
	}
}

func TestResolveMountsRemoval(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &config.Config{
		Workspaces: map[string]config.Workspace{
			"default": {
				Paths:  []string{"/tmp"},
				Mounts: []string{":/home/agent/.opencode"},
			},
		},
	}

	resolved, err := workspace.Resolve(cfg, "default")
	if err != nil {
		t.Fatalf("Resolve returned error: %v", err)
	}

	want := []string{
		filepath.Join(home, ".config", "opencode") + ":/home/agent/.config/opencode:rw",
		filepath.Join(home, ".claude", "transcripts") + ":/home/agent/.claude/transcripts:rw",
		filepath.Join(home, ".agents") + ":/home/agent/.agents:ro",
	}

	if !reflect.DeepEqual(resolved.Mounts, want) {
		t.Fatalf("mounts mismatch: got %#v want %#v", resolved.Mounts, want)
	}
}

func TestResolveMountsMergeError(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{
		Defaults: config.Defaults{
			Mounts: []string{"relative-host:/container:rw"},
		},
		Workspaces: map[string]config.Workspace{
			"default": {Paths: []string{"/tmp"}},
		},
	}

	_, err := workspace.Resolve(cfg, "default")
	if err == nil {
		t.Fatal("expected Resolve to fail when mount merge fails")
	}
	if !strings.Contains(err.Error(), "merge mounts for workspace default") {
		t.Fatalf("unexpected error context: %v", err)
	}
}
