package agent

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

func TestWaitProcessSuppressesRuntimeExitEventForCleanExit(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return nil
	})

	h.process.waitProcess()

	require.Empty(t, h.events)
	exitCode, _ := h.process.ExitSummary()
	require.Equal(t, 0, exitCode)
}

func TestWaitForExitKeepsChildSemanticsWhileExitCallbackHasIndependentFence(t *testing.T) {
	callbackStarted := make(chan struct{})
	callbackRelease := make(chan struct{})
	var releaseOnce sync.Once
	release := func() { releaseOnce.Do(func() { close(callbackRelease) }) }
	t.Cleanup(release)

	process, err := NewProcess(
		t.Context(),
		&launchStateCharacterizationDriver{name: "builtin"},
		drivercontract.LaunchConfig{AgentID: "agent-callback-fence", WorkDir: t.TempDir()},
		ProcessOptions{OnExit: func(*Process) {
			close(callbackStarted)
			<-callbackRelease
		}},
	)
	require.NoError(t, err)

	process.Stop()
	require.True(t, process.WaitForExit(time.Second), "child-exit fence must not inherit onExit latency")
	select {
	case <-callbackStarted:
	case <-time.After(time.Second):
		t.Fatal("exit callback did not start")
	}

	shortCtx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
	defer cancel()
	require.False(t, process.WaitForExitCallback(shortCtx), "callback fence must remain open while onExit is blocked")

	release()
	joinCtx, joinCancel := context.WithTimeout(context.Background(), time.Second)
	defer joinCancel()
	require.True(t, process.WaitForExitCallback(joinCtx))
}

func TestNewProcessRevokesAnalysisTokenOnSpawnFailureAndExit(t *testing.T) {
	previous := revokeRuntimeAnalysisToken
	var (
		mu      sync.Mutex
		revoked []string
	)
	revokeRuntimeAnalysisToken = func(value string) {
		mu.Lock()
		revoked = append(revoked, value)
		mu.Unlock()
	}
	t.Cleanup(func() { revokeRuntimeAnalysisToken = previous })

	_, err := NewProcess(
		t.Context(),
		deliveryTestDriver{},
		drivercontract.LaunchConfig{
			AgentID:       "agent-spawn-failure",
			WorkDir:       t.TempDir(),
			AnalysisToken: "analysis-failed",
		},
		ProcessOptions{},
	)
	require.Error(t, err)

	process, err := NewProcess(
		t.Context(),
		&launchStateCharacterizationDriver{name: "builtin"},
		drivercontract.LaunchConfig{
			AgentID:       "agent-exit",
			WorkDir:       t.TempDir(),
			AnalysisToken: "analysis-live",
		},
		ProcessOptions{},
	)
	require.NoError(t, err)
	process.Stop()
	joinCtx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	require.True(t, process.WaitForExitCallback(joinCtx))

	mu.Lock()
	defer mu.Unlock()
	require.ElementsMatch(t, []string{"analysis-failed", "analysis-live"}, revoked)
}

func TestWaitProcessSuppressesRuntimeExitEventAfterStop(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return errors.New("terminated")
	})

	h.process.Stop()
	h.process.waitProcess()

	require.Empty(t, h.events)
	exitCode, _ := h.process.ExitSummary()
	require.NotZero(t, exitCode)
}

func TestWaitProcessSuppressesWakeFailureAfterStop(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return errors.New("signal: terminated")
	})
	h.process.mu.Lock()
	h.process.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	h.process.pendingWakeToken = "wake-token"
	h.process.mu.Unlock()

	h.process.Stop()
	h.process.waitProcess()

	require.Empty(t, h.wakeEvents)
	require.Empty(t, h.events)
	exitCode, confirmed := h.process.ExitSummary()
	require.NotZero(t, exitCode)
	require.False(t, confirmed)
}

func TestWaitProcessSuppressesRuntimeExitEventAfterSleep(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return errors.New("terminated")
	})

	h.process.Sleep()
	h.process.waitProcess()

	require.Empty(t, h.events)
	require.True(t, h.process.IsSleeping())
	exitCode, _ := h.process.ExitSummary()
	require.NotZero(t, exitCode)
}

func TestWaitProcessEmitsRuntimeExitEventForUnexpectedNonzeroExit(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return errors.New("exit status 7")
	})

	h.process.waitProcess()

	require.Len(t, h.events, 1)
	require.Equal(t, consts.RuntimeEventClassRuntimeExitedUnexpectedly, h.events[0].EventClass)
	require.Equal(t, "exit status 7", h.events[0].Detail)
}

