package config

import (
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"testing"
	"time"
)

// Local helpers for pointer-type AgentProfile construction in tests.
func bptr(v bool) *bool                   { return &v }
func sptr(v string) *string               { return &v }
func iptr(v int) *int                     { return &v }
func dptr(v time.Duration) *time.Duration { return &v }

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

	cfg := Default()
	if cfg == nil {
		t.Fatal("Default returned nil")
	}

	wantVaultDir := filepath.Join(mustHomeDir(t), ".openpass")
	if cfg.VaultDir != wantVaultDir {
		t.Fatalf("VaultDir = %q, want %q", cfg.VaultDir, wantVaultDir)
	}
	if cfg.DefaultAgent != "default" {
		t.Fatalf("DefaultAgent = %q, want %q", cfg.DefaultAgent, "default")
	}

	// Built-in profile assertions
	type wantProfile struct {
		approvalMode string
		canWrite     bool
	}
	wantProfiles := map[string]wantProfile{
		"default":     {canWrite: false, approvalMode: "deny"},
		"claude-code": {canWrite: true, approvalMode: "deny"},
		"codex":       {canWrite: false, approvalMode: "deny"},
		"hermes":      {canWrite: true, approvalMode: "deny"},
		"openclaw":    {canWrite: true, approvalMode: "deny"},
		"opencode":    {canWrite: false, approvalMode: "deny"},
	}
	for name, want := range wantProfiles {
		got, ok := cfg.Agents[name]
		if !ok {
			t.Fatalf("missing built-in profile: %s", name)
		}
		if *got.CanWrite != want.canWrite {
			t.Fatalf("profile %q CanWrite = %v, want %v", name, got.CanWrite, want.canWrite)
		}
		if *got.ApprovalMode != want.approvalMode {
			t.Fatalf("profile %q ApprovalMode = %q, want %q", name, *got.ApprovalMode, want.approvalMode)
		}
	}
}

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

	path := writeTempFile(t, []byte("vaultDir: /custom/vault\nagents:\n  claude:\n    allowedPaths:\n      - personal/\n    canWrite: false\n    approvalMode: prompt\n"))

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if cfg.VaultDir != "/custom/vault" {
		t.Fatalf("VaultDir = %q, want %q", cfg.VaultDir, "/custom/vault")
	}
	if _, ok := cfg.Agents["default"]; !ok {
		t.Fatal("default profile should be present when omitted from file")
	}

	got := cfg.Agents["claude"]
	if got.Name != "claude" {
		t.Errorf("Name = %q, want claude", got.Name)
	}
	if got.CanWrite == nil || *got.CanWrite {
		t.Error("CanWrite should be false")
	}
	if got.ApprovalMode == nil || *got.ApprovalMode != "prompt" {
		t.Errorf("ApprovalMode = %v, want prompt", got.ApprovalMode)
	}
}

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

	path := writeTempFile(t, nil)

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if cfg.DefaultAgent != "default" {
		t.Fatalf("DefaultAgent = %q, want %q", cfg.DefaultAgent, "default")
	}
	if _, ok := cfg.Agents["default"]; !ok {
		t.Fatal("missing default agent profile")
	}
}

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

	_, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
	if err == nil {
		t.Fatal("Load() error = nil, want non-nil")
	}
}

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

	path := writeTempFile(t, []byte("vaultDir: [unterminated\n"))
	_, err := Load(path)
	if err == nil {
		t.Fatal("Load() error = nil, want non-nil")
	}
}

func TestSaveWritesToDefaultConfigPath(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &Config{
		VaultDir:       filepath.Join(home, ".openpass"),
		DefaultAgent:   "default",
		SessionTimeout: defaultSessionTimeout,
		Agents: map[string]AgentProfile{
			"default": {
				Name:         "default",
				AllowedPaths: []string{},
				CanWrite:     bptr(false),
				ApprovalMode: StrPtr("none"),
			},
		},
	}

	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	wantPath := filepath.Join(home, ".openpass", "config.yaml")
	if _, err := os.Stat(wantPath); err != nil {
		t.Fatalf("config file missing at %q: %v", wantPath, err)
	}

	loaded, err := Load(wantPath)
	if err != nil {
		t.Fatalf("Load(saved) error = %v", err)
	}
	agent := loaded.Agents["default"]
	if *agent.CanWrite != false || *agent.ApprovalMode != "none" {
		t.Fatalf("saved agent = %+v, want CanWrite=false ApprovalMode=none", agent)
	}
}

func TestSaveCreatesConfigDirectory(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := Default()
	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	if _, err := os.Stat(filepath.Join(home, ".openpass")); err != nil {
		t.Fatalf("config directory missing: %v", err)
	}
}

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

	yaml := "agents:\n  myagent:\n    allowedPaths: [\"*\"]\n    canWrite: true\n    approvalMode: deny\n"
	path := writeTempFile(t, []byte(yaml))

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	agent := cfg.Agents["myagent"]
	if *agent.ApprovalMode != "deny" {
		t.Fatalf("ApprovalMode = %q, want %q", *agent.ApprovalMode, "deny")
	}
}

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

	yaml := "agents:\n  old-agent:\n    allowedPaths: [\"*\"]\n    canWrite: true\n    requireApproval: true\n"
	path := writeTempFile(t, []byte(yaml))

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	agent := cfg.Agents["old-agent"]
	if *agent.ApprovalMode != "prompt" {
		t.Fatalf("ApprovalMode = %q, want %q", *agent.ApprovalMode, "prompt")
	}
}

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

	yaml := "agents:\n  both:\n    allowedPaths: [\"*\"]\n    canWrite: true\n    requireApproval: true\n    approvalMode: none\n"
	path := writeTempFile(t, []byte(yaml))

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	agent := cfg.Agents["both"]
	if *agent.ApprovalMode != "none" {
		t.Fatalf("ApprovalMode = %q, want %q", *agent.ApprovalMode, "none")
	}
}

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

	yaml := "agents:\n  bad:\n    approvalMode: invalid\n"
	path := writeTempFile(t, []byte(yaml))

	_, err := Load(path)
	if err == nil {
		t.Fatal("Load() expected error for invalid approvalMode, got nil")
	}
}

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

	yaml := "agents:\n  restricted:\n    allowedPaths: [\"*\"]\n    canWrite: false\n    redactFields:\n      - totp.secret\n      - password\n      - api.*\n"
	path := writeTempFile(t, []byte(yaml))

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	agent := cfg.Agents["restricted"]
	if len(agent.RedactFields) != 3 {
		t.Fatalf("RedactFields length = %d, want 3", len(agent.RedactFields))
	}
	if agent.RedactFields[0] != "totp.secret" {
		t.Errorf("RedactFields[0] = %q, want %q", agent.RedactFields[0], "totp.secret")
	}
	if agent.RedactFields[1] != "password" {
		t.Errorf("RedactFields[1] = %q, want %q", agent.RedactFields[1], "password")
	}
	if agent.RedactFields[2] != "api.*" {
		t.Errorf("RedactFields[2] = %q, want %q", agent.RedactFields[2], "api.*")
	}
}

func TestSaveWritesRedactFields(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &Config{
		VaultDir:       filepath.Join(home, ".openpass"),
		DefaultAgent:   "default",
		SessionTimeout: defaultSessionTimeout,
		Agents: map[string]AgentProfile{
			"restricted": {
				Name:         "restricted",
				AllowedPaths: []string{"*"},
				CanWrite:     bptr(false),
				ApprovalMode: StrPtr("deny"),
				RedactFields: []string{"totp.secret", "password"},
			},
		},
	}

	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	loaded, err := Load(filepath.Join(home, ".openpass", "config.yaml"))
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	agent := loaded.Agents["restricted"]
	if len(agent.RedactFields) != 2 {
		t.Fatalf("RedactFields length = %d, want 2", len(agent.RedactFields))
	}
	if agent.RedactFields[0] != "totp.secret" || agent.RedactFields[1] != "password" {
		t.Errorf("RedactFields = %v, want [totp.secret password]", agent.RedactFields)
	}
}

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

	profile := newDefaultAgentProfile("test-agent")

	if profile.Name != "test-agent" {
		t.Errorf("Name = %q, want %q", profile.Name, "test-agent")
	}
	if profile.AllowedPaths == nil {
		t.Fatal("AllowedPaths should not be nil")
	}
	if len(profile.AllowedPaths) != 0 {
		t.Errorf("AllowedPaths length = %d, want 0", len(profile.AllowedPaths))
	}
	if profile.CanWrite != nil && *profile.CanWrite {
		t.Error("CanWrite should be false")
	}
	if profile.ApprovalMode == nil || *profile.ApprovalMode != "deny" {
		t.Errorf("ApprovalMode = %v, want deny", profile.ApprovalMode)
	}
}

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

	yaml := `vault:
  path: /my/vault
  default_recipients:
    - age1abc
git:
  auto_push: false
  commit_template: "custom commit"
mcp:
  port: 9090
  bind: "0.0.0.0"
clipboard:
  auto_clear_duration: 60
`
	path := writeTempFile(t, []byte(yaml))

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if cfg.Vault == nil {
		t.Fatal("Vault should not be nil")
	}
	if cfg.Vault.Path != "/my/vault" {
		t.Errorf("Vault.Path = %q, want %q", cfg.Vault.Path, "/my/vault")
	}
	if len(cfg.Vault.DefaultRecipients) != 1 || cfg.Vault.DefaultRecipients[0] != "age1abc" {
		t.Errorf("Vault.DefaultRecipients = %v, want [age1abc]", cfg.Vault.DefaultRecipients)
	}

	if cfg.Git == nil {
		t.Fatal("Git should not be nil")
	}
	if cfg.Git.AutoPush {
		t.Error("Git.AutoPush should be false")
	}
	if cfg.Git.CommitTemplate != "custom commit" {
		t.Errorf("Git.CommitTemplate = %q, want %q", cfg.Git.CommitTemplate, "custom commit")
	}

	if cfg.MCP == nil {
		t.Fatal("MCP should not be nil")
	}
	if cfg.MCP.Port != 9090 {
		t.Errorf("MCP.Port = %d, want %d", cfg.MCP.Port, 9090)
	}
	if cfg.MCP.Bind != "0.0.0.0" {
		t.Errorf("MCP.Bind = %q, want %q", cfg.MCP.Bind, "0.0.0.0")
	}

	if cfg.Clipboard == nil {
		t.Fatal("Clipboard should not be nil")
	}
	if cfg.Clipboard.AutoClearDuration != 60 {
		t.Errorf("Clipboard.AutoClearDuration = %d, want %d", cfg.Clipboard.AutoClearDuration, 60)
	}
}

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

	yaml := `vault:
  path: /only/vault
`
	path := writeTempFile(t, []byte(yaml))

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if cfg.Vault == nil {
		t.Fatal("Vault should not be nil")
	}
	if cfg.Vault.Path != "/only/vault" {
		t.Errorf("Vault.Path = %q, want %q", cfg.Vault.Path, "/only/vault")
	}
}

