package daemon

import (
	"context"
	"errors"
	"fmt"
	"os/user"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

func TestWindowsInstallerRegistersExplicitServiceOwner(t *testing.T) {
	var calls [][]string
	previous := windowsTaskCommandOutput
	windowsTaskCommandOutput = func(_ context.Context, args ...string) ([]byte, error) {
		calls = append(calls, append([]string(nil), args...))
		if args[0] == "/Query" {
			return nil, errors.New("task not found")
		}
		return nil, nil
	}
	t.Cleanup(func() { windowsTaskCommandOutput = previous })

	installer := newWindowsInstaller(`C:\Users\alice\.agentteam\bin\agentteam.exe`, "")
	require.NoError(t, installer.Install(InstallOptions{}))
	require.Equal(t, []string{
		"/Create",
		"/TN", "AgentTeam",
		"/TR", `"C:\Users\alice\.agentteam\bin\agentteam.exe" daemon --owner-kind service`,
		"/SC", "ONLOGON",
		"/RL", "HIGHEST",
		"/F",
	}, calls[1])
}

func TestWindowsLegacyTaskMatching(t *testing.T) {
	installer := newWindowsInstaller(`C:\Users\alice\.agentteam\bin\agentteam.exe`, "")
	base := legacyWindowsTaskSnapshot()
	tests := []struct {
		name   string
		mutate func(*windowsTaskSnapshot)
		want   bool
	}{
		{name: "legacy daemon", want: true},
		{name: "legacy headless", mutate: func(task *windowsTaskSnapshot) { task.Arguments = "daemon --headless" }, want: true},
		{name: "current service owner", mutate: func(task *windowsTaskSnapshot) { task.Arguments = "daemon --owner-kind service" }},
		{name: "different executable", mutate: func(task *windowsTaskSnapshot) { task.Execute = `C:\Other\agentteam.exe` }},
		{name: "subdirectory task", mutate: func(task *windowsTaskSnapshot) { task.TaskPath = `\AgentTeam\` }},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			task := base
			if test.mutate != nil {
				test.mutate(&task)
			}
			require.Equal(t, test.want, installer.matchesLegacyTask(task))
		})
	}
}

func TestWindowsLegacyTaskReconciliation(t *testing.T) {
	legacy := legacyWindowsTaskSnapshot()
	installer := newWindowsInstaller(legacy.Execute, "")

	t.Run("current daemon yields at the observation boundary", func(t *testing.T) {
		queries := 0
		var observed time.Duration
		withWindowsTaskHooks(t,
			func(context.Context) (windowsTaskSnapshot, error) {
				queries++
				if queries <= 5 {
					return legacy, nil
				}
				yielded := legacy
				yielded.State = "Ready"
				return yielded, nil
			},
			func(context.Context, string) (windowsProcessSnapshot, error) {
				return windowsProcessSnapshot{Complete: true}, nil
			},
			func(context.Context, ...string) ([]byte, error) {
				t.Fatal("scheduled task command should not run")
				return nil, nil
			},
			func(_ context.Context, delay time.Duration) error { observed += delay; return nil },
		)
		require.NoError(t, installer.reconcileLegacyElectronTask())
		require.LessOrEqual(t, observed, windowsLegacyTaskObservationWindow)
		require.Greater(t, observed, windowsLegacyTaskObservationWindow-windowsLegacyTaskPollInterval)
	})

	t.Run("persistent legacy daemon is stopped and deleted", func(t *testing.T) {
		queries := 0
		var calls [][]string
		withWindowsTaskHooks(t,
			func(context.Context) (windowsTaskSnapshot, error) {
				queries++
				switch {
				case queries <= 6:
					return legacy, nil
				case queries == 7:
					stopped := legacy
					stopped.State = "Ready"
					return stopped, nil
				default:
					return windowsTaskSnapshot{}, nil
				}
			},
			func(context.Context, string) (windowsProcessSnapshot, error) {
				return windowsProcessSnapshot{Complete: true}, nil
			},
			func(_ context.Context, args ...string) ([]byte, error) {
				calls = append(calls, append([]string(nil), args...))
				return nil, nil
			},
			func(context.Context, time.Duration) error { return nil },
		)
		require.NoError(t, installer.reconcileLegacyElectronTask())
		require.Equal(t, [][]string{{"/End", "/TN", `\AgentTeam`}, {"/Delete", "/TN", `\AgentTeam`, "/F"}}, calls)
	})

	t.Run("waits for task state after the managed process exits", func(t *testing.T) {
		queries := 0
		var calls [][]string
		withWindowsTaskHooks(t,
			func(context.Context) (windowsTaskSnapshot, error) {
				queries++
				if queries <= 7 {
					return legacy, nil
				}
				if queries == 8 {
					stopped := legacy
					stopped.State = "Ready"
					return stopped, nil
				}
				return windowsTaskSnapshot{}, nil
			},
			func(context.Context, string) (windowsProcessSnapshot, error) {
				return windowsProcessSnapshot{Complete: true}, nil
			},
			func(_ context.Context, args ...string) ([]byte, error) {
				calls = append(calls, append([]string(nil), args...))
				return nil, nil
			},
			func(context.Context, time.Duration) error { return nil },
		)
		require.NoError(t, installer.reconcileLegacyElectronTask())
		require.Equal(t, [][]string{{"/End", "/TN", `\AgentTeam`}, {"/Delete", "/TN", `\AgentTeam`, "/F"}}, calls)
	})

	t.Run("query and process ambiguity block startup", func(t *testing.T) {
		withWindowsTaskHooks(t,
			func(context.Context) (windowsTaskSnapshot, error) { return legacy, errors.New("provider denied") },
			func(context.Context, string) (windowsProcessSnapshot, error) {
				t.Fatal("process query should not run")
				return windowsProcessSnapshot{}, nil
			},
			func(context.Context, ...string) ([]byte, error) { t.Fatal("command should not run"); return nil, nil },
			func(context.Context, time.Duration) error { return nil },
		)
		require.ErrorContains(t, installer.reconcileLegacyElectronTask(), "provider denied")

		ready := legacy
		ready.State = "Ready"
		withWindowsTaskHooks(t,
			func(context.Context) (windowsTaskSnapshot, error) { return ready, nil },
			func(context.Context, string) (windowsProcessSnapshot, error) {
				return windowsProcessSnapshot{UnreadablePIDs: []int{1130}}, nil
			},
			func(context.Context, ...string) ([]byte, error) { t.Fatal("command should not run"); return nil, nil },
			func(context.Context, time.Duration) error { return nil },
		)
		err := installer.reconcileLegacyElectronTask()
		require.ErrorContains(t, err, "process paths unavailable")
	})

	t.Run("delete failure blocks startup", func(t *testing.T) {
		queries := 0
		withWindowsTaskHooks(t,
			func(context.Context) (windowsTaskSnapshot, error) {
				queries++
				if queries <= 6 {
					return legacy, nil
				}
				stopped := legacy
				stopped.State = "Ready"
				return stopped, nil
			},
			func(context.Context, string) (windowsProcessSnapshot, error) {
				return windowsProcessSnapshot{Complete: true}, nil
			},
			func(_ context.Context, args ...string) ([]byte, error) {
				if args[0] == "/Delete" {
					return []byte("access denied"), errors.New("exit status 1")
				}
				return nil, nil
			},
			func(context.Context, time.Duration) error { return nil },
		)
		err := installer.reconcileLegacyElectronTask()
		require.ErrorContains(t, err, "delete legacy scheduled task")
		require.ErrorContains(t, err, "access denied")
		require.ErrorContains(t, err, "elevated Command Prompt")
	})
}

func legacyWindowsTaskSnapshot() windowsTaskSnapshot {
	return windowsTaskSnapshot{
		Exists:      true,
		TaskPath:    `\`,
		TaskName:    "AgentTeam",
		State:       "Running",
		ActionCount: 1,
		Execute:     `C:\Users\alice\.agentteam\bin\agentteam.exe`,
		Arguments:   "daemon --headless",
	}
}

func withWindowsTaskHooks(
	t *testing.T,
	query func(context.Context) (windowsTaskSnapshot, error),
	processes func(context.Context, string) (windowsProcessSnapshot, error),
	command func(context.Context, ...string) ([]byte, error),
	sleep func(context.Context, time.Duration) error,
) {
	t.Helper()
	previousQuery := windowsTaskSnapshotQuery
	previousProcesses := windowsProcessSnapshotQuery
	previousCommand := windowsTaskCommandOutput
	previousSleep := windowsTaskSleep
	windowsTaskSnapshotQuery = query
	windowsProcessSnapshotQuery = processes
	windowsTaskCommandOutput = command
	windowsTaskSleep = sleep
	t.Cleanup(func() {
		windowsTaskSnapshotQuery = previousQuery
		windowsProcessSnapshotQuery = previousProcesses
		windowsTaskCommandOutput = previousCommand
		windowsTaskSleep = previousSleep
	})
}

func TestLaunchdInstallerPlistContentUsesResolvedShellPath(t *testing.T) {
	currentUser, err := user.Current()
	require.NoError(t, err)

	t.Setenv("SUDO_USER", currentUser.Username)
	t.Setenv("PATH", "/usr/bin:/bin")

	prevLookup := loginShellPathForUser
	loginShellPathForUser = func(username string) (string, error) {
		require.Equal(t, currentUser.Username, username)
		return "/opt/homebrew/bin:/usr/bin:/bin", nil
	}
	t.Cleanup(func() {
		loginShellPathForUser = prevLookup
	})

	installer := newLaunchdInstaller("/tmp/agentteam", "")
	content := installer.plistContent()
	wantPath := filepath.Join(currentUser.HomeDir, ".agentteam", "bin") + string(filepath.ListSeparator) + "/opt/homebrew/bin:/usr/bin:/bin"

	require.Contains(t, content, "<key>PATH</key>")
	require.Contains(t, content, "<string>"+wantPath+"</string>")
}

func TestLaunchdInstallerPlistContentFallsBackToCurrentPath(t *testing.T) {
	currentUser, err := user.Current()
	require.NoError(t, err)

	t.Setenv("SUDO_USER", currentUser.Username)
	t.Setenv("PATH", "/usr/local/bin:/usr/bin:/bin")

	prevLookup := loginShellPathForUser
	loginShellPathForUser = func(string) (string, error) {
		return "", assertiveErr("shell lookup failed")
	}
	t.Cleanup(func() {
		loginShellPathForUser = prevLookup
	})

	installer := newLaunchdInstaller("/tmp/agentteam", "")
	content := installer.plistContent()
	wantPath := filepath.Join(currentUser.HomeDir, ".agentteam", "bin") + string(filepath.ListSeparator) + "/usr/local/bin:/usr/bin:/bin"

	require.Contains(t, content, "<string>"+wantPath+"</string>")
}

func TestLaunchdInstallerPlistContentPinsConfigDir(t *testing.T) {
	configDir := filepath.Join(t.TempDir(), "agentteam-profile")
	installer := newLaunchdInstaller("/tmp/agentteam", filepath.Join(configDir, "config.yaml"))
	content := installer.plistContent()

	require.Contains(t, content, "<key>"+profile.ConfigDirEnvKey+"</key>")
	require.Contains(t, content, "<string>"+configDir+"</string>")
	require.Contains(t, content, "<string>--owner-kind</string><string>service</string>")
}

func TestSystemdInstallerSystemScopeUsesSystemUnitPathAndManager(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("Linux-only systemd service installer")
	}

	installer := newSystemdInstaller("/tmp/agentteam", "", ServiceScopeSystem)

	require.Equal(t, "agentteam.service", installer.unitName)
	require.Equal(t, "/etc/systemd/system/agentteam.service", installer.unitFile)
	require.Equal(t, []string{"systemctl", "start"}, installer.systemctlArgs("start"))
	require.Contains(t, installer.unitContent(), "WantedBy=multi-user.target")
}

func TestSystemdInstallerUnitContentPinsConfigDir(t *testing.T) {
	configDir := filepath.Join(t.TempDir(), "agentteam-profile")
	installer := newSystemdInstaller("/tmp/agentteam", filepath.Join(configDir, "config.yaml"), ServiceScopeSystem)
	content := installer.unitContent()

	require.Contains(t, content, `Environment="`+profile.ConfigDirEnvKey+`=`+configDir+`"`)
	require.NotContains(t, content, "PIDFile=")
	require.Contains(t, content, "ExecStart=/tmp/agentteam daemon --owner-kind service")
}

func TestSystemdInstallerUserScopeUsesUserUnitPathAndManager(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("Linux-only systemd service installer")
	}

	configDir := t.TempDir()
	t.Setenv("XDG_CONFIG_HOME", configDir)
	t.Setenv("HOME", filepath.Join(t.TempDir(), "home"))
	t.Setenv("PATH", "/usr/local/bin:/usr/bin:/bin")

	installer := newSystemdInstaller("/tmp/agentteam", "", ServiceScopeUser)

	require.Equal(t, "agentteam.service", installer.unitName)
	require.Equal(t, filepath.Join(configDir, "systemd", "user", "agentteam.service"), installer.unitFile)
	require.Equal(t, []string{"systemctl", "--user", "start"}, installer.systemctlArgs("start"))
	content := installer.unitContent()
	require.Contains(t, content, "WantedBy=default.target")
	require.NotContains(t, content, "User=")
}

func TestSystemdInstallerQuotesEnvironmentAssignments(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("Linux-only systemd service installer")
	}

	configDir := t.TempDir()
	t.Setenv("XDG_CONFIG_HOME", configDir)
	t.Setenv("HOME", filepath.Join(t.TempDir(), "home"))
	t.Setenv("PATH", "/usr/local/bin:/mnt/c/Program Files/Git/cmd:/usr/bin:/bin")

	installer := newSystemdInstaller("/tmp/agentteam", "", ServiceScopeUser)
	content := installer.unitContent()

	require.Contains(t, content, `Environment="HOME=`)
	require.Contains(t, content, `Environment="PATH=`)
	require.Contains(t, content, `/mnt/c/Program Files/Git/cmd`)
}

func TestSystemdStatusFromIsActivePreservesNonActiveOutput(t *testing.T) {
	require.Equal(t, "active", systemdStatusFromIsActive([]byte("active\n"), nil))
	require.Equal(t, "failed", systemdStatusFromIsActive([]byte("failed\n"), assertiveErr("exit status 3")))
	require.Equal(t, "activating", systemdStatusFromIsActive([]byte("activating\n"), assertiveErr("exit status 3")))
	require.Equal(t, "inactive", systemdStatusFromIsActive(nil, assertiveErr("exit status 3")))
	require.Equal(t, "unknown", systemdStatusFromIsActive(nil, nil))
}

type assertiveErr string

func (e assertiveErr) Error() string { return string(e) }

func withLaunchdCommandHooks(
	t *testing.T,
	runner func(args ...string) error,
	output func(args ...string) ([]byte, error),
) {
	t.Helper()
	originalRunner := launchdCommandRunner
	originalOutput := launchdCommandOutput
	originalTimeout := launchdSettleTimeout
	originalPollInterval := launchdPollInterval
	launchdCommandRunner = runner
	launchdCommandOutput = output
	launchdSettleTimeout = 0
	launchdPollInterval = 0
	t.Cleanup(func() {
		launchdCommandRunner = originalRunner
		launchdCommandOutput = originalOutput
		launchdSettleTimeout = originalTimeout
		launchdPollInterval = originalPollInterval
	})
}

func TestLaunchdInstallerUsesPingCAPDaemonLabel(t *testing.T) {
	installer := newLaunchdInstaller("/tmp/agentteam", "")
	require.Equal(t, launchdServiceLabel, installer.label)
	require.Equal(t, launchdPlistFile(launchdServiceLabel), installer.plistFile)
	require.Contains(t, installer.plistContent(), "<string>"+launchdServiceLabel+"</string>")
}

func TestLaunchdServiceTargetForUID(t *testing.T) {
	require.Equal(t, "gui/501", launchdDomainTargetForUID(501))
	require.Equal(t, "gui/501/cn.pingcap.agentteam.daemon", launchdServiceTargetForUID(501, launchdServiceLabel))
}

func TestLaunchdInstallerRestartUsesKickstartWithoutReinstall(t *testing.T) {
	var calls [][]string
	withLaunchdCommandHooks(t, func(args ...string) error {
		calls = append(calls, append([]string(nil), args...))
		return nil
	}, func(args ...string) ([]byte, error) {
		require.Equal(t, []string{"print", launchdServiceTarget(launchdServiceLabel)}, args)
		return []byte("state = running\npid = 123"), nil
	})

	installer := &launchdInstaller{label: launchdServiceLabel, plistFile: "/tmp/cn.pingcap.agentteam.daemon.plist"}
	require.NoError(t, installer.Restart())
	require.Equal(t, [][]string{{"kickstart", "-k", launchdServiceTarget(launchdServiceLabel)}}, calls)
}

func TestLaunchdInstallerRestartFallsBackToBootoutBootstrap(t *testing.T) {
	var calls [][]string
	state := serviceStatusActive
	withLaunchdCommandHooks(t, func(args ...string) error {
		calls = append(calls, append([]string(nil), args...))
		switch args[0] {
		case "kickstart":
			if len(args) > 1 && args[1] == "-k" {
				return assertiveErr("kickstart failed")
			}
			state = serviceStatusActive
		case "bootout":
			state = serviceStatusNotLoaded
		case "bootstrap":
			state = serviceStatusInactive
		}
		return nil
	}, func(args ...string) ([]byte, error) {
		require.Equal(t, []string{"print", launchdServiceTarget(launchdServiceLabel)}, args)
		if state == serviceStatusNotLoaded {
			return []byte("Could not find service"), assertiveErr("missing")
		}
		if state == serviceStatusInactive {
			return []byte("state = waiting"), nil
		}
		return []byte("state = running\npid = 123"), nil
	})

	installer := &launchdInstaller{label: launchdServiceLabel, plistFile: "/tmp/cn.pingcap.agentteam.daemon.plist"}
	require.NoError(t, installer.Restart())
	require.Equal(t, [][]string{
		{"kickstart", "-k", launchdServiceTarget(launchdServiceLabel)},
		{"bootout", launchdServiceTarget(launchdServiceLabel)},
		{"bootstrap", launchdDomainTarget(), "/tmp/cn.pingcap.agentteam.daemon.plist"},
		{"kickstart", launchdServiceTarget(launchdServiceLabel)},
	}, calls)
}

// withLaunchdUnloadFakes replaces launchctl with in-memory fakes so
// unloadLaunchdService tests never invoke real launchctl and never send real
// signals — the running daemon is untouched. Force-kills are observable via the
// runner fake (a "kill" arg), never via a real signal. Timeouts are shrunk so
// the loops resolve from state transitions, not wall-clock waits.
func withLaunchdUnloadFakes(
	t *testing.T,
	runner func(args ...string) error,
	output func(args ...string) ([]byte, error),
) {
	t.Helper()
	prevRunner, prevOutput := launchdCommandRunner, launchdCommandOutput
	prevPoll := launchdPollInterval
	prevExit, prevMargin, prevForce := launchdExitTimeout, launchdExitWaitMargin, launchdForceExitTimeout
	launchdCommandRunner = runner
	launchdCommandOutput = output
	launchdPollInterval = time.Millisecond
	launchdExitTimeout = 50 * time.Millisecond
	launchdExitWaitMargin = 20 * time.Millisecond
	launchdForceExitTimeout = 50 * time.Millisecond
	t.Cleanup(func() {
		launchdCommandRunner = prevRunner
		launchdCommandOutput = prevOutput
		launchdPollInterval = prevPoll
		launchdExitTimeout = prevExit
		launchdExitWaitMargin = prevMargin
		launchdForceExitTimeout = prevForce
	})
}

func TestUnloadLaunchdServiceSucceedsOnGracefulExitWithoutKilling(t *testing.T) {
	// bootout succeeds; the daemon is briefly active then deregisters on its own
	// within the budget. unload must return success and must NOT force-kill.
	prints := 0
	var calls []string
	withLaunchdUnloadFakes(t,
		func(args ...string) error {
			calls = append(calls, args[0])
			return nil
		},
		func(args ...string) ([]byte, error) {
			prints++
			if prints <= 2 {
				return []byte("state = running\npid = 456"), nil
			}
			return []byte("Could not find service"), assertiveErr("missing")
		},
	)

	require.NoError(t, unloadLaunchdService(launchdServiceLabel))
	require.NotContains(t, calls, "kill", "must not force-kill a daemon that exits on its own")
}

func TestUnloadLaunchdServiceForceKillsStalledDaemonByServiceTarget(t *testing.T) {
	// bootout succeeds but the daemon never deregisters on its own. unload must
	// ask launchd to SIGKILL the job by service identity (no raw PID) and then
	// confirm deregistration.
	killed := false
	var killArgs []string
	withLaunchdUnloadFakes(t,
		func(args ...string) error {
			if args[0] == "kill" {
				killed = true
				killArgs = args
			}
			return nil
		},
		func(args ...string) ([]byte, error) {
			if killed {
				return []byte("Could not find service"), assertiveErr("missing")
			}
			return []byte("state = running\npid = 456"), nil
		},
	)

	require.NoError(t, unloadLaunchdService(launchdServiceLabel))
	require.True(t, killed, "must force-kill a daemon that does not exit on its own")
	require.Equal(t, []string{"kill", "SIGKILL", launchdServiceTarget(launchdServiceLabel)}, killArgs)
}

func TestUnloadLaunchdServiceErrorsWhenDaemonCannotBeRemoved(t *testing.T) {
	// bootout succeeds, the daemon stalls, the force-kill runs, but the service
	// still never deregisters. unload must return an error — never mask a dirty
	// state as a successful uninstall.
	killed := false
	withLaunchdUnloadFakes(t,
		func(args ...string) error {
			if args[0] == "kill" {
				killed = true
			}
			return nil
		},
		func(args ...string) ([]byte, error) {
			return []byte("state = running\npid = 456"), nil // never deregisters
		},
	)

	err := unloadLaunchdService(launchdServiceLabel)
	require.Error(t, err)
	require.True(t, killed, "must attempt the force kill before giving up")
}

func TestUnloadLaunchdServiceErrorsWithoutKillingWhenStatusUnobservable(t *testing.T) {
	// If launchctl print fails for a reason other than "Could not find service",
	// the post-condition cannot be verified. unload must return an error AND must
	// not force-kill — no destructive operation while the job state is
	// unobservable.
	var calls []string
	withLaunchdUnloadFakes(t,
		func(args ...string) error {
			calls = append(calls, args[0])
			return nil
		},
		func(args ...string) ([]byte, error) {
			return []byte("Operation not permitted"), assertiveErr("denied")
		},
	)

	err := unloadLaunchdService(launchdServiceLabel)
	require.Error(t, err)
	require.NotContains(t, calls, "kill", "must not force-kill when job state is unobservable")
}

func TestUnloadLaunchdServiceDoesNotKillOnTransientPrintError(t *testing.T) {
	// The first status read fails transiently; a subsequent read would show the
	// service still active. unload must NOT treat the read error as a timeout and
	// force-kill ~immediately (skipping the full graceful drain). It must surface
	// an error without killing.
	var calls []string
	prints := 0
	withLaunchdUnloadFakes(t,
		func(args ...string) error {
			calls = append(calls, args[0])
			return nil
		},
		func(args ...string) ([]byte, error) {
			prints++
			if prints == 1 {
				return []byte("Operation not permitted"), assertiveErr("transient")
			}
			return []byte("state = running\npid = 456"), nil // would show active
		},
	)

	err := unloadLaunchdService(launchdServiceLabel)
	require.Error(t, err)
	require.NotContains(t, calls, "kill", "must not force-kill when the first status read failed transiently")
}

func TestLaunchdInstallerPlistContentDeclaresExitTimeout(t *testing.T) {
	installer := newLaunchdInstaller("/tmp/agentteam", "")
	content := installer.plistContent()
	require.Contains(t, content, "<key>ExitTimeOut</key>")
	require.Contains(t, content, fmt.Sprintf("<integer>%d</integer>", int(launchdExitTimeout.Seconds())))
}

func TestLaunchdInstallerStatusParsesRunningStateAndPID(t *testing.T) {
	status, err := launchdStatusFromPrint([]byte("state = running"), nil)
	require.NoError(t, err)
	require.Equal(t, serviceStatusActive, status)

	status, err = launchdStatusFromPrint([]byte("state = waiting\npid = 123"), nil)
	require.NoError(t, err)
	require.Equal(t, serviceStatusActive, status)
}

func TestLaunchdStatusFromPrintOnlyTreatsExplicitNotFoundAsNotLoaded(t *testing.T) {
	// Explicit absence is not-loaded with no error...
	status, err := launchdStatusFromPrint([]byte("Could not find service"), assertiveErr("missing"))
	require.NoError(t, err)
	require.Equal(t, serviceStatusNotLoaded, status)

	// ...but any other print failure must propagate rather than be silently read
	// as not-loaded, which would falsely report a successful uninstall.
	_, err = launchdStatusFromPrint([]byte("Operation not permitted"), assertiveErr("denied"))
	require.Error(t, err)
}

func TestLaunchdInstallerStatusReportsInactiveWhenLoadedButNotRunning(t *testing.T) {
	withLaunchdCommandHooks(t, func(args ...string) error {
		t.Fatalf("runner should not be called by Status")
		return nil
	}, func(args ...string) ([]byte, error) {
		require.Equal(t, []string{"print", launchdServiceTarget(launchdServiceLabel)}, args)
		return []byte("state = waiting\npid = 0"), nil
	})

	installer := &launchdInstaller{label: launchdServiceLabel}
	status, err := installer.Status()
	require.NoError(t, err)
	require.Equal(t, serviceStatusInactive, status)
}

func TestLaunchdInstallerStatusReportsNotLoadedWhenMissing(t *testing.T) {
	withLaunchdCommandHooks(t, func(args ...string) error {
		t.Fatalf("runner should not be called by Status")
		return nil
	}, func(args ...string) ([]byte, error) {
		require.Equal(t, []string{"print", launchdServiceTarget(launchdServiceLabel)}, args)
		return []byte("Could not find service"), assertiveErr("missing")
	})

	installer := &launchdInstaller{label: launchdServiceLabel}
	status, err := installer.Status()
	require.NoError(t, err)
	require.Equal(t, serviceStatusNotLoaded, status)
}

func TestLaunchdInstallerInstallCanRegisterWithoutStarting(t *testing.T) {
	var calls [][]string
	withLaunchdCommandHooks(t, func(args ...string) error {
		calls = append(calls, append([]string(nil), args...))
		return nil
	}, func(args ...string) ([]byte, error) {
		return []byte("Could not find service"), assertiveErr("missing")
	})

	tmpDir := t.TempDir()
	plistFile := filepath.Join(tmpDir, "cn.pingcap.agentteam.daemon.plist")
	installer := &launchdInstaller{
		label:     launchdServiceLabel,
		plistFile: plistFile,
		binPath:   "/usr/local/bin/agentteam",
	}

	require.NoError(t, installer.Install(InstallOptions{}))
	require.Empty(t, calls)
	require.FileExists(t, plistFile)
}

func TestLaunchdInstallerInstallStartBootstrapsKickstartsAndWaitsRunning(t *testing.T) {
	tmpDir := t.TempDir()
	plistFile := filepath.Join(tmpDir, "cn.pingcap.agentteam.daemon.plist")

	state := serviceStatusNotLoaded
	var calls [][]string
	withLaunchdCommandHooks(t, func(args ...string) error {
		calls = append(calls, append([]string(nil), args...))
		switch args[0] {
		case "bootout":
			state = serviceStatusNotLoaded
		case "bootstrap":
			state = serviceStatusInactive
		case "kickstart":
			state = serviceStatusActive
		}
		return nil
	}, func(args ...string) ([]byte, error) {
		require.Len(t, args, 2)
		require.Equal(t, "print", args[0])
		if args[1] == launchdServiceTarget(legacyLaunchdServiceLabel) {
			return []byte("Could not find service"), assertiveErr("missing")
		}
		require.Equal(t, launchdServiceTarget(launchdServiceLabel), args[1])
		if state == serviceStatusNotLoaded {
			return []byte("Could not find service"), assertiveErr("missing")
		}
		if state == serviceStatusInactive {
			return []byte("state = waiting"), nil
		}
		return []byte("state = running\npid = 123"), nil
	})

	installer := &launchdInstaller{
		label:     launchdServiceLabel,
		plistFile: plistFile,
		binPath:   "/usr/local/bin/agentteam",
	}
	require.NoError(t, installer.Install(InstallOptions{Start: true}))
	require.Equal(t, [][]string{
		{"bootout", launchdServiceTarget(launchdServiceLabel)},
		{"bootstrap", launchdDomainTarget(), plistFile},
		{"kickstart", launchdServiceTarget(launchdServiceLabel)},
	}, calls)
	require.FileExists(t, plistFile)
}

func TestLaunchdInstallerInstallStartCleansPlistWhenBootstrapFails(t *testing.T) {
	tmpDir := t.TempDir()
	plistFile := filepath.Join(tmpDir, "cn.pingcap.agentteam.daemon.plist")

	withLaunchdCommandHooks(t, func(args ...string) error {
		if args[0] == "bootstrap" {
			return assertiveErr("kickstart failed")
		}
		return nil
	}, func(args ...string) ([]byte, error) {
		return []byte("Could not find service"), assertiveErr("missing")
	})

	installer := &launchdInstaller{
		label:     launchdServiceLabel,
		plistFile: plistFile,
		binPath:   "/usr/local/bin/agentteam",
	}
	err := installer.Install(InstallOptions{Start: true})
	require.Error(t, err)
	require.NoFileExists(t, plistFile)
}