func TestWaitProcessEmitsWakeFailureForUnexpectedCleanExitWithPendingWake(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return nil
	})
	h.process.mu.Lock()
	h.process.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	h.process.pendingWakeToken = "wake-token"
	h.process.mu.Unlock()

	h.process.waitProcess()

	require.Len(t, h.wakeEvents, 1)
	require.Equal(t, "failed", h.wakeEvents[0].Status)
	require.Equal(t, "process exited with code 0", h.wakeEvents[0].Detail)
	require.Len(t, h.events, 1)
	require.Equal(t, consts.RuntimeEventClassRuntimeExitedUnexpectedly, h.events[0].EventClass)
	require.Equal(t, "process exited with code 0", h.events[0].Detail)
}

func TestRuntimeErrorFailsPendingWakeAndCleanExitDoesNotOverwriteIt(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return nil
	})
	h.process.stderrTail = func() string { return "claude stderr diagnostic" }
	h.process.mu.Lock()
	h.process.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	h.process.pendingWakeToken = "wake-token"
	h.process.mu.Unlock()

	h.process.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "runtime said no",
	})
	h.process.handleParsedEvent(drivercontract.ParsedEvent{
		Kind: consts.ParsedEventKindTurnEnd,
	})
	h.process.waitProcess()

	require.Len(t, h.wakeEvents, 1)
	require.Equal(t, consts.WakeStatusFailed, h.wakeEvents[0].Status)
	require.Contains(t, h.wakeEvents[0].Detail, "runtime said no")
	require.Contains(t, h.wakeEvents[0].Detail, "claude stderr diagnostic")
	require.Len(t, h.events, 1)
	require.Equal(t, consts.RuntimeEventClassWakeFailed, h.events[0].EventClass)
	require.Contains(t, h.events[0].Detail, "runtime said no")
	exitCode, confirmed := h.process.ExitSummary()
	require.Equal(t, 0, exitCode)
	require.False(t, confirmed)
}

func TestRuntimeErrorSanitizesOpenCodeSessionStorageFailure(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return nil
	})
	h.process.stderrTail = func() string {
		return `ERROR service=session.processor error=Failed query: insert into "part" ("id", "message_id", "session_id", "time_created", "time_updated", "data") values (?, ?, ?, ?, ?, ?) on conflict ("part"."id") do update set "time_updated" = ?, "data" = ?`
	}
	h.process.mu.Lock()
	h.process.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	h.process.pendingWakeToken = "wake-token"
	h.process.mu.Unlock()

	var parsed []drivercontract.ParsedEvent
	h.process.SetOnEvent(func(ev drivercontract.ParsedEvent) {
		parsed = append(parsed, ev)
	})

	rawMessage := `Failed query: insert into "part" ("id", "message_id", "session_id", "time_created", "time_updated", "data") values (?, ?, ?, ?, ?, ?) on conflict ("part"."id") do update set "time_updated" = ?, "data" = ?`
	h.process.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: rawMessage,
	})

	require.Len(t, h.wakeEvents, 1)
	require.Equal(t, consts.WakeStatusFailed, h.wakeEvents[0].Status)
	require.Contains(t, h.wakeEvents[0].Detail, "OpenCode runtime failed while writing local session data")
	require.Contains(t, h.wakeEvents[0].Detail, "opencode_session_storage_error")
	require.NotContains(t, h.wakeEvents[0].Detail, `insert into "part"`)
	require.NotContains(t, h.wakeEvents[0].Detail, "stderr:")
	require.Len(t, h.events, 1)
	require.Equal(t, h.wakeEvents[0].Detail, h.events[0].Detail)

	require.Len(t, parsed, 1)
	require.Equal(t, h.wakeEvents[0].Detail, parsed[0].Message)
	require.NotContains(t, parsed[0].Message, `insert into "part"`)
	require.Equal(t, openCodeSessionStorageErrorCode, parsed[0].Metadata["runtime_error_code"])
	require.NotEmpty(t, parsed[0].Metadata["runtime_error_detail_fingerprint"])
	require.NotZero(t, parsed[0].Metadata["runtime_error_detail_bytes"])
	require.NotContains(t, fmt.Sprint(parsed[0].Metadata), `insert into "part"`)
}

func TestRuntimeErrorWithoutPendingWakeEmitsRuntimeFailure(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error {
		return nil
	})

	h.process.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "runtime said no",
	})
	h.process.waitProcess()

	require.Empty(t, h.wakeEvents)
	require.Len(t, h.events, 1)
	require.Equal(t, consts.RuntimeEventClassRuntimeExitedUnexpectedly, h.events[0].EventClass)
	require.Equal(t, "runtime said no", h.events[0].Detail)
}