func TestSaveWithAllConfigSections(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &Config{
		VaultDir:       filepath.Join(home, ".openpass"),
		DefaultAgent:   "default",
		SessionTimeout: defaultSessionTimeout,
		Agents: map[string]AgentProfile{
			"default": {
				Name:         "default",
				AllowedPaths: []string{"*"},
				CanWrite:     bptr(false),
				ApprovalMode: StrPtr("none"),
			},
		},
		Vault: &VaultConfig{
			Path:              "/vault/path",
			DefaultRecipients: []string{"recipient1"},
			ConfirmRemove:     true,
		},
		Git: &GitConfig{
			AutoPush:       false,
			CommitTemplate: "custom",
		},
		MCP: &MCPConfig{
			Port:          9090,
			Bind:          "0.0.0.0",
			Stdio:         true,
			HTTPTokenFile: "/token/path",
		},
		Update: &UpdateConfig{
			CacheTTL: 24 * time.Hour,
		},
		Clipboard: &ClipboardConfig{
			AutoClearDuration: 60,
		},
	}

	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	wantPath := filepath.Join(home, ".openpass", "config.yaml")
	loaded, err := Load(wantPath)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if loaded.Vault == nil {
		t.Fatal("Vault should be saved and loaded")
	}
	if loaded.Vault.Path != "/vault/path" {
		t.Errorf("Vault.Path = %q, want %q", loaded.Vault.Path, "/vault/path")
	}
	if loaded.Vault.DefaultRecipients[0] != "recipient1" {
		t.Errorf("Vault.DefaultRecipients = %v, want [recipient1]", loaded.Vault.DefaultRecipients)
	}

	if loaded.Git == nil {
		t.Fatal("Git should be saved and loaded")
	}
	if !loaded.Git.AutoPush {
		t.Error("Git.AutoPush should be true (saved false is omitted by omitempty)")
	}

	if loaded.MCP == nil {
		t.Fatal("MCP should be saved and loaded")
	}
	if loaded.MCP.Port != 9090 {
		t.Errorf("MCP.Port = %d, want %d", loaded.MCP.Port, 9090)
	}

	if loaded.Clipboard == nil {
		t.Fatal("Clipboard should be saved and loaded")
	}
	if loaded.Clipboard.AutoClearDuration != 60 {
		t.Errorf("Clipboard.AutoClearDuration = %d, want %d", loaded.Clipboard.AutoClearDuration, 60)
	}
}

func TestSaveWithNilConfigReturnsError(t *testing.T) {
	var cfg *Config
	if err := cfg.Save(); err == nil {
		t.Fatal("Save() on nil Config should return error")
	}
}

func TestSaveLoadRoundTrip_PreservesAllFields(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &Config{
		VaultDir:       filepath.Join(home, ".openpass"),
		DefaultAgent:   "test-agent",
		SessionTimeout: defaultSessionTimeout,
		Agents: map[string]AgentProfile{
			"test-agent": {
				Name:            "test-agent",
				AllowedPaths:    []string{"path1", "path2"},
				CanWrite:        bptr(true),
				ApprovalMode:    sptr("prompt"),
				ApprovalTimeout: dptr(2 * time.Minute),
				RequireApproval: bptr(true),
				DynamicProviders: map[string][]string{
					"postgres": {"readonly", "admin"},
					"aws-sts":  {"*"},
				},
			},
		},
		Vault: &VaultConfig{
			Path:              "/vault/path",
			DefaultRecipients: []string{"recipient1", "recipient2"},
			ConfirmRemove:     true,
		},
		MCP: &MCPConfig{
			Port:              9090,
			Bind:              "0.0.0.0",
			Stdio:             true,
			HTTPTokenFile:     "/token/path",
			ReadHeaderTimeout: 7 * time.Second,
			ReadTimeout:       15 * time.Second,
			WriteTimeout:      20 * time.Second,
			ShutdownTimeout:   8 * time.Second,
			ApprovalTimeout:   45 * time.Second,
		},
		Clipboard: &ClipboardConfig{
			AutoClearDuration: 90,
		},
	}

	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	wantPath := filepath.Join(home, ".openpass", "config.yaml")
	loaded, err := Load(wantPath)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	// Verify Vault
	if loaded.Vault == nil {
		t.Fatal("Vault should not be nil after round-trip")
	}
	if loaded.Vault.Path != cfg.Vault.Path {
		t.Errorf("Vault.Path = %q, want %q", loaded.Vault.Path, cfg.Vault.Path)
	}
	if len(loaded.Vault.DefaultRecipients) != len(cfg.Vault.DefaultRecipients) {
		t.Errorf("Vault.DefaultRecipients len = %d, want %d", len(loaded.Vault.DefaultRecipients), len(cfg.Vault.DefaultRecipients))
	}
	if loaded.Vault.ConfirmRemove != cfg.Vault.ConfirmRemove {
		t.Errorf("Vault.ConfirmRemove = %v, want %v", loaded.Vault.ConfirmRemove, cfg.Vault.ConfirmRemove)
	}

	// Verify MCP
	if loaded.MCP == nil {
		t.Fatal("MCP should not be nil after round-trip")
	}
	if loaded.MCP.Port != cfg.MCP.Port {
		t.Errorf("MCP.Port = %d, want %d", loaded.MCP.Port, cfg.MCP.Port)
	}
	if loaded.MCP.Bind != cfg.MCP.Bind {
		t.Errorf("MCP.Bind = %q, want %q", loaded.MCP.Bind, cfg.MCP.Bind)
	}
	if loaded.MCP.Stdio != cfg.MCP.Stdio {
		t.Errorf("MCP.Stdio = %v, want %v", loaded.MCP.Stdio, cfg.MCP.Stdio)
	}
	if loaded.MCP.HTTPTokenFile != cfg.MCP.HTTPTokenFile {
		t.Errorf("MCP.HTTPTokenFile = %q, want %q", loaded.MCP.HTTPTokenFile, cfg.MCP.HTTPTokenFile)
	}
	if loaded.MCP.ReadHeaderTimeout != cfg.MCP.ReadHeaderTimeout {
		t.Errorf("MCP.ReadHeaderTimeout = %v, want %v", loaded.MCP.ReadHeaderTimeout, cfg.MCP.ReadHeaderTimeout)
	}
	if loaded.MCP.ReadTimeout != cfg.MCP.ReadTimeout {
		t.Errorf("MCP.ReadTimeout = %v, want %v", loaded.MCP.ReadTimeout, cfg.MCP.ReadTimeout)
	}
	if loaded.MCP.WriteTimeout != cfg.MCP.WriteTimeout {
		t.Errorf("MCP.WriteTimeout = %v, want %v", loaded.MCP.WriteTimeout, cfg.MCP.WriteTimeout)
	}
	if loaded.MCP.ShutdownTimeout != cfg.MCP.ShutdownTimeout {
		t.Errorf("MCP.ShutdownTimeout = %v, want %v", loaded.MCP.ShutdownTimeout, cfg.MCP.ShutdownTimeout)
	}
	if loaded.MCP.ApprovalTimeout != cfg.MCP.ApprovalTimeout {
		t.Errorf("MCP.ApprovalTimeout = %v, want %v", loaded.MCP.ApprovalTimeout, cfg.MCP.ApprovalTimeout)
	}

	// Verify Clipboard
	if loaded.Clipboard == nil {
		t.Fatal("Clipboard should not be nil after round-trip")
	}
	if loaded.Clipboard.AutoClearDuration != cfg.Clipboard.AutoClearDuration {
		t.Errorf("Clipboard.AutoClearDuration = %d, want %d", loaded.Clipboard.AutoClearDuration, cfg.Clipboard.AutoClearDuration)
	}

	// Verify AgentProfile
	agent, ok := loaded.Agents["test-agent"]
	if !ok {
		t.Fatal("test-agent profile should exist after round-trip")
	}
	if *agent.CanWrite != *cfg.Agents["test-agent"].CanWrite {
		t.Errorf("agent.CanWrite = %v, want %v", *agent.CanWrite, *cfg.Agents["test-agent"].CanWrite)
	}
	if *agent.RequireApproval != *cfg.Agents["test-agent"].RequireApproval {
		t.Errorf("agent.RequireApproval = %v, want %v", agent.RequireApproval, cfg.Agents["test-agent"].RequireApproval)
	}
	if *agent.ApprovalTimeout != *cfg.Agents["test-agent"].ApprovalTimeout {
		t.Errorf("agent.ApprovalTimeout = %v, want %v", agent.ApprovalTimeout, cfg.Agents["test-agent"].ApprovalTimeout)
	}
	if *agent.ApprovalMode != *cfg.Agents["test-agent"].ApprovalMode {
		t.Errorf("agent.ApprovalMode = %q, want %q", *agent.ApprovalMode, *cfg.Agents["test-agent"].ApprovalMode)
	}
	if len(agent.AllowedPaths) != len(cfg.Agents["test-agent"].AllowedPaths) {
		t.Errorf("agent.AllowedPaths len = %d, want %d", len(agent.AllowedPaths), len(cfg.Agents["test-agent"].AllowedPaths))
	}
	// ExposeValueTools gets backward compat default &true after Load()
	if agent.ExposeValueTools == nil || !*agent.ExposeValueTools {
		t.Error("agent.ExposeValueTools should default to true after round-trip")
	}
	if agent.DynamicProviders == nil {
		t.Fatal("agent.DynamicProviders should not be nil after round-trip")
	}
	if len(agent.DynamicProviders) != 2 {
		t.Errorf("agent.DynamicProviders len = %d, want 2", len(agent.DynamicProviders))
	}
	if roles, ok := agent.DynamicProviders["postgres"]; !ok {
		t.Error("agent.DynamicProviders missing postgres provider")
	} else if len(roles) != 2 || roles[0] != "readonly" || roles[1] != "admin" {
		t.Errorf("agent.DynamicProviders['postgres'] = %v, want [readonly admin]", roles)
	}
	if roles, ok := agent.DynamicProviders["aws-sts"]; !ok {
		t.Error("agent.DynamicProviders missing aws-sts provider")
	} else if len(roles) != 1 || roles[0] != "*" {
		t.Errorf("agent.DynamicProviders['aws-sts'] = %v, want [*]", roles)
	}
}

func mustHomeDir(t *testing.T) string {
	t.Helper()
	home, err := os.UserHomeDir()
	if err != nil {
		t.Fatalf("os.UserHomeDir() error = %v", err)
	}
	return home
}

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

	content := []byte(`
mcp:
  rate_limit: 120
update:
  cache_ttl: 12h
`)
	path := writeTempFile(t, content)

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.MCP == nil {
		t.Fatal("MCP config is nil")
	}
	if cfg.MCP.RateLimit != 120 {
		t.Errorf("MCP.RateLimit = %d, want 120", cfg.MCP.RateLimit)
	}
	if cfg.Update == nil {
		t.Fatal("Update config is nil")
	}
	if cfg.Update.CacheTTL != 12*time.Hour {
		t.Errorf("Update.CacheTTL = %v, want 12h", cfg.Update.CacheTTL)
	}
}

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

	path := writeTempFile(t, []byte("{}\n"))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	// When no MCP section is present, cfg.MCP is nil and callers use defaults
	if cfg.MCP != nil {
		t.Errorf("MCP config should be nil when not specified, got %+v", cfg.MCP)
	}
	// When no Update section is present, cfg.Update is nil and callers use defaults
	if cfg.Update != nil {
		t.Errorf("Update config should be nil when not specified, got %+v", cfg.Update)
	}

	// Verify defaults are used when section is explicitly empty
	path2 := writeTempFile(t, []byte("mcp:\n  bind: 127.0.0.1\nupdate:\n  cache_ttl: 0s\n"))
	cfg2, err := Load(path2)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg2.MCP == nil {
		t.Fatal("MCP config is nil")
	}
	if cfg2.MCP.RateLimit != 60 {
		t.Errorf("MCP.RateLimit default = %d, want 60", cfg2.MCP.RateLimit)
	}
	if cfg2.Update == nil {
		t.Fatal("Update config is nil")
	}
	if cfg2.Update.CacheTTL != 0 {
		t.Errorf("Update.CacheTTL = %v, want 0", cfg2.Update.CacheTTL)
	}
}