func TestSubagentActivityIsForwardedWithoutChangingPrimaryProcessState(t *testing.T) {
	h := newRuntimeExitTestHarness(t, func() error { return nil })
	ap := h.process
	wake := &pendingDelivery{Wake: WakeInfo{MessageID: "msg-primary", Target: "#agentteam:thread", DeliveryClass: DeliveryClassVisibleReply}, Status: pendingDeliveryAwaitingCheck}
	ap.mu.Lock()
	ap.SessionID = "thread-primary"
	ap.isIdle = true
	ap.pendingWake = wake
	ap.pendingWakeToken = "wake-token"
	ap.runtimeHandoffEpoch = 7
	ap.inbox = []types.InboxMessage{{MessageID: "queued-message"}}
	ap.mu.Unlock()

	var parsed []drivercontract.ParsedEvent
	ap.SetOnEvent(func(ev drivercontract.ParsedEvent) {
		parsed = append(parsed, ev)
	})

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindSubagentActivity,
		Message: "Started /root/reviewer",
		Metadata: map[string]any{
			"agentThreadId": "thread-child",
			"agentPath":     "/root/reviewer",
		},
	})

	require.Len(t, parsed, 1)
	require.Equal(t, consts.ParsedEventKindSubagentActivity, parsed[0].Kind)
	require.Equal(t, "thread-primary", parsed[0].SessionID)
	require.Empty(t, h.wakeEvents)

	ap.mu.Lock()
	defer ap.mu.Unlock()
	require.True(t, ap.isIdle, "parent-side activity projection must not manufacture a busy root turn")
	require.Same(t, wake, ap.pendingWake)
	require.Equal(t, "wake-token", ap.pendingWakeToken)
	require.Equal(t, uint64(7), ap.runtimeHandoffEpoch)
	require.Equal(t, []types.InboxMessage{{MessageID: "queued-message"}}, ap.inbox)
}

func TestNewProcessEmitsSpawnPreparedRecoveryPointWhenReporterAttaches(t *testing.T) {
	driver := &spawnPreparedRecoveryPointDriver{}
	ap, err := NewProcess(
		t.Context(),
		driver,
		drivercontract.LaunchConfig{
			AgentID:     "agent-spawn-recovery",
			RuntimeType: runtimeconfig.TypeOpenCode,
			WorkDir:     t.TempDir(),
		},
		ProcessOptions{},
	)
	require.NoError(t, err)
	defer ap.WaitForExit(time.Second)

	var prepared []types.RecoveryPointPreparedEvent
	ap.SetOnRecoveryPoint(func(ev types.RecoveryPointPreparedEvent) {
		prepared = append(prepared, ev)
	})

	require.Equal(t, []types.RecoveryPointPreparedEvent{{
		AgentID:     "agent-spawn-recovery",
		MessageID:   "msg-initial",
		ThreadID:    "thread-initial",
		Strategy:    "codex_app_server_turns",
		RecoveryRef: `{"threadId":"thread-initial","turnCount":0}`,
	}}, prepared)
}

func TestNewProcessInstallsRuntimeEventCallbackBeforeStdoutParsing(t *testing.T) {
	events := make(chan RuntimeEvent, 1)
	ap, err := NewProcess(
		t.Context(),
		&stdoutRuntimeErrorDriver{},
		drivercontract.LaunchConfig{
			AgentID:     "agent-early-runtime-event",
			RuntimeType: runtimeconfig.TypeOpenCode,
			WorkDir:     t.TempDir(),
		},
		ProcessOptions{OnRuntimeEvent: func(ev RuntimeEvent) {
			events <- ev
		}},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		ap.Stop()
		require.True(t, ap.WaitForExit(time.Second))
	})

	select {
	case ev := <-events:
		require.Equal(t, consts.RuntimeEventClassRuntimeExitedUnexpectedly, ev.EventClass)
		require.Equal(t, "runtime failed before callback attach would have completed", ev.Detail)
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for early runtime event")
	}
}