func writeTempFile(t *testing.T, content []byte) string {
	t.Helper()

	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, content, 0o600); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}
	return path
}

// --- Validation Edge-Case Tests ---

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

	// These paths escape the expected directory via ..
	badPaths := []string{
		"../etc/passwd",
		"foo/../../etc/passwd",
		"foo/bar/../../../root/.ssh",
		"./../../../etc",
		"..",
	}
	for _, p := range badPaths {
		err := validateConfigPath(p)
		if err == nil {
			t.Errorf("validateConfigPath(%q) = nil, want error", p)
		}
	}
}

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

	validPaths := []string{
		"config.yaml",
		"subdir/config.yaml",
		"./config.yaml",
		"foo/bar.yaml",
		"~/.openpass/config.yaml",
	}
	for _, p := range validPaths {
		err := validateConfigPath(p)
		if err != nil {
			t.Errorf("validateConfigPath(%q) = %v, want nil", p, err)
		}
	}
}

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

	// MCP bind is explicitly set to empty string, which should fail
	yaml := `mcp:
  bind: ""
`
	path := writeTempFile(t, []byte(yaml))
	_, err := Load(path)
	if err == nil {
		t.Fatal("Load() with empty MCP bind should return error")
	}
}

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

	// MCP section present but bind explicitly empty string
	yaml := `mcp:
  bind: ""
`
	path := writeTempFile(t, []byte(yaml))
	_, err := Load(path)
	if err == nil {
		t.Fatal("Load() with empty MCP bind should return error")
	}
}

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

	// MCP section with port only, bind defaults to 127.0.0.1 which is non-empty
	yaml := `mcp:
  port: 8080
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.MCP == nil {
		t.Fatal("MCP should not be nil")
	}
	if cfg.MCP.Bind != "127.0.0.1" {
		t.Errorf("MCP.Bind = %q, want 127.0.0.1", cfg.MCP.Bind)
	}
}

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

	testCases := []struct {
		name string
		yaml string
	}{
		{"invalid_mode", `agents:
  test:
    approvalMode: invalid_mode
`},
		{"random_string", `agents:
  test:
    approvalMode: something
`},
		{"numeric", `agents:
  test:
    approvalMode: 123
`},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			path := writeTempFile(t, []byte(tc.yaml))
			_, err := Load(path)
			if err == nil {
				t.Errorf("Load() with approvalMode %q = nil, want error", tc.name)
			}
		})
	}
}

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

	validModes := []string{"none", "deny", "prompt"}
	for _, mode := range validModes {
		yaml := "agents:\n  test:\n    approvalMode: " + mode + "\n"
		path := writeTempFile(t, []byte(yaml))
		cfg, err := Load(path)
		if err != nil {
			t.Errorf("Load() with approvalMode %q = error %v", mode, err)
		} else if *cfg.Agents["test"].ApprovalMode != mode {
			t.Errorf("ApprovalMode = %q, want %q", *cfg.Agents["test"].ApprovalMode, mode)
		}
	}
}

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

	yaml := `agents:
  test:
    requireApproval: true
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if *cfg.Agents["test"].ApprovalMode != "prompt" {
		t.Errorf("ApprovalMode = %q, want prompt", *cfg.Agents["test"].ApprovalMode)
	}
}

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

	yaml := `agents:
  test:
    requireApproval: false
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if *cfg.Agents["test"].ApprovalMode != "none" {
		t.Errorf("ApprovalMode = %q, want none", *cfg.Agents["test"].ApprovalMode)
	}
}

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

	yaml := `agents:
  test:
    requireApproval: true
    approvalMode: deny
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if *cfg.Agents["test"].ApprovalMode != "deny" {
		t.Errorf("ApprovalMode = %q, want deny (approvalMode should take precedence)", *cfg.Agents["test"].ApprovalMode)
	}
}

// --- Default Value Tests ---

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

	cfg := Default()

	wantProfiles := []string{"default", "claude-code", "codex", "hermes", "openclaw", "opencode"}
	for _, name := range wantProfiles {
		if _, ok := cfg.Agents[name]; !ok {
			t.Errorf("missing built-in profile: %s", name)
		}
	}
}

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

	cfg := Default()
	if cfg.SessionTimeout != defaultSessionTimeout {
		t.Errorf("SessionTimeout = %v, want %v", cfg.SessionTimeout, defaultSessionTimeout)
	}
}

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

	cfg := Default()
	home, err := os.UserHomeDir()
	if err != nil {
		t.Skip("cannot determine home dir")
	}
	expectedPrefix := home + string(filepath.Separator)
	if cfg.VaultDir[:len(expectedPrefix)] != expectedPrefix {
		t.Errorf("VaultDir = %q, want prefix %q", cfg.VaultDir, expectedPrefix)
	}
}

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

	// When a referenced defaultAgent doesn't exist in the file,
	// Load should create it
	yaml := `defaultAgent: nonexistent
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if _, ok := cfg.Agents["nonexistent"]; !ok {
		t.Error("nonexistent agent should be auto-created")
	}
	if cfg.DefaultAgent != "nonexistent" {
		t.Errorf("DefaultAgent = %q, want nonexistent", cfg.DefaultAgent)
	}
}

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

	// Even with empty config, Agents map should be initialized
	path := writeTempFile(t, []byte("{}\n"))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.Agents == nil {
		t.Fatal("Agents should not be nil")
	}
	if len(cfg.Agents) == 0 {
		t.Error("Agents should contain built-in profiles")
	}
}

// --- File Permission/Error Tests ---

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

	// Path in a directory that doesn't exist
	nonExistentPath := filepath.Join(t.TempDir(), "does", "not", "exist", "config.yaml")
	_, err := Load(nonExistentPath)
	if err == nil {
		t.Fatal("Load() on nonexistent path should return error")
	}
}

func TestSave_PermissionDeniedOnReadOnlyDir(t *testing.T) {
	t.Skip("Skipping: root-owned temp dir not reliable on macOS")

	// Create a read-only directory
	home := t.TempDir()
	readonlyDir := filepath.Join(home, "readonly")
	if err := os.MkdirAll(readonlyDir, 0o444); err != nil {
		t.Skip("cannot create readonly dir")
	}
	t.Setenv("HOME", home)

	cfg := &Config{
		VaultDir:       filepath.Join(readonlyDir, ".openpass"),
		DefaultAgent:   "default",
		SessionTimeout: defaultSessionTimeout,
		Agents:         builtinAgentProfiles(),
	}

	err := cfg.Save()
	if err == nil {
		t.Fatal("Save() to read-only directory should fail")
	}
}

func TestSave_FilePermissionDenied(t *testing.T) {
	t.Skip("Skipping: root-owned temp dir not reliable on macOS")

	home := t.TempDir()
	readonlyDir := filepath.Join(home, ".openpass")
	if err := os.MkdirAll(readonlyDir, 0o500); err != nil {
		t.Skip("cannot create dir with restricted perms")
	}
	t.Setenv("HOME", home)

	// Create a file inside that's read-only
	configPath := filepath.Join(readonlyDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte("test: true\n"), 0o400); err != nil {
		t.Skip("cannot create read-only file")
	}

	cfg := &Config{
		VaultDir:       readonlyDir,
		DefaultAgent:   "default",
		SessionTimeout: defaultSessionTimeout,
		Agents:         builtinAgentProfiles(),
	}

	err := cfg.Save()
	if err == nil {
		t.Fatal("Save() to read-only file should fail")
	}
}

// --- Nested Config Structure Tests ---

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

	yaml := `mcp:
  port: 9090
  bind: "0.0.0.0"
  read_header_timeout: 3s
  read_timeout: 15s
  write_timeout: 20s
  shutdown_timeout: 8s
  approval_timeout: 45s
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.MCP == nil {
		t.Fatal("MCP should not be nil")
	}
	if cfg.MCP.ReadHeaderTimeout != 3*time.Second {
		t.Errorf("MCP.ReadHeaderTimeout = %v, want 3s", cfg.MCP.ReadHeaderTimeout)
	}
	if cfg.MCP.ReadTimeout != 15*time.Second {
		t.Errorf("MCP.ReadTimeout = %v, want 15s", cfg.MCP.ReadTimeout)
	}
	if cfg.MCP.WriteTimeout != 20*time.Second {
		t.Errorf("MCP.WriteTimeout = %v, want 20s", cfg.MCP.WriteTimeout)
	}
	if cfg.MCP.ShutdownTimeout != 8*time.Second {
		t.Errorf("MCP.ShutdownTimeout = %v, want 8s", cfg.MCP.ShutdownTimeout)
	}
	if cfg.MCP.ApprovalTimeout != 45*time.Second {
		t.Errorf("MCP.ApprovalTimeout = %v, want 45s", cfg.MCP.ApprovalTimeout)
	}
}

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

	yaml := `vault:
  path: /custom/vault
  default_recipients:
    - age1recipient1
    - age1recipient2
  confirm_remove: true
  useTouchID: true
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.Vault == nil {
		t.Fatal("Vault should not be nil")
	}
	if cfg.Vault.Path != "/custom/vault" {
		t.Errorf("Vault.Path = %q, want /custom/vault", cfg.Vault.Path)
	}
	if len(cfg.Vault.DefaultRecipients) != 2 {
		t.Errorf("Vault.DefaultRecipients len = %d, want 2", len(cfg.Vault.DefaultRecipients))
	}
	if !cfg.Vault.ConfirmRemove {
		t.Error("Vault.ConfirmRemove should be true")
	}
	if !cfg.Vault.UseTouchID {
		t.Error("Vault.UseTouchID should be true")
	}
}

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

	yaml := `git:
  auto_push: false
  commit_template: "Custom: {{.Date}} {{.Message}}"
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.Git == nil {
		t.Fatal("Git should not be nil")
	}
	if cfg.Git.AutoPush {
		t.Error("Git.AutoPush should be false")
	}
	if cfg.Git.CommitTemplate != "Custom: {{.Date}} {{.Message}}" {
		t.Errorf("Git.CommitTemplate = %q", cfg.Git.CommitTemplate)
	}
}

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

	yaml := `update:
  cache_ttl: 48h
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.Update == nil {
		t.Fatal("Update should not be nil")
	}
	if cfg.Update.CacheTTL != 48*time.Hour {
		t.Errorf("Update.CacheTTL = %v, want 48h", cfg.Update.CacheTTL)
	}
}

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

	yaml := `clipboard:
  auto_clear_duration: 0
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.Clipboard == nil {
		t.Fatal("Clipboard should not be nil")
	}
	if cfg.Clipboard.AutoClearDuration != 0 {
		t.Errorf("Clipboard.AutoClearDuration = %d, want 0 (disabled)", cfg.Clipboard.AutoClearDuration)
	}
}

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

	yaml := `vaultDir: /global/vault
defaultAgent: claude-code
sessionTimeout: 30m
useTouchID: true
agents:
  claude-code:
    allowedPaths:
      - "work/*"
      - "personal/*"
    canWrite: true
    approvalMode: none
vault:
  path: /global/vault
  default_recipients:
    - age1global
git:
  auto_push: true
mcp:
  port: 9090
  bind: "0.0.0.0"
update:
  cache_ttl: 12h
clipboard:
  auto_clear_duration: 60
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if cfg.VaultDir != "/global/vault" {
		t.Errorf("VaultDir = %q, want /global/vault", cfg.VaultDir)
	}
	if cfg.DefaultAgent != "claude-code" {
		t.Errorf("DefaultAgent = %q, want claude-code", cfg.DefaultAgent)
	}
	if cfg.SessionTimeout != 30*time.Minute {
		t.Errorf("SessionTimeout = %v, want 30m", cfg.SessionTimeout)
	}
	if cfg.UseTouchID == nil || !*cfg.UseTouchID {
		t.Error("UseTouchID should be true")
	}

	agent := cfg.Agents["claude-code"]
	if len(agent.AllowedPaths) != 2 {
		t.Errorf("AllowedPaths len = %d, want 2", len(agent.AllowedPaths))
	}
	if agent.CanWrite == nil || !*agent.CanWrite {
		t.Error("CanWrite should be true")
	}

	if cfg.Vault == nil || cfg.Vault.Path != "/global/vault" {
		t.Error("Vault not properly loaded")
	}
	if cfg.Git == nil || !cfg.Git.AutoPush {
		t.Error("Git not properly loaded")
	}
	if cfg.MCP == nil || cfg.MCP.Port != 9090 {
		t.Error("MCP not properly loaded")
	}
	if cfg.Update == nil || cfg.Update.CacheTTL != 12*time.Hour {
		t.Error("Update not properly loaded")
	}
	if cfg.Clipboard == nil || cfg.Clipboard.AutoClearDuration != 60 {
		t.Error("Clipboard not properly loaded")
	}
}

// --- MCP Deprecated Field Test ---

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

	defaults := defaultMCPConfig()
	fileCfg := MCPConfig{
		ApprovalRequired: true,
	}
	// Should not error, just print warning
	result := defaults
	MergeFromMCP(&result, fileCfg)
	if result.ApprovalRequired {
		t.Error("ApprovalRequired should not be set in result (deprecated, ignored)")
	}
}

// --- Update Section Missing Field Test ---

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

	defaults := defaultUpdateConfig()
	// File config with nil CacheTTL (section present but field omitted)
	fileCfg := &UpdateConfig{
		CacheTTL: 0,
	}
	result := defaults
	MergeFromUpdate(&result, *fileCfg)

	// Should keep the default value
	if result.CacheTTL != defaults.CacheTTL {
		t.Errorf("CacheTTL = %v, want default %v", result.CacheTTL, defaults.CacheTTL)
	}
}

// --- SessionTimeout Edge Cases ---

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

	// Zero or negative session timeout in file should be ignored (kept at default)
	yaml := `sessionTimeout: -5m
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	// Should still be default since -5m < 0 is ignored
	if cfg.SessionTimeout != defaultSessionTimeout {
		t.Errorf("SessionTimeout = %v, want %v (negative should be ignored)", cfg.SessionTimeout, defaultSessionTimeout)
	}
}

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

	yaml := `sessionTimeout: 0s
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	// Zero should also be ignored (line 115: raw.SessionTimeout > 0)
	if cfg.SessionTimeout != defaultSessionTimeout {
		t.Errorf("SessionTimeout = %v, want %v (zero should be ignored)", cfg.SessionTimeout, defaultSessionTimeout)
	}
}

// --- UseTouchID Tests ---

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

	yaml := `useTouchID: true
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.UseTouchID == nil || !*cfg.UseTouchID {
		t.Error("UseTouchID should be true")
	}
}

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

	yaml := `useTouchID: false
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.UseTouchID != nil && *cfg.UseTouchID {
		t.Error("UseTouchID should be false")
	}
}

// --- Agent AllowedPaths Tests ---

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

	yaml := `agents:
  test:
    allowedPaths: ~
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	// When allowedPaths is null in YAML, it should become empty slice
	agent := cfg.Agents["test"]
	if agent.AllowedPaths == nil {
		t.Error("AllowedPaths should not be nil (should be empty slice)")
	}
	if len(agent.AllowedPaths) != 0 {
		t.Errorf("AllowedPaths len = %d, want 0", len(agent.AllowedPaths))
	}
}

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

	yaml := `agents:
  test:
    allowedPaths:
      - "work/*"
      - "personal/*"
      - "shared/*"
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent := cfg.Agents["test"]
	if len(agent.AllowedPaths) != 3 {
		t.Errorf("AllowedPaths len = %d, want 3", len(agent.AllowedPaths))
	}
}

// --- DefaultConfigPath Tests ---

func TestDefaultConfigPath_ReturnsExpectedPath(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	path, err := defaultConfigPath()
	if err != nil {
		t.Fatalf("defaultConfigPath() error = %v", err)
	}

	expected := filepath.Join(home, ".openpass", "config.yaml")
	if path != expected {
		t.Errorf("defaultConfigPath() = %q, want %q", path, expected)
	}
}

// --- Empty Config File Tests ---

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

	path := writeTempFile(t, []byte("   \n\t\n  \n"))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	// Should return defaults for everything
	if cfg.VaultDir == "" {
		t.Error("VaultDir should not be empty")
	}
	if cfg.DefaultAgent != "default" {
		t.Errorf("DefaultAgent = %q, want default", cfg.DefaultAgent)
	}
}

// --- MergeFromMCP full timeout coverage ---

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

	defaults := defaultMCPConfig()
	readHeaderTimeout := 3 * time.Second
	readTimeout := 15 * time.Second
	writeTimeout := 20 * time.Second
	shutdownTimeout := 8 * time.Second
	approvalTimeout := 45 * time.Second

	fileCfg := MCPConfig{
		ReadHeaderTimeout: readHeaderTimeout,
		ReadTimeout:       readTimeout,
		WriteTimeout:      writeTimeout,
		ShutdownTimeout:   shutdownTimeout,
		ApprovalTimeout:   approvalTimeout,
	}
	result := defaults
	MergeFromMCP(&result, fileCfg)

	if result.ReadHeaderTimeout != 3*time.Second {
		t.Errorf("ReadHeaderTimeout = %v, want 3s", result.ReadHeaderTimeout)
	}
	if result.ReadTimeout != 15*time.Second {
		t.Errorf("ReadTimeout = %v, want 15s", result.ReadTimeout)
	}
	if result.WriteTimeout != 20*time.Second {
		t.Errorf("WriteTimeout = %v, want 20s", result.WriteTimeout)
	}
	if result.ShutdownTimeout != 8*time.Second {
		t.Errorf("ShutdownTimeout = %v, want 8s", result.ShutdownTimeout)
	}
	if result.ApprovalTimeout != 45*time.Second {
		t.Errorf("ApprovalTimeout = %v, want 45s", result.ApprovalTimeout)
	}
}

// --- Validate() Tests ---

func TestValidate_ValidConfigPasses(t *testing.T) {
	t.Parallel()
	cfg := Default()
	if err := cfg.Validate(); err != nil {
		t.Fatalf("Validate() on default config = %v, want nil", err)
	}
}

func TestValidate_EmptyVaultDirFails(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.VaultDir = ""
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with empty VaultDir = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "vaultDir") {
		t.Errorf("error = %q, should mention vaultDir", errStr)
	}
	if !strings.Contains(errStr, "OPENPASS_VAULT") {
		t.Errorf("error = %q, should mention OPENPASS_VAULT environment variable", errStr)
	}
	if !strings.Contains(errStr, "config.yaml") {
		t.Errorf("error = %q, should mention config.yaml", errStr)
	}
}

func TestValidate_ZeroSessionTimeoutFails(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.SessionTimeout = 0
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with zero SessionTimeout = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "sessionTimeout") {
		t.Errorf("error = %q, should mention sessionTimeout", errStr)
	}
	if !strings.Contains(errStr, "default") {
		t.Errorf("error = %q, should mention default", errStr)
	}
}

func TestValidate_MissingDefaultAgentFails(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.DefaultAgent = "nonexistent"
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with missing defaultAgent = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "defaultAgent") {
		t.Errorf("error = %q, should mention defaultAgent", errStr)
	}
	if !strings.Contains(errStr, "agent profile") && !strings.Contains(errStr, "agents") {
		t.Errorf("error = %q, should hint about defining an agent profile", errStr)
	}
}

func TestValidate_InvalidApprovalModeFails(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Agents["test"] = AgentProfile{
		Name:         "test",
		AllowedPaths: []string{"*"},
		ApprovalMode: StrPtr("invalid"),
	}
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with invalid approvalMode = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "approvalMode") {
		t.Errorf("error = %q, should mention approvalMode", errStr)
	}
	if !strings.Contains(errStr, "none, deny, prompt, auto") {
		t.Errorf("error = %q, should list valid approval modes", errStr)
	}
}

func TestValidate_InvalidGlobPatternFails(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Agents["test"] = AgentProfile{
		Name:         "test",
		AllowedPaths: []string{"["}, // invalid glob
		ApprovalMode: StrPtr("none"),
	}
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with invalid glob = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "allowedPaths") {
		t.Errorf("error = %q, should mention allowedPaths", errStr)
	}
	if !strings.Contains(errStr, "glob") {
		t.Errorf("error = %q, should mention glob pattern syntax", errStr)
	}
}

func TestValidate_MultipleErrorsAggregated(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.VaultDir = ""
	cfg.SessionTimeout = 0
	cfg.DefaultAgent = "missing"
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with multiple errors = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "vaultDir") {
		t.Error("missing vaultDir error")
	}
	if !strings.Contains(errStr, "sessionTimeout") {
		t.Error("missing sessionTimeout error")
	}
	if !strings.Contains(errStr, "defaultAgent") {
		t.Error("missing defaultAgent error")
	}
	if !strings.Contains(errStr, "OPENPASS_VAULT") {
		t.Error("hint for vaultDir should mention OPENPASS_VAULT")
	}
}

func TestValidate_AcceptsAllValidApprovalModes(t *testing.T) {
	t.Parallel()
	validModes := []string{"none", "deny", "prompt", "auto"}
	for _, mode := range validModes {
		cfg := Default()
		cfg.Agents["test"] = AgentProfile{
			Name:         "test",
			AllowedPaths: []string{"*"},
			ApprovalMode: sptr(mode),
		}
		if err := cfg.Validate(); err != nil {
			t.Errorf("Validate() with approvalMode %q = %v, want nil", mode, err)
		}
	}
}

func TestValidate_NegativeAuditMaxFileSizeFails(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Audit = &AuditConfig{MaxFileSize: -1}
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with negative MaxFileSize = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "audit.maxFileSize") {
		t.Errorf("error = %q, should mention audit.maxFileSize", errStr)
	}
	if !strings.Contains(errStr, "greater than 0") {
		t.Errorf("error = %q, should mention required value", errStr)
	}
}

func TestValidate_NegativeClipboardAutoClearFails(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Clipboard = &ClipboardConfig{AutoClearDuration: -1}
	err := cfg.Validate()
	if err == nil {
		t.Fatal("Validate() with negative AutoClearDuration = nil, want error")
	}
	errStr := err.Error()
	if !strings.Contains(errStr, "clipboard.autoClearDuration") {
		t.Errorf("error = %q, should mention clipboard.autoClearDuration", errStr)
	}
	if !strings.Contains(errStr, "non-negative") {
		t.Errorf("error = %q, should mention required value", errStr)
	}
}

func TestValidate_ZeroClipboardAutoClearPasses(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Clipboard = &ClipboardConfig{AutoClearDuration: 0}
	if err := cfg.Validate(); err != nil {
		t.Fatalf("Validate() with zero AutoClearDuration = %v, want nil", err)
	}
}

// --- Profile Tests ---

func TestLoad_Profiles(t *testing.T) {
	t.Parallel()
	yaml := `profiles:
  work:
    vault: ~/.openpass-work
  family:
    vault: ~/vaults/family