func TestNewProcessCharacterizesLaunchStateAndWakeTracking(t *testing.T) {
	driver := &launchStateCharacterizationDriver{name: "codex"}
	workDir := t.TempDir()
	var wakeEvents []WakeStatusEvent

	ap, err := NewProcess(
		t.Context(),
		driver,
		drivercontract.LaunchConfig{
			AgentID:     "agent-launch-state",
			RuntimeType: runtimeconfig.TypeCodex,
			WorkDir:     workDir,
		},
		ProcessOptions{Wake: &WakeInfo{MessageID: "msg-wake", Target: "#agentteam:thread", MessageType: "message"}, OnWakeStatus: func(ev WakeStatusEvent) {
			wakeEvents = append(wakeEvents, ev)
		}},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		ap.Stop()
		require.True(t, ap.WaitForExit(time.Second))
	})

	require.Equal(t, "running", ap.State())
	require.True(t, ap.IsIdle(), "launch without an initial prompt starts in idle drain mode")
	require.Equal(t, "codex", ap.DriverName())
	require.Equal(t, workDir, ap.WorkDir())
	require.Len(t, wakeEvents, 1)
	require.Equal(t, WakeStatusEvent{
		MessageID: "msg-wake",
		WakeToken: wakeEvents[0].WakeToken,
		Target:    "#agentteam:thread",
		Status:    consts.WakeStatusReceived,
		Detail:    "agent starting",
	}, wakeEvents[0])
	require.NotEmpty(t, wakeEvents[0].WakeToken)
}

func TestNewProcessCharacterizesInitialTurnState(t *testing.T) {
	ap, err := NewProcess(
		t.Context(),
		&launchStateCharacterizationDriver{name: "opencode"},
		testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-initial-turn", RuntimeType: runtimeconfig.TypeOpenCode, WorkDir: t.TempDir()}, testLaunchSurfaces{Prompt: "initial task"}),
		ProcessOptions{},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		ap.Stop()
		require.True(t, ap.WaitForExit(time.Second))
	})

	require.Equal(t, "running", ap.State())
	require.False(t, ap.IsIdle(), "launch with an initial prompt owns an active turn until turn_end")
	require.Equal(t, "opencode", ap.DriverName())
}

func TestPrepareSpawnRecoveryPointUsesRecoveryDriverState(t *testing.T) {
	driver := &spawnRecoveryCapableDriver{
		strategy:    "opencode_session_revert",
		recoveryRef: `{"sessionId":"sess-initial","messageId":"msg_1"}`,
	}

	point := prepareSpawnRecoveryPoint(driver, " msg-initial ", " sess-initial ")

	require.Equal(t, &drivercontract.PreparedRecoveryPoint{
		MessageID:   "msg-initial",
		ThreadID:    "sess-initial",
		Strategy:    "opencode_session_revert",
		RecoveryRef: `{"sessionId":"sess-initial","messageId":"msg_1"}`,
	}, point)
	require.Equal(t, 1, driver.prepareCalls)
}

func TestPrepareSpawnRecoveryPointPrefersMessageBoundRecoveryDriver(t *testing.T) {
	driver := &messageBoundRecoveryCapableDriver{
		spawnRecoveryCapableDriver: spawnRecoveryCapableDriver{
			strategy:    "opencode_session_revert",
			recoveryRef: `{"sessionId":"sess-initial","messageId":"msg_bound"}`,
		},
	}

	point := prepareSpawnRecoveryPoint(driver, " msg-initial ", " sess-initial ")

	require.Equal(t, &drivercontract.PreparedRecoveryPoint{
		MessageID:   "msg-initial",
		ThreadID:    "sess-initial",
		Strategy:    "opencode_session_revert",
		RecoveryRef: `{"sessionId":"sess-initial","messageId":"msg_bound"}`,
	}, point)
	require.Equal(t, []string{"msg-initial"}, driver.messageIDs)
	require.Equal(t, 0, driver.prepareCalls)
}

type runtimeExitTestHarness struct {
	process    *Process
	events     []RuntimeEvent
	wakeEvents []WakeStatusEvent
}

type spawnPreparedRecoveryPointDriver struct{}

func (d *spawnPreparedRecoveryPointDriver) Name() string { return "opencode" }

func (d *spawnPreparedRecoveryPointDriver) SupportsStdinNotification() bool { return true }

func (d *spawnPreparedRecoveryPointDriver) MCPToolPrefix() string { return "mcp_agentteam_" }

func (d *spawnPreparedRecoveryPointDriver) Spawn(context.Context, drivercontract.LaunchConfig) (drivercontract.SpawnResult, error) {
	return drivercontract.SpawnResult{
		Cmd:    exec.Command("true"),
		Stdout: io.NopCloser(strings.NewReader("")),
		Wait:   func() error { return nil },
		PreparedRecoveryPoint: &drivercontract.PreparedRecoveryPoint{
			MessageID:   " msg-initial ",
			ThreadID:    " thread-initial ",
			Strategy:    " codex_app_server_turns ",
			RecoveryRef: `{"threadId":"thread-initial","turnCount":0}`,
		},
	}, nil
}

func (d *spawnPreparedRecoveryPointDriver) ParseLine(string) []drivercontract.ParsedEvent { return nil }