defaultProfile: work
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if len(cfg.Profiles) != 2 {
		t.Fatalf("Profiles len = %d, want 2", len(cfg.Profiles))
	}
	if cfg.DefaultProfile != "work" {
		t.Errorf("DefaultProfile = %q, want work", cfg.DefaultProfile)
	}
	work := cfg.ProfileForName("work")
	if work == nil {
		t.Fatal("ProfileForName(work) = nil")
	}
	if work.VaultPath != "~/.openpass-work" {
		t.Errorf("work.VaultPath = %q, want ~/.openpass-work", work.VaultPath)
	}
}

func TestLoad_ProfileMissingVaultPath(t *testing.T) {
	t.Parallel()
	yaml := `profiles:
  empty:
    vault: ""
`
	path := writeTempFile(t, []byte(yaml))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	empty := cfg.ProfileForName("empty")
	if empty == nil {
		t.Fatal("ProfileForName(empty) = nil")
	}
	if empty.VaultPath != "" {
		t.Errorf("empty.VaultPath = %q, want empty", empty.VaultPath)
	}
}

func TestLoad_NoProfiles(t *testing.T) {
	t.Parallel()
	path := writeTempFile(t, []byte("{}\n"))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if cfg.Profiles != nil && len(cfg.Profiles) != 0 {
		t.Errorf("Profiles = %v, want nil or empty", cfg.Profiles)
	}
}

func TestSave_Profiles(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := Default()
	cfg.Profiles = map[string]*Profile{
		"work": {VaultPath: "~/.openpass-work"},
	}
	cfg.DefaultProfile = "work"

	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	loaded, err := Load(filepath.Join(home, ".openpass", "config.yaml"))
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if len(loaded.Profiles) != 1 {
		t.Fatalf("Profiles len = %d, want 1", len(loaded.Profiles))
	}
	if loaded.DefaultProfile != "work" {
		t.Errorf("DefaultProfile = %q, want work", loaded.DefaultProfile)
	}
	work := loaded.ProfileForName("work")
	if work == nil || work.VaultPath != "~/.openpass-work" {
		t.Errorf("work profile = %v, want VaultPath=~/.openpass-work", work)
	}
}

func TestVaultDirForProfile(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Profiles = map[string]*Profile{
		"work": {VaultPath: "~/.openpass-work"},
	}
	if got := cfg.VaultDirForProfile("work"); got != "~/.openpass-work" {
		t.Errorf("VaultDirForProfile(work) = %q, want ~/.openpass-work", got)
	}
	if got := cfg.VaultDirForProfile("missing"); got != "" {
		t.Errorf("VaultDirForProfile(missing) = %q, want empty", got)
	}
}

func TestProfileForName_NilProfiles(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Profiles = nil
	if got := cfg.ProfileForName("work"); got != nil {
		t.Errorf("ProfileForName with nil Profiles = %v, want nil", got)
	}
}

func TestSetAuthMethod_Passphrase(t *testing.T) {
	t.Parallel()
	cfg := Default()
	if err := cfg.SetAuthMethod("passphrase"); err != nil {
		t.Fatalf("SetAuthMethod(passphrase): %v", err)
	}
	if cfg.AuthMethod != AuthMethodPassphrase {
		t.Errorf("AuthMethod = %q, want %q", cfg.AuthMethod, AuthMethodPassphrase)
	}
	if cfg.UseTouchID != nil && *cfg.UseTouchID {
		t.Error("expected UseTouchID=false for passphrase method")
	}
}

func TestSetAuthMethod_TouchID(t *testing.T) {
	t.Parallel()
	cfg := Default()
	if err := cfg.SetAuthMethod("touch-id"); err != nil {
		t.Fatalf("SetAuthMethod(touch-id): %v", err)
	}
	if cfg.AuthMethod != AuthMethodTouchID {
		t.Errorf("AuthMethod = %q, want %q", cfg.AuthMethod, AuthMethodTouchID)
	}
	if cfg.UseTouchID == nil || !*cfg.UseTouchID {
		t.Error("expected UseTouchID=true for touch-id method")
	}
}

func TestSetAuthMethod_SetsVaultSection(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Vault = &VaultConfig{}
	if err := cfg.SetAuthMethod("passphrase"); err != nil {
		t.Fatalf("SetAuthMethod: %v", err)
	}
	if cfg.Vault.AuthMethod != AuthMethodPassphrase {
		t.Errorf("Vault.AuthMethod = %q, want %q", cfg.Vault.AuthMethod, AuthMethodPassphrase)
	}
}

func TestSetAuthMethod_InvalidMethod(t *testing.T) {
	t.Parallel()
	cfg := Default()
	if err := cfg.SetAuthMethod("invalid-method"); err == nil {
		t.Error("expected error for invalid auth method, got nil")
	}
}

func TestIsLegacyMode_NilConfig(t *testing.T) {
	t.Parallel()
	var cfg *Config
	if !cfg.IsLegacyMode() {
		t.Error("expected IsLegacyMode=true for nil config")
	}
}

func TestIsLegacyMode_NilVault(t *testing.T) {
	t.Parallel()
	cfg := Default()
	cfg.Vault = nil
	if !cfg.IsLegacyMode() {
		t.Error("expected IsLegacyMode=true when vault config is nil")
	}
}

func TestIsLegacyMode_ExplicitTrue(t *testing.T) {
	t.Parallel()
	cfg := Default()
	legacy := true
	cfg.Vault = &VaultConfig{LegacyMode: &legacy}
	if !cfg.IsLegacyMode() {
		t.Error("expected IsLegacyMode=true when explicitly set")
	}
}

func TestIsLegacyMode_ExplicitFalse(t *testing.T) {
	t.Parallel()
	cfg := Default()
	legacy := false
	cfg.Vault = &VaultConfig{LegacyMode: &legacy}
	if cfg.IsLegacyMode() {
		t.Error("expected IsLegacyMode=false when explicitly disabled")
	}
}

func TestDefaultAuditConfig(t *testing.T) {
	t.Parallel()
	ac := defaultAuditConfig()
	if ac.MaxFileSize <= 0 {
		t.Error("expected positive MaxFileSize")
	}
	if ac.MaxBackups <= 0 {
		t.Error("expected positive MaxBackups")
	}
	if ac.MaxAgeDays <= 0 {
		t.Error("expected positive MaxAgeDays")
	}
}

func TestDefaultLoggingConfig(t *testing.T) {
	t.Parallel()
	lc := defaultLoggingConfig()
	if lc.Level == "" {
		t.Error("expected non-empty Level")
	}
	if lc.Format == "" {
		t.Error("expected non-empty Format")
	}
}

func TestMergeFromAudit_Nil(t *testing.T) {
	t.Parallel()
	defaults := defaultAuditConfig()
	result := defaults
	if result.MaxFileSize != defaults.MaxFileSize {
		t.Errorf("expected defaults preserved, got %d", result.MaxFileSize)
	}
}

func TestMergeFromAudit_Override(t *testing.T) {
	t.Parallel()
	fileCfg := AuditConfig{
		MaxFileSize: 50 * 1024 * 1024,
		MaxBackups:  3,
		MaxAgeDays:  7,
	}
	defaults := defaultAuditConfig()
	result := defaults
	MergeFromAudit(&result, fileCfg)
	if result.MaxFileSize != 50*1024*1024 {
		t.Errorf("MaxFileSize = %d, want %d", result.MaxFileSize, 50*1024*1024)
	}
	if result.MaxBackups != 3 {
		t.Errorf("MaxBackups = %d, want 3", result.MaxBackups)
	}
	if result.MaxAgeDays != 7 {
		t.Errorf("MaxAgeDays = %d, want 7", result.MaxAgeDays)
	}
}

func TestMergeFromLogging_Nil(t *testing.T) {
	t.Parallel()
	defaults := defaultLoggingConfig()
	result := defaults
	if result.Level != defaults.Level {
		t.Errorf("expected defaults preserved, got %q", result.Level)
	}
}

func TestMergeFromLogging_Override(t *testing.T) {
	t.Parallel()
	fileCfg := LoggingConfig{
		Level:  "debug",
		Format: "json",
	}
	defaults := defaultLoggingConfig()
	result := defaults
	MergeFromLogging(&result, fileCfg)
	if result.Level != "debug" {
		t.Errorf("Level = %q, want debug", result.Level)
	}
	if result.Format != "json" {
		t.Errorf("Format = %q, want json", result.Format)
	}
}

func TestEffectiveAuthMethod_Nil(t *testing.T) {
	var c *Config
	if got := c.EffectiveAuthMethod(); got != AuthMethodPassphrase {
		t.Errorf("nil config EffectiveAuthMethod = %q, want %q", got, AuthMethodPassphrase)
	}
}

func TestEffectiveAuthMethod_AuthMethod(t *testing.T) {
	c := &Config{AuthMethod: "passphrase"}
	if got := c.EffectiveAuthMethod(); got != AuthMethodPassphrase {
		t.Errorf("EffectiveAuthMethod = %q, want %q", got, AuthMethodPassphrase)
	}
}

func TestEffectiveAuthMethod_VaultAuthMethod(t *testing.T) {
	c := &Config{Vault: &VaultConfig{AuthMethod: "passphrase"}}
	if got := c.EffectiveAuthMethod(); got != AuthMethodPassphrase {
		t.Errorf("EffectiveAuthMethod = %q, want %q", got, AuthMethodPassphrase)
	}
}

func TestEffectiveAuthMethod_VaultUseTouchID(t *testing.T) {
	c := &Config{Vault: &VaultConfig{UseTouchID: true}}
	if got := c.EffectiveAuthMethod(); got != AuthMethodTouchID {
		t.Errorf("EffectiveAuthMethod = %q, want %q", got, AuthMethodTouchID)
	}
}

func TestEffectiveAuthMethod_Default(t *testing.T) {
	c := &Config{}
	if got := c.EffectiveAuthMethod(); got != AuthMethodPassphrase {
		t.Errorf("EffectiveAuthMethod = %q, want %q", got, AuthMethodPassphrase)
	}
}

// --- Tier Preset Tests ---

func TestPresets_ApplyTierPreset_ReadOnly(t *testing.T) {
	t.Parallel()
	p := &AgentProfile{Name: "test", AllowedPaths: []string{"*"}}
	ok := ApplyTierPreset(p, "read-only")
	if !ok {
		t.Fatal("ApplyTierPreset returned false for known tier")
	}
	if p.CanWrite != nil && *p.CanWrite {
		t.Error("CanWrite should be false for read-only")
	}
	if p.CanReadValues != nil && *p.CanReadValues {
		t.Error("CanReadValues should be false for read-only")
	}
	if p.ExposeValueTools != nil && *p.ExposeValueTools {
		t.Error("ExposeValueTools should be false for read-only")
	}
	if *p.ApprovalMode != "deny" {
		t.Errorf("ApprovalMode = %q, want deny", *p.ApprovalMode)
	}
	// AllowedPaths should be preserved
	if len(p.AllowedPaths) != 1 || p.AllowedPaths[0] != "*" {
		t.Error("AllowedPaths should be preserved from original profile")
	}
}

func TestPresets_ApplyTierPreset_Standard(t *testing.T) {
	t.Parallel()
	p := &AgentProfile{Name: "test", AllowedPaths: []string{"*"}}
	ok := ApplyTierPreset(p, "standard")
	if !ok {
		t.Fatal("ApplyTierPreset returned false for known tier")
	}
	if p.CanWrite != nil && *p.CanWrite {
		t.Error("CanWrite should be false for standard")
	}
	if p.CanReadValues == nil || !*p.CanReadValues {
		t.Error("CanReadValues should be true for standard")
	}
	if p.ExposeValueTools != nil && *p.ExposeValueTools {
		t.Error("ExposeValueTools should be false for standard")
	}
	if *p.ApprovalMode != "prompt" {
		t.Errorf("ApprovalMode = %q, want prompt", *p.ApprovalMode)
	}
}

func TestPresets_ApplyTierPreset_Admin(t *testing.T) {
	t.Parallel()
	p := &AgentProfile{Name: "test", AllowedPaths: []string{"*"}}
	ok := ApplyTierPreset(p, "admin")
	if !ok {
		t.Fatal("ApplyTierPreset returned false for known tier")
	}
	if p.CanWrite == nil || !*p.CanWrite {
		t.Error("CanWrite should be true for admin")
	}
	if p.CanReadValues == nil || !*p.CanReadValues {
		t.Error("CanReadValues should be true for admin")
	}
	if p.ExposeValueTools == nil || !*p.ExposeValueTools {
		t.Error("ExposeValueTools should be true for admin")
	}
	if p.CanRunCommands == nil || !*p.CanRunCommands {
		t.Error("CanRunCommands should be true for admin")
	}
	if p.CanManageConfig == nil || !*p.CanManageConfig {
		t.Error("CanManageConfig should be true for admin")
	}
	if p.CanUseClipboard == nil || !*p.CanUseClipboard {
		t.Error("CanUseClipboard should be true for admin")
	}
}

func TestPresets_UnknownTier_Ignored(t *testing.T) {
	t.Parallel()
	p := &AgentProfile{Name: "test", AllowedPaths: []string{"*"}}
	p.CanWrite = bptr(true)
	ok := ApplyTierPreset(p, "nonexistent")
	if ok {
		t.Fatal("ApplyTierPreset should return false for unknown tier")
	}
	if p.CanWrite == nil || !*p.CanWrite {
		t.Error("CanWrite should not be changed by unknown tier")
	}
}

// --- Tier Preset Load Tests ---

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

	yamlContent := `agents:
  my-agent:
    tier: standard
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent, ok := cfg.Agents["my-agent"]
	if !ok {
		t.Fatal("my-agent should exist")
	}
	// Verify preset was applied
	if agent.CanWrite != nil && *agent.CanWrite {
		t.Error("CanWrite should be false (standard preset)")
	}
	if agent.CanReadValues == nil || !*agent.CanReadValues {
		t.Error("CanReadValues should be true (standard preset)")
	}
	if agent.ExposeValueTools != nil && *agent.ExposeValueTools {
		t.Error("ExposeValueTools should be false (standard preset)")
	}
	if agent.Tier == nil || *agent.Tier != "standard" {
		t.Errorf("Tier = %v, want standard", agent.Tier)
	}
}

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

	yamlContent := `agents:
  my-agent:
    tier: standard
    canWrite: true
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent, ok := cfg.Agents["my-agent"]
	if !ok {
		t.Fatal("my-agent should exist")
	}
	// Preset sets CanWrite=false, but explicit YAML override should win
	if agent.CanWrite == nil || !*agent.CanWrite {
		t.Error("CanWrite should be true (explicit YAML overrides preset)")
	}
	// Preset should still apply for non-overridden fields
	if agent.ExposeValueTools != nil && *agent.ExposeValueTools {
		t.Error("ExposeValueTools should be false (from preset, not overridden)")
	}
}

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

	yamlContent := `agents:
  my-agent:
    tier: read-only
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent, ok := cfg.Agents["my-agent"]
	if !ok {
		t.Fatal("my-agent should exist")
	}
	if agent.CanWrite != nil && *agent.CanWrite {
		t.Error("CanWrite should be false (read-only preset)")
	}
	if agent.CanReadValues != nil && *agent.CanReadValues {
		t.Error("CanReadValues should be false (read-only preset)")
	}
	if agent.ExposeValueTools != nil && *agent.ExposeValueTools {
		t.Error("ExposeValueTools should be false (read-only preset)")
	}
}

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

	yamlContent := `agents:
  my-agent:
    tier: admin
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent, ok := cfg.Agents["my-agent"]
	if !ok {
		t.Fatal("my-agent should exist")
	}
	if agent.CanWrite == nil || !*agent.CanWrite {
		t.Error("CanWrite should be true (admin preset)")
	}
	if agent.CanReadValues == nil || !*agent.CanReadValues {
		t.Error("CanReadValues should be true (admin preset)")
	}
	if agent.ExposeValueTools == nil || !*agent.ExposeValueTools {
		t.Error("ExposeValueTools should be true (admin preset)")
	}
	if agent.CanRunCommands == nil || !*agent.CanRunCommands {
		t.Error("CanRunCommands should be true (admin preset)")
	}
}

// --- ExposeValueTools Tests ---

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

	yamlContent := `agents:
  my-agent:
    exposeValueTools: true
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent := cfg.Agents["my-agent"]
	if agent.ExposeValueTools == nil || !*agent.ExposeValueTools {
		t.Error("ExposeValueTools should be true (explicitly set)")
	}
}

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

	yamlContent := `agents:
  my-agent:
    exposeValueTools: false
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent := cfg.Agents["my-agent"]
	if agent.ExposeValueTools != nil && *agent.ExposeValueTools {
		t.Error("ExposeValueTools should be false (explicitly set)")
	}
}

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

	// Config without tier and without exposeValueTools should default to true
	yamlContent := `agents:
  my-agent:
    canWrite: true
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent := cfg.Agents["my-agent"]
	if agent.ExposeValueTools == nil || !*agent.ExposeValueTools {
		t.Error("ExposeValueTools should default to true for backward compat")
	}
}

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

	// Config with tier=standard but no exposeValueTools - should be false from preset
	yamlContent := `agents:
  my-agent:
    tier: standard
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent := cfg.Agents["my-agent"]
	if agent.ExposeValueTools != nil && *agent.ExposeValueTools {
		t.Error("ExposeValueTools should be false (from standard preset)")
	}
}

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

	// Config with tier=standard but explicit exposeValueTools: true - value should be true
	yamlContent := `agents:
  my-agent:
    tier: standard
    exposeValueTools: true
`
	path := writeTempFile(t, []byte(yamlContent))
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	agent := cfg.Agents["my-agent"]
	if agent.ExposeValueTools == nil || !*agent.ExposeValueTools {
		t.Error("ExposeValueTools should be true (explicit override of standard preset)")
	}
}

func TestEffectiveRedactFields(t *testing.T) {
	p := AgentProfile{
		RedactFields: []string{"totp.*"},
		PerToolRedactFields: map[string][]string{
			"get_entry_value": {"password"},
		},
	}

	// Global patterns always included
	got := p.EffectiveRedactFields("get_entry_value")
	if len(got) != 2 {
		t.Fatalf("len = %d, want 2; got %v", len(got), got)
	}

	// Tool with no per-tool config — only global
	got = p.EffectiveRedactFields("list_entries")
	if len(got) != 1 || got[0] != "totp.*" {
		t.Errorf("list_entries = %v, want [totp.*]", got)
	}

	// Dedup: same pattern in both global and per-tool
	p2 := AgentProfile{
		RedactFields: []string{"password"},
		PerToolRedactFields: map[string][]string{
			"get_entry_value": {"password", "api_key"},
		},
	}
	got = p2.EffectiveRedactFields("get_entry_value")
	// Should be ["password", "api_key"] — no duplicate password
	if len(got) != 2 {
		t.Errorf("dedup: len = %d, want 2; got %v", len(got), got)
	}

	// Nil profile — nil result
	var nilProfile *AgentProfile
	if nilProfile.EffectiveRedactFields("x") != nil {
		t.Error("nil profile should return nil")
	}

	// Empty profile — nil result
	empty := AgentProfile{}
	if empty.EffectiveRedactFields("x") != nil {
		t.Error("empty profile should return nil")
	}
}