func (d *spawnPreparedRecoveryPointDriver) EncodeStdinMessage(text, _ string) []byte {
	return []byte(text)
}

type stdoutRuntimeErrorDriver struct{}

func (d *stdoutRuntimeErrorDriver) Name() string { return "opencode" }

func (d *stdoutRuntimeErrorDriver) SupportsStdinNotification() bool { return true }

func (d *stdoutRuntimeErrorDriver) MCPToolPrefix() string { return "mcp_agentteam_" }

func (d *stdoutRuntimeErrorDriver) Spawn(ctx context.Context, _ drivercontract.LaunchConfig) (drivercontract.SpawnResult, error) {
	return drivercontract.SpawnResult{
		Stdout:    io.NopCloser(strings.NewReader("runtime-error\n")),
		Wait:      func() error { <-ctx.Done(); return nil },
		Terminate: func() {},
	}, nil
}

func (d *stdoutRuntimeErrorDriver) ParseLine(string) []drivercontract.ParsedEvent {
	return []drivercontract.ParsedEvent{{
		Kind:    consts.ParsedEventKindError,
		Message: "runtime failed before callback attach would have completed",
	}}
}

func (d *stdoutRuntimeErrorDriver) EncodeStdinMessage(text, _ string) []byte {
	return []byte(text)
}

type launchStateCharacterizationDriver struct {
	name string
}

func (d *launchStateCharacterizationDriver) Name() string { return d.name }

func (d *launchStateCharacterizationDriver) SupportsStdinNotification() bool { return true }

func (d *launchStateCharacterizationDriver) MCPToolPrefix() string { return "mcp_agentteam_" }

func (d *launchStateCharacterizationDriver) Spawn(ctx context.Context, _ drivercontract.LaunchConfig) (drivercontract.SpawnResult, error) {
	return drivercontract.SpawnResult{
		Stdout:    io.NopCloser(strings.NewReader("")),
		Wait:      func() error { <-ctx.Done(); return nil },
		Terminate: func() {},
	}, nil
}

func (d *launchStateCharacterizationDriver) ParseLine(string) []drivercontract.ParsedEvent {
	return nil
}

func (d *launchStateCharacterizationDriver) EncodeStdinMessage(text, _ string) []byte {
	return []byte(text)
}

type spawnRecoveryCapableDriver struct {
	strategy     string
	recoveryRef  string
	prepareCalls int
}

func (d *spawnRecoveryCapableDriver) Name() string { return "opencode" }

func (d *spawnRecoveryCapableDriver) SupportsStdinNotification() bool { return true }

func (d *spawnRecoveryCapableDriver) MCPToolPrefix() string { return "mcp_agentteam_" }

func (d *spawnRecoveryCapableDriver) Spawn(context.Context, drivercontract.LaunchConfig) (drivercontract.SpawnResult, error) {
	return drivercontract.SpawnResult{}, nil
}

func (d *spawnRecoveryCapableDriver) ParseLine(string) []drivercontract.ParsedEvent { return nil }

func (d *spawnRecoveryCapableDriver) EncodeStdinMessage(text, _ string) []byte {
	return []byte(text)
}

func (d *spawnRecoveryCapableDriver) PrepareRecoveryPoint() (string, string, bool, error) {
	d.prepareCalls++
	return d.strategy, d.recoveryRef, true, nil
}

func (d *spawnRecoveryCapableDriver) RestoreRecoveryPoint(context.Context, string, string) error {
	return nil
}

type messageBoundRecoveryCapableDriver struct {
	spawnRecoveryCapableDriver
	messageIDs []string
}

func (d *messageBoundRecoveryCapableDriver) PrepareRecoveryPointForMessage(messageID string) (string, string, bool, error) {
	d.messageIDs = append(d.messageIDs, messageID)
	return d.strategy, d.recoveryRef, true, nil
}

func newRuntimeExitTestHarness(t *testing.T, waitFunc func() error) *runtimeExitTestHarness {
	t.Helper()

	_, cancel := context.WithCancel(t.Context())
	h := &runtimeExitTestHarness{}
	ap := &Process{
		AgentID: "agent-runtime-exit",
		processRuntimeBinding: processRuntimeBinding{driver: &spawnPreparedRecoveryPointDriver{},
			waitFunc:      waitFunc,
			terminateFunc: func() {},
			cancel:        cancel},
		processLifecycleState: processLifecycleState{exited: make(chan struct{}),
			exitCode: -1}}

	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		h.events = append(h.events, ev)
	}
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		h.wakeEvents = append(h.wakeEvents, ev)
	}
	h.process = ap
	return h
}