func TestLoadParsesPerToolRedactFields(t *testing.T) {
	dir := t.TempDir()
	content := `agents:
  restricted:
    allowedPaths: ["*"]
    canWrite: false
    redactFields:
      - totp.*
    perToolRedactFields:
      get_entry_value:
        - password
      get_entry:
        - password
        - api_key
`
	path := filepath.Join(dir, "config.yaml")
	if err := os.WriteFile(path, []byte(content), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	agent := cfg.Agents["restricted"]
	if len(agent.PerToolRedactFields) != 2 {
		t.Fatalf("PerToolRedactFields len = %d, want 2", len(agent.PerToolRedactFields))
	}
	if len(agent.PerToolRedactFields["get_entry_value"]) != 1 || agent.PerToolRedactFields["get_entry_value"][0] != "password" {
		t.Errorf("get_entry_value = %v", agent.PerToolRedactFields["get_entry_value"])
	}
	if len(agent.PerToolRedactFields["get_entry"]) != 2 {
		t.Errorf("get_entry = %v", agent.PerToolRedactFields["get_entry"])
	}
}

func TestPerToolRedactFieldsDeepMerge(t *testing.T) {
	// Verify that loading a YAML with per-tool patterns actually works end-to-end,
	// and that EffectiveRedactFields returns the combined union correctly after Load.
	dir := t.TempDir()
	content := `agents:
  test:
    allowedPaths: ["*"]
    redactFields:
      - totp.*
    perToolRedactFields:
      get_entry_value:
        - password
        - api_key
`
	path := filepath.Join(dir, "config.yaml")
	if err := os.WriteFile(path, []byte(content), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	agent := cfg.Agents["test"]

	// EffectiveRedactFields("get_entry_value") should return totp.*, password, api_key
	got := agent.EffectiveRedactFields("get_entry_value")
	if len(got) != 3 {
		t.Fatalf("want 3 patterns, got %v", got)
	}

	// EffectiveRedactFields("list_entries") should return only the global pattern
	global := agent.EffectiveRedactFields("list_entries")
	if len(global) != 1 || global[0] != "totp.*" {
		t.Errorf("list_entries: want [totp.*], got %v", global)
	}
}

func TestSaveRoundTripsPerToolRedactFields(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")
	cfg := &Config{
		VaultDir: dir,
		Agents: map[string]AgentProfile{
			"test": {
				AllowedPaths: []string{"*"},
				PerToolRedactFields: map[string][]string{
					"get_entry_value": {"password"},
				},
			},
		},
	}
	if err := cfg.SaveTo(path); err != nil {
		t.Fatalf("SaveTo: %v", err)
	}
	loaded, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	got := loaded.Agents["test"].PerToolRedactFields["get_entry_value"]
	if len(got) != 1 || got[0] != "password" {
		t.Errorf("round-trip: got %v", got)
	}
}

// --- Comprehensive Round-Trip Tests (pre-refactor baseline) ---

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

	// Creates a fully populated AgentProfile, deep-copies it, and verifies
	// every field is independently copied using reflect. If a new field is
	// added to AgentProfile without being covered by the deep-copy logic,
	// this test will fail — either as a nil-pointer dereference (the field
	// needs a non-zero value in the source) or as a non-independent copy.
	src := AgentProfile{
		Name:                "test-agent",
		Tier:                sptr("admin"),
		ApprovalMode:        sptr("prompt"),
		AllowedPaths:        []string{"path1", "path2"},
		RedactFields:        []string{"secret"},
		PerToolRedactFields: map[string][]string{"get": {"token"}},
		CanWrite:            bptr(true),
		CanRunCommands:      bptr(true),
		CanManageConfig:     bptr(true),
		CanUseClipboard:     bptr(true),
		CanUseAutotype:      bptr(true),
		CanReadValues:       bptr(true),
		ExposeValueTools:    bptr(true),
		AutoUnseal:          bptr(true),
		RequireApproval:     bptr(true),
		ApprovalTimeout:     dptr(2 * time.Minute),
		AllowedTools:        []string{"tool1"},
		MaxReadsPerHour:     iptr(100),
		MaxReadsPerDay:      iptr(500),
		MaxSecretsInSession: iptr(10),
		DynamicProviders:    map[string][]string{"pg": {"ro"}},
		AllowedEnvVars:      []string{"PATH"},
		AllowedExecutables:  []string{"psql"},
		PromptInjectionMode: sptr("deny"),
		PreCallHooks:        []string{"pre1"},
		PostCallHooks:       []string{"post1"},
		SkillPath:           sptr("~/.skills/test/SKILL.md"),
		SkillVersion:        sptr("1.0.0"),
	}

	srcMap := map[string]AgentProfile{"agent": src}
	cpMap := copyAgentProfiles(srcMap)
	cp, ok := cpMap["agent"]
	if !ok {
		t.Fatal("copyAgentProfiles did not preserve map key")
	}

	// Use reflect to walk every field of AgentProfile and verify deep-copy.
	sv := reflect.ValueOf(src)
	cv := reflect.ValueOf(cp)
	typ := reflect.TypeOf(AgentProfile{})

	for i := range typ.NumField() {
		ft := typ.Field(i)
		if !ft.IsExported() {
			continue
		}
		sf := sv.Field(i)
		cf := cv.Field(i)

		switch ft.Type.Kind() {
		case reflect.String:
			if sf.String() != cf.String() {
				t.Errorf("copyAgentProfiles: field %q value mismatch: got %q, want %q",
					ft.Name, cf.String(), sf.String())
			}
		case reflect.Ptr:
			if sf.IsNil() {
				t.Errorf("copyAgentProfiles: field %q is nil in source — test must set a non-zero value to verify deep copy",
					ft.Name)
				continue
			}
			if cf.IsNil() {
				t.Errorf("copyAgentProfiles: field %q is nil in copy — deep copy lost the pointer", ft.Name)
				continue
			}
			// Same pointer address => shallow copy
			if cf.UnsafePointer() == sf.UnsafePointer() {
				t.Errorf("copyAgentProfiles: field %q is a shallow copy (same pointer)", ft.Name)
			}
			// Verify value equality
			se := sf.Elem()
			ce := cf.Elem()
			switch se.Kind() {
			case reflect.Bool:
				if se.Bool() != ce.Bool() {
					t.Errorf("copyAgentProfiles: field %q value mismatch: got %v, want %v",
						ft.Name, ce.Bool(), se.Bool())
				}
			case reflect.Int, reflect.Int64:
				if se.Int() != ce.Int() {
					t.Errorf("copyAgentProfiles: field %q value mismatch: got %d, want %d",
						ft.Name, ce.Int(), se.Int())
				}
			case reflect.String:
				if se.String() != ce.String() {
					t.Errorf("copyAgentProfiles: field %q value mismatch: got %q, want %q",
						ft.Name, ce.String(), se.String())
				}
			default:
				// Other kinds are not used in AgentProfile pointers; skip value comparison
			}
		case reflect.Slice:
			if sf.IsNil() {
				t.Errorf("copyAgentProfiles: field %q is nil in source — test must set a non-zero value", ft.Name)
				continue
			}
			if cf.IsNil() {
				t.Errorf("copyAgentProfiles: field %q is nil in copy — deep copy lost the slice", ft.Name)
				continue
			}
			if cf.Len() != sf.Len() {
				t.Errorf("copyAgentProfiles: field %q length mismatch: got %d, want %d",
					ft.Name, cf.Len(), sf.Len())
				continue
			}
			if cf.Len() > 0 && cf.Index(0).UnsafePointer() == sf.Index(0).UnsafePointer() {
				t.Errorf("copyAgentProfiles: field %q is a shallow copy (same backing array)", ft.Name)
			}
		case reflect.Map:
			if sf.IsNil() {
				t.Errorf("copyAgentProfiles: field %q is nil in source — test must set a non-zero value", ft.Name)
				continue
			}
			if cf.IsNil() {
				t.Errorf("copyAgentProfiles: field %q is nil in copy — deep copy lost the map", ft.Name)
				continue
			}
			for _, k := range sf.MapKeys() {
				svElem := sf.MapIndex(k)
				cvElem := cf.MapIndex(k)
				if !cvElem.IsValid() {
					t.Errorf("copyAgentProfiles: field %q missing key %v in copy", ft.Name, k)
					continue
				}
				if cvElem.Kind() == reflect.Slice && cvElem.Len() > 0 && svElem.Len() > 0 {
					if cvElem.Index(0).UnsafePointer() == svElem.Index(0).UnsafePointer() {
						t.Errorf("copyAgentProfiles: field %q map value is a shallow copy (same backing array)", ft.Name)
					}
				}
			}
		default:
			t.Errorf("copyAgentProfiles: unhandled field type %s for field %q — update test",
				ft.Type.Kind(), ft.Name)
		}
	}

	// Verify that modifying the original does not affect the copy.
	cp2 := copyAgentProfiles(map[string]AgentProfile{"a": src})
	orig := cp2["a"]
	orig.AllowedPaths[0] = "MUTATED"
	orig.AllowedTools[0] = "MUTATED"
	orig.Tier = sptr("MUTATED")
	orig.CanWrite = bptr(false)
	cp2 = copyAgentProfiles(map[string]AgentProfile{"a": orig})
	if *cp2["a"].Tier != "MUTATED" {
		t.Error("latest copy should reflect updates")
	}
}

func TestRoundTrip_AllFieldsSet(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	cfg := &Config{
		VaultDir:       filepath.Join(home, ".openpass"),
		DefaultAgent:   "test-agent",
		SessionTimeout: 10 * time.Minute,
		AuthMethod:     "touchid",
		UseTouchID:     bptr(true),
		EnvWhitelist:   []string{"HOME", "PATH"},
		DefaultProfile: "work",
		Profiles: map[string]*Profile{
			"work": {VaultPath: "~/.openpass-work"},
		},
		ScanPatterns: []CustomPattern{
			{Name: "test", Pattern: "test.*", Description: "test pattern", Severity: "high"},
		},
		Agents: map[string]AgentProfile{
			"test-agent": {
				Name:                "test-agent",
				Tier:                sptr("admin"),
				ApprovalMode:        sptr("prompt"),
				AllowedPaths:        []string{"path1", "path2"},
				RedactFields:        []string{"password", "api.*"},
				PerToolRedactFields: map[string][]string{"get": {"token"}},
				CanWrite:            bptr(true),
				CanRunCommands:      bptr(true),
				CanManageConfig:     bptr(true),
				CanUseClipboard:     bptr(true),
				CanUseAutotype:      bptr(true),
				CanReadValues:       bptr(true),
				ExposeValueTools:    bptr(true),
				AutoUnseal:          bptr(true),
				RequireApproval:     bptr(true),
				ApprovalTimeout:     dptr(2 * time.Minute),
				AllowedTools:        []string{"tool1", "tool2"},
				MaxReadsPerHour:     iptr(100),
				MaxReadsPerDay:      iptr(500),
				MaxSecretsInSession: IntPtr(10),
				DynamicProviders: map[string][]string{
					"postgres": {"readonly", "admin"},
				},
				AllowedEnvVars:      []string{"DATABASE_URL"},
				AllowedExecutables:  []string{"psql"},
				PromptInjectionMode: StrPtr("deny"),
				PreCallHooks:        []string{"hook1"},
				PostCallHooks:       []string{"hook2"},
				SkillPath:           sptr("~/.skills/test/SKILL.md"),
				SkillVersion:        sptr("1.0.0"),
			},
		},
		Vault: &VaultConfig{
			Path:              "/vault/path",
			DefaultRecipients: []string{"recipient1"},
			ConfirmRemove:     true,
			AuthMethod:        "passphrase",
			UseTouchID:        false,
			SearchWorkers:     4,
			PseudonymizePaths: true,
			FormatVersion:     2,
		},
		Git: &GitConfig{
			AutoPush:         true,
			AutoPull:         false,
			AutoPullInterval: 30 * time.Second,
			CommitTemplate:   "custom template",
		},
		MCP: &MCPConfig{
			Port:                9090,
			Bind:                "0.0.0.0",
			Stdio:               true,
			HTTPTokenFile:       "/tmp/token",
			OTLPEndpoint:        "http://otel:4318",
			ReadHeaderTimeout:   5 * time.Second,
			ReadTimeout:         10 * time.Second,
			WriteTimeout:        10 * time.Second,
			ShutdownTimeout:     5 * time.Second,
			ApprovalTimeout:     30 * time.Second,
			RateLimit:           120,
			TrustedProxyIPs:     []string{"10.0.0.0/8"},
			MetricsAuthRequired: false,
			TLSCertFile:         "/certs/cert.pem",
			TLSKeyFile:          "/certs/key.pem",
			AllowInsecureBind:   false,
			OAuth: &OAuthConfig{
				AccessTokenTTL:  1 * time.Hour,
				RefreshTokenTTL: 24 * time.Hour,
			},
		},
		Update: &UpdateConfig{
			CacheTTL: 12 * time.Hour,
		},
		Clipboard: &ClipboardConfig{
			AutoClearDuration: 45,
			PrintByDefault:    false,
		},
		Audit: &AuditConfig{
			MaxFileSize: 50 * 1024 * 1024,
			MaxBackups:  3,
			MaxAgeDays:  7,
		},
		Logging: &LoggingConfig{
			Level:  "debug",
			Format: "json",
		},
	}

	path := filepath.Join(home, ".openpass", "config.yaml")
	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	loaded, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	// Verify Config-level fields
	if loaded.VaultDir != cfg.VaultDir {
		t.Errorf("VaultDir = %q, want %q", loaded.VaultDir, cfg.VaultDir)
	}
	if loaded.DefaultAgent != cfg.DefaultAgent {
		t.Errorf("DefaultAgent = %q, want %q", loaded.DefaultAgent, cfg.DefaultAgent)
	}
	if loaded.SessionTimeout != cfg.SessionTimeout {
		t.Errorf("SessionTimeout = %v, want %v", loaded.SessionTimeout, cfg.SessionTimeout)
	}
	if loaded.AuthMethod != cfg.AuthMethod {
		t.Errorf("AuthMethod = %q, want %q", loaded.AuthMethod, cfg.AuthMethod)
	}
	if loaded.UseTouchID == nil || !*loaded.UseTouchID {
		t.Error("UseTouchID should be true")
	}
	if loaded.DefaultProfile != "work" {
		t.Errorf("DefaultProfile = %q, want work", loaded.DefaultProfile)
	}
	if len(loaded.ScanPatterns) != 1 {
		t.Errorf("ScanPatterns len = %d, want 1", len(loaded.ScanPatterns))
	}

	// Verify AgentProfile
	agent := loaded.Agents["test-agent"]
	want := cfg.Agents["test-agent"]
	if *agent.Tier != *want.Tier {
		t.Errorf("Tier = %q, want %q", *agent.Tier, *want.Tier)
	}
	if *agent.ApprovalMode != *want.ApprovalMode {
		t.Errorf("ApprovalMode = %q, want %q", *agent.ApprovalMode, *want.ApprovalMode)
	}
	if *agent.CanWrite != *want.CanWrite {
		t.Errorf("CanWrite = %v, want %v", agent.CanWrite, want.CanWrite)
	}
	if *agent.CanRunCommands != *want.CanRunCommands {
		t.Errorf("CanRunCommands = %v, want %v", *agent.CanRunCommands, *want.CanRunCommands)
	}
	if *agent.CanManageConfig != *want.CanManageConfig {
		t.Errorf("CanManageConfig = %v, want %v", *agent.CanManageConfig, *want.CanManageConfig)
	}
	if *agent.CanUseClipboard != *want.CanUseClipboard {
		t.Errorf("CanUseClipboard = %v, want %v", *agent.CanUseClipboard, *want.CanUseClipboard)
	}
	if *agent.CanUseAutotype != *want.CanUseAutotype {
		t.Errorf("CanUseAutotype = %v, want %v", *agent.CanUseAutotype, *want.CanUseAutotype)
	}
	if *agent.CanReadValues != *want.CanReadValues {
		t.Errorf("CanReadValues = %v, want %v", *agent.CanReadValues, *want.CanReadValues)
	}
	if *agent.ExposeValueTools != *want.ExposeValueTools {
		t.Errorf("ExposeValueTools = %v, want %v", *agent.ExposeValueTools, *want.ExposeValueTools)
	}
	if *agent.AutoUnseal != *want.AutoUnseal {
		t.Errorf("AutoUnseal = %v, want %v", *agent.AutoUnseal, *want.AutoUnseal)
	}
	if *agent.RequireApproval != *want.RequireApproval {
		t.Errorf("RequireApproval = %v, want %v", *agent.RequireApproval, *want.RequireApproval)
	}
	if *agent.ApprovalTimeout != *want.ApprovalTimeout {
		t.Errorf("ApprovalTimeout = %v, want %v", *agent.ApprovalTimeout, *want.ApprovalTimeout)
	}
	if *agent.MaxReadsPerHour != *want.MaxReadsPerHour {
		t.Errorf("MaxReadsPerHour = %d, want %d", *agent.MaxReadsPerHour, *want.MaxReadsPerHour)
	}
	if *agent.MaxReadsPerDay != *want.MaxReadsPerDay {
		t.Errorf("MaxReadsPerDay = %d, want %d", *agent.MaxReadsPerDay, *want.MaxReadsPerDay)
	}
	if *agent.MaxSecretsInSession != *want.MaxSecretsInSession {
		t.Errorf("MaxSecretsInSession = %d, want %d", *agent.MaxSecretsInSession, *want.MaxSecretsInSession)
	}
	if *agent.PromptInjectionMode != *want.PromptInjectionMode {
		t.Errorf("PromptInjectionMode = %q, want %q", *agent.PromptInjectionMode, *want.PromptInjectionMode)
	}
	if *agent.SkillPath != *want.SkillPath {
		t.Errorf("SkillPath = %q, want %q", *agent.SkillPath, *want.SkillPath)
	}
	if *agent.SkillVersion != *want.SkillVersion {
		t.Errorf("SkillVersion = %q, want %q", *agent.SkillVersion, *want.SkillVersion)
	}

	// Verify VaultConfig
	if loaded.Vault == nil {
		t.Fatal("Vault should not be nil")
	}
	if loaded.Vault.Path != "/vault/path" {
		t.Errorf("Vault.Path = %q, want /vault/path", loaded.Vault.Path)
	}
	if loaded.Vault.ConfirmRemove != true {
		t.Error("Vault.ConfirmRemove should be true")
	}
	if loaded.Vault.AuthMethod != "passphrase" {
		t.Errorf("Vault.AuthMethod = %q, want passphrase", loaded.Vault.AuthMethod)
	}
	if loaded.Vault.UseTouchID {
		t.Error("Vault.UseTouchID should be false")
	}
	if loaded.Vault.SearchWorkers != 4 {
		t.Errorf("Vault.SearchWorkers = %d, want 4", loaded.Vault.SearchWorkers)
	}
	if !loaded.Vault.PseudonymizePaths {
		t.Error("Vault.PseudonymizePaths should be true")
	}

	// Verify GitConfig
	if loaded.Git == nil {
		t.Fatal("Git should not be nil")
	}
	if !loaded.Git.AutoPush {
		t.Error("Git.AutoPush should be true")
	}
	// omitempty omits false - AutoPull defaults to true from defaultGitConfig
	_ = loaded.Git.AutoPull
	if loaded.Git.AutoPullInterval != 30*time.Second {
		t.Errorf("Git.AutoPullInterval = %v, want 30s", loaded.Git.AutoPullInterval)
	}
	if loaded.Git.CommitTemplate != "custom template" {
		t.Errorf("Git.CommitTemplate = %q, want custom template", loaded.Git.CommitTemplate)
	}

	// Verify MCPConfig
	if loaded.MCP == nil {
		t.Fatal("MCP should not be nil")
	}
	if loaded.MCP.Port != 9090 {
		t.Errorf("MCP.Port = %d, want 9090", loaded.MCP.Port)
	}
	if loaded.MCP.Bind != "0.0.0.0" {
		t.Errorf("MCP.Bind = %q, want 0.0.0.0", loaded.MCP.Bind)
	}
	if !loaded.MCP.Stdio {
		t.Error("MCP.Stdio should be true")
	}
	if loaded.MCP.RateLimit != 120 {
		t.Errorf("MCP.RateLimit = %d, want 120", loaded.MCP.RateLimit)
	}

	// Verify UpdateConfig
	if loaded.Update == nil {
		t.Fatal("Update should not be nil")
	}
	if loaded.Update.CacheTTL != 12*time.Hour {
		t.Errorf("Update.CacheTTL = %v, want 12h", loaded.Update.CacheTTL)
	}

	// Verify ClipboardConfig
	if loaded.Clipboard == nil {
		t.Fatal("Clipboard should not be nil")
	}
	if loaded.Clipboard.AutoClearDuration != 45 {
		t.Errorf("Clipboard.AutoClearDuration = %d, want 45", loaded.Clipboard.AutoClearDuration)
	}

	// Verify AuditConfig
	if loaded.Audit == nil {
		t.Fatal("Audit should not be nil")
	}
	if loaded.Audit.MaxFileSize != 50*1024*1024 {
		t.Errorf("Audit.MaxFileSize = %d, want %d", loaded.Audit.MaxFileSize, 50*1024*1024)
	}
	if loaded.Audit.MaxBackups != 3 {
		t.Errorf("Audit.MaxBackups = %d, want 3", loaded.Audit.MaxBackups)
	}
	if loaded.Audit.MaxAgeDays != 7 {
		t.Errorf("Audit.MaxAgeDays = %d, want 7", loaded.Audit.MaxAgeDays)
	}

	// Verify LoggingConfig
	if loaded.Logging == nil {
		t.Fatal("Logging should not be nil")
	}
	if loaded.Logging.Level != "debug" {
		t.Errorf("Logging.Level = %q, want debug", loaded.Logging.Level)
	}
	if loaded.Logging.Format != "json" {
		t.Errorf("Logging.Format = %q, want json", loaded.Logging.Format)
	}
}

func TestRoundTrip_DefaultsApplied(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	// Minimal config - all defaults should apply
	cfg := &Config{
		VaultDir:       filepath.Join(home, ".openpass"),
		DefaultAgent:   "default",
		SessionTimeout: defaultSessionTimeout,
		Agents: map[string]AgentProfile{
			"default": newDefaultAgentProfile("default"),
		},
	}

	path := filepath.Join(home, ".openpass", "config.yaml")
	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	loaded, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if loaded.SessionTimeout != defaultSessionTimeout {
		t.Errorf("SessionTimeout = %v, want %v", loaded.SessionTimeout, defaultSessionTimeout)
	}
	if loaded.AuthMethod != AuthMethodPassphrase {
		t.Errorf("AuthMethod = %q, want %q", loaded.AuthMethod, AuthMethodPassphrase)
	}
}

func TestRoundTrip_ExplicitZeroValues(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping on windows: HOME env behavior differs")
	}
	home := t.TempDir()
	t.Setenv("HOME", home)

	// Config with explicitly set zero values
	cfg := &Config{
		VaultDir:       filepath.Join(home, ".openpass"),
		DefaultAgent:   "default",
		SessionTimeout: defaultSessionTimeout,
		AuthMethod:     "passphrase",
		Agents: map[string]AgentProfile{
			"default": {
				Name:             "default",
				AllowedPaths:     []string{},
				CanWrite:         bptr(false),
				CanRunCommands:   bptr(false),
				CanManageConfig:  bptr(false),
				CanUseClipboard:  bptr(false),
				CanUseAutotype:   bptr(false),
				CanReadValues:    bptr(false),
				ExposeValueTools: bptr(false),
				AutoUnseal:       bptr(false),
				RequireApproval:  bptr(false),
				ApprovalMode:     sptr("deny"),
			},
		},
	}

	path := filepath.Join(home, ".openpass", "config.yaml")
	if err := cfg.Save(); err != nil {
		t.Fatalf("Save() error = %v", err)
	}

	loaded, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	agent := loaded.Agents["default"]
	if agent.CanWrite != nil && *agent.CanWrite {
		t.Error("CanWrite should be false (explicit zero)")
	}
	if agent.CanRunCommands != nil && *agent.CanRunCommands {
		t.Error("CanRunCommands should be false (explicit zero)")
	}
	if agent.CanManageConfig != nil && *agent.CanManageConfig {
		t.Error("CanManageConfig should be false (explicit zero)")
	}
	if agent.CanUseClipboard != nil && *agent.CanUseClipboard {
		t.Error("CanUseClipboard should be false (explicit zero)")
	}
	if agent.CanUseAutotype != nil && *agent.CanUseAutotype {
		t.Error("CanUseAutotype should be false (explicit zero)")
	}
	if agent.CanReadValues != nil && *agent.CanReadValues {
		t.Error("CanReadValues should be false (explicit zero)")
	}
	if agent.ExposeValueTools != nil && *agent.ExposeValueTools {
		t.Error("ExposeValueTools should be false (explicit zero)")
	}
	if agent.AutoUnseal != nil && *agent.AutoUnseal {
		t.Error("AutoUnseal should be false (explicit zero)")
	}
	if agent.RequireApproval != nil && *agent.RequireApproval {
		t.Error("RequireApproval should be false (explicit zero)")
	}
	if agent.ApprovalMode == nil || *agent.ApprovalMode != "deny" {
		t.Errorf("ApprovalMode = %v, want deny", agent.ApprovalMode)
	}
	if agent.CanManageConfig != nil && *agent.CanManageConfig {
		t.Error("CanManageConfig should be false (explicit zero)")
	}
	if agent.CanUseClipboard != nil && *agent.CanUseClipboard {
		t.Error("CanUseClipboard should be false (explicit zero)")
	}
	if agent.CanUseAutotype != nil && *agent.CanUseAutotype {
		t.Error("CanUseAutotype should be false (explicit zero)")
	}
	if agent.CanReadValues != nil && *agent.CanReadValues {
		t.Error("CanReadValues should be false (explicit zero)")
	}
	if agent.ExposeValueTools != nil && *agent.ExposeValueTools {
		t.Error("ExposeValueTools should be false (explicit zero)")
	}
	if agent.AutoUnseal != nil && *agent.AutoUnseal {
		t.Error("AutoUnseal should be false (explicit zero)")
	}
	if agent.RequireApproval != nil && *agent.RequireApproval {
		t.Error("RequireApproval should be false (explicit zero)")
	}
	if agent.ApprovalMode == nil || *agent.ApprovalMode != "deny" {
		t.Errorf("ApprovalMode = %v, want deny", agent.ApprovalMode)
	}
}
