package worker_test

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"reflect"
	"strings"
	"testing"
	"time"

	"git.toki-labs.com/toki/gito/services/core/internal/config"
	"git.toki-labs.com/toki/gito/services/core/internal/core"
	"git.toki-labs.com/toki/gito/services/core/internal/events"
	"git.toki-labs.com/toki/gito/services/core/internal/storage"
	"git.toki-labs.com/toki/gito/services/core/internal/worker"
)

type fakePicker struct {
	pickFn func(ctx context.Context, now time.Time) (core.Operation, bool, error)
	calls  int
}

func (f *fakePicker) PickQueuedOperation(ctx context.Context, now time.Time) (core.Operation, bool, error) {
	f.calls++
	if f.pickFn != nil {
		return f.pickFn(ctx, now)
	}
	return core.Operation{}, false, nil
}

type fakeClock struct {
	now time.Time
}

func (f fakeClock) Now() time.Time {
	return f.now
}

type fakeOperationStore struct {
	operation      core.Operation
	startedCalls   int
	succeededCalls int
	failedCalls    int
	cancelledCalls int
}

func (f *fakeOperationStore) CreateOperation(_ context.Context, op core.Operation) (storage.OperationCreateResult, error) {
	f.operation = op
	return storage.OperationCreateResult{Operation: op, Created: true}, nil
}
func (f *fakeOperationStore) GetOperation(_ context.Context, id string) (core.Operation, error) {
	if f.operation.ID != id {
		return core.Operation{}, storage.ErrOperationNotFound
	}
	return f.operation, nil
}
func (f *fakeOperationStore) ListOperations(_ context.Context, _ string) ([]core.Operation, error) {
	return nil, nil
}
func (f *fakeOperationStore) StartOperation(_ context.Context, id string, now time.Time) (core.Operation, error) {
	f.startedCalls++
	return f.transition(id, core.OperationRunning, now)
}
func (f *fakeOperationStore) SucceedOperation(_ context.Context, id string, now time.Time) (core.Operation, error) {
	f.succeededCalls++
	return f.transition(id, core.OperationSucceeded, now)
}
func (f *fakeOperationStore) FailOperation(_ context.Context, id string, now time.Time) (core.Operation, error) {
	f.failedCalls++
	return f.transition(id, core.OperationFailed, now)
}
func (f *fakeOperationStore) CancelOperation(_ context.Context, id string, now time.Time) (core.Operation, error) {
	f.cancelledCalls++
	return f.transition(id, core.OperationCancelled, now)
}
func (f *fakeOperationStore) PickQueuedOperation(_ context.Context, _ time.Time) (core.Operation, bool, error) {
	return core.Operation{}, false, nil
}
func (f *fakeOperationStore) PickQueuedAgentRunOperation(_ context.Context, _ time.Time) (core.Operation, bool, error) {
	return core.Operation{}, false, nil
}
func (f *fakeOperationStore) transition(id string, next core.OperationState, now time.Time) (core.Operation, error) {
	if f.operation.ID != id {
		return core.Operation{}, storage.ErrOperationNotFound
	}
	if !f.operation.State.CanTransitionTo(next) {
		return core.Operation{}, storage.ErrInvalidOperation
	}
	f.operation.State = next
	f.operation.UpdatedAt = now
	return f.operation, nil
}

type fakeOperationEventStore struct {
	events []storage.OperationEvent
}

func (f *fakeOperationEventStore) AppendEvent(_ context.Context, ev storage.OperationEvent) error {
	f.events = append(f.events, ev)
	return nil
}
func (f *fakeOperationEventStore) ListEvents(_ context.Context, _ string) ([]storage.OperationEvent, error) {
	return f.events, nil
}
func (f *fakeOperationEventStore) ListPendingEvents(_ context.Context, _ int) ([]storage.OperationEvent, error) {
	return f.events, nil
}
func (f *fakeOperationEventStore) MarkPublished(_ context.Context, _ string, _ time.Time) (storage.OperationEvent, error) {
	return storage.OperationEvent{}, nil
}

type fakeAgentRunInputLoader struct {
	input worker.AgentRunInput
	err   error
	calls int
}

func (f *fakeAgentRunInputLoader) LoadAgentRunInput(_ context.Context, op core.Operation) (worker.AgentRunInput, error) {
	f.calls++
	if f.err != nil {
		return worker.AgentRunInput{}, f.err
	}
	if f.input.OperationID == "" {
		f.input.OperationID = op.ID
	}
	if f.input.RepoID == "" {
		f.input.RepoID = op.RepoID
	}
	return f.input, nil
}

type fakeAgentRunInvoker struct {
	result worker.AgentRunResult
	err    error
	calls  int
}

func (f *fakeAgentRunInvoker) InvokeAgentRun(_ context.Context, _ worker.AgentRunInput) (worker.AgentRunResult, error) {
	f.calls++
	return f.result, f.err
}

type fakeGitRunner struct {
	outputs []string
	errs    []error
	calls   []fakeGitCall
}

type fakeGitCall struct {
	workdir string
	args    []string
}

func (f *fakeGitRunner) Run(workdir string, args ...string) (string, error) {
	f.calls = append(f.calls, fakeGitCall{
		workdir: workdir,
		args:    append([]string(nil), args...),
	})
	callIndex := len(f.calls) - 1
	var output string
	if callIndex < len(f.outputs) {
		output = f.outputs[callIndex]
	}
	var err error
	if callIndex < len(f.errs) {
		err = f.errs[callIndex]
	}
	return output, err
}

type fakeRevisionScanner struct {
	result worker.RevisionScanResult
	err    error
	calls  []worker.RevisionScanRequest
}

func (f *fakeRevisionScanner) ScanAgentRunRevision(ctx context.Context, req worker.RevisionScanRequest) (worker.RevisionScanResult, error) {
	f.calls = append(f.calls, req)
	return f.result, f.err
}

func decodePayload(t *testing.T, raw []byte) map[string]any {
	t.Helper()
	payload := map[string]any{}
	if err := json.Unmarshal(raw, &payload); err != nil {
		t.Fatalf("decode payload: %v", err)
	}
	return payload
}

func runningAgentRunOperation(id string) core.Operation {
	return core.Operation{
		ID:     id,
		RepoID: "repo-1",
		Type:   core.OperationAgentRun,
		State:  core.OperationRunning,
	}
}

func TestRunnerRunOnceSkipsWhenDisabled(t *testing.T) {
	cfg := config.Config{WorkerEnabled: false}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	picker := &fakePicker{}
	clock := fakeClock{now: time.Now()}

	runner := worker.NewRunner(cfg, logger, picker, clock, nil)
	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if picker.calls != 0 {
		t.Errorf("picker called %d times, expected 0 when disabled", picker.calls)
	}
}

func TestRunnerRunOnceNoPicker(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	clock := fakeClock{now: time.Now()}

	runner := worker.NewRunner(cfg, logger, nil, clock, nil)
	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error when picker is nil: %v", err)
	}
}

func TestRunnerRunOnceReportsNoWork(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return core.Operation{}, false, nil
		},
	}
	clock := fakeClock{now: time.Now()}

	runner := worker.NewRunner(cfg, logger, picker, clock, nil)
	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if picker.calls != 1 {
		t.Errorf("expected picker to be called once, got %d", picker.calls)
	}
}

func TestRunnerRunOnceNonAgentOperationKeepsExistingBehavior(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	opStore := &fakeOperationStore{}
	eventStore := &fakeOperationEventStore{}
	expectedOp := core.Operation{ID: "op-test-1", Type: core.OperationClone}
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return expectedOp, true, nil
		},
	}
	clock := fakeClock{now: time.Now()}

	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: &fakeAgentRunInputLoader{},
		AgentRunInvoker:     &fakeAgentRunInvoker{},
	})
	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if picker.calls != 1 {
		t.Errorf("expected picker to be called once, got %d", picker.calls)
	}
	if opStore.startedCalls != 0 || opStore.succeededCalls != 0 || opStore.failedCalls != 0 || opStore.cancelledCalls != 0 {
		t.Errorf("agent_run state transitions should not run for non-agent operation")
	}
	if len(eventStore.events) != 0 {
		t.Fatalf("expected no events for non-agent operation, got %d", len(eventStore.events))
	}
}

func TestRunnerRunOnceReturnsPickerError(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	expectedErr := errors.New("db error")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return core.Operation{}, false, expectedErr
		},
	}
	clock := fakeClock{now: time.Now()}

	runner := worker.NewRunner(cfg, logger, picker, clock, nil)
	err := runner.RunOnce(context.Background())
	if !errors.Is(err, expectedErr) {
		t.Fatalf("expected error %v, got %v", expectedErr, err)
	}
}

func TestRunnerRunOnceAgentRunInvokesBackendAndLogsEvents(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-success")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/tmp/workspace",
			ExpectedRevision: "before-revision",
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{
			State:   worker.AgentRunStateSucceeded,
			Message: "done",
		},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{
			"",
			"before-revision\n",
			"",
		},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if loader.calls != 1 {
		t.Fatalf("expected loader call count 1, got %d", loader.calls)
	}
	if invoker.calls != 1 {
		t.Fatalf("expected invoker call count 1, got %d", invoker.calls)
	}
	if opStore.startedCalls != 0 || opStore.succeededCalls != 1 {
		t.Fatalf("expected no start and one succeed call, got start=%d succeed=%d", opStore.startedCalls, opStore.succeededCalls)
	}
	if opStore.failedCalls != 0 || opStore.cancelledCalls != 0 {
		t.Fatalf("did not expect failure/cancel calls, got fail=%d cancel=%d", opStore.failedCalls, opStore.cancelledCalls)
	}
	if opStore.operation.State != core.OperationSucceeded {
		t.Fatalf("operation state mismatch: got %q want %q", opStore.operation.State, core.OperationSucceeded)
	}
	if len(eventStore.events) != 4 {
		t.Fatalf("expected 4 events, got %d", len(eventStore.events))
	}

	wantEventTypes := []string{
		events.OperationStarted,
		events.AgentRunStarted,
		events.OperationCompleted,
		events.AgentRunCompleted,
	}
	for idx, ev := range eventStore.events {
		if ev.Event.Type != wantEventTypes[idx] {
			t.Fatalf("event[%d]: got %q want %q", idx, ev.Event.Type, wantEventTypes[idx])
		}
		payload := decodePayload(t, ev.Event.Payload)
		if got := payload["operation_id"]; got != "agent-run-success" {
			t.Fatalf("event[%d]: operation_id mismatch: got %v want %v", idx, got, "agent-run-success")
		}
	}

	startState := decodePayload(t, eventStore.events[0].Event.Payload)["state"]
	if startState != string(core.OperationRunning) {
		t.Fatalf("event started state mismatch: got %v want %q", startState, core.OperationRunning)
	}
	endState := decodePayload(t, eventStore.events[2].Event.Payload)["state"]
	if endState != string(core.OperationSucceeded) {
		t.Fatalf("event completed state mismatch: got %v want %q", endState, core.OperationSucceeded)
	}
}

func TestRunnerRunOnceAgentRunRecordsGitEvidence(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-evidence")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "before-revision",
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{
			State:   worker.AgentRunStateSucceeded,
			Message: "done",
		},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{
			"",
			"after-revision\n",
			"A\tREADME.md\nM\tservices/core/internal/worker/runner.go\n",
		},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 1 || opStore.failedCalls != 0 {
		t.Fatalf("expected one succeed and no fail call, got succeed=%d fail=%d", opStore.succeededCalls, opStore.failedCalls)
	}
	if len(eventStore.events) != 4 {
		t.Fatalf("expected 4 events, got %d", len(eventStore.events))
	}

	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["status_clean"] != true {
		t.Fatalf("status_clean: got %v want true", completedPayload["status_clean"])
	}
	if completedPayload["status_output"] != "" {
		t.Fatalf("status_output: got %v want empty string", completedPayload["status_output"])
	}
	if completedPayload["before_revision"] != "before-revision" {
		t.Fatalf("before_revision: got %v want %q", completedPayload["before_revision"], "before-revision")
	}
	if completedPayload["after_revision"] != "after-revision" {
		t.Fatalf("after_revision: got %v want %q", completedPayload["after_revision"], "after-revision")
	}
	changedFiles, ok := completedPayload["changed_files"].([]any)
	if !ok {
		t.Fatalf("changed_files has type %T, want []any", completedPayload["changed_files"])
	}
	if len(changedFiles) != 2 {
		t.Fatalf("changed_files len: got %d want 2", len(changedFiles))
	}
	firstFile, ok := changedFiles[0].(map[string]any)
	if !ok {
		t.Fatalf("changed_files[0] has type %T, want map[string]any", changedFiles[0])
	}
	if firstFile["path"] != "README.md" || firstFile["change_type"] != "added" {
		t.Fatalf("changed_files[0]: got %#v", firstFile)
	}
	secondFile, ok := changedFiles[1].(map[string]any)
	if !ok {
		t.Fatalf("changed_files[1] has type %T, want map[string]any", changedFiles[1])
	}
	if secondFile["path"] != "services/core/internal/worker/runner.go" || secondFile["change_type"] != "modified" {
		t.Fatalf("changed_files[1]: got %#v", secondFile)
	}

	wantCalls := []fakeGitCall{
		{workdir: "/repo/worktree", args: []string{"status", "--porcelain"}},
		{workdir: "/repo/worktree", args: []string{"rev-parse", "HEAD"}},
		{workdir: "/repo/worktree", args: []string{"diff", "--name-status", "before-revision..after-revision"}},
	}
	if !reflect.DeepEqual(gitRunner.calls, wantCalls) {
		t.Fatalf("git calls:\n got %#v\nwant %#v", gitRunner.calls, wantCalls)
	}
}

func TestRunnerRunOnceAgentRunFailsWhenGitEvidenceIsDirty(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-dirty")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "before-revision",
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{
			State:   worker.AgentRunStateSucceeded,
			Message: "done",
		},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{
			" M README.md\n?? scratch.txt\n",
			"after-revision\n",
			"M\tREADME.md\n",
		},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 0 || opStore.failedCalls != 1 {
		t.Fatalf("expected no succeed and one fail call, got succeed=%d fail=%d", opStore.succeededCalls, opStore.failedCalls)
	}
	if opStore.operation.State != core.OperationFailed {
		t.Fatalf("operation state mismatch: got %q want %q", opStore.operation.State, core.OperationFailed)
	}
	if len(eventStore.events) != 4 {
		t.Fatalf("expected 4 events, got %d", len(eventStore.events))
	}
	if eventStore.events[2].Event.Type != events.OperationFailed {
		t.Fatalf("completion event type: got %q want %q", eventStore.events[2].Event.Type, events.OperationFailed)
	}

	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["state"] != string(core.OperationFailed) {
		t.Fatalf("completion state mismatch: got %v want %q", completedPayload["state"], core.OperationFailed)
	}
	if completedPayload["message"] != "workspace dirty after agent run" {
		t.Fatalf("message: got %v want %q", completedPayload["message"], "workspace dirty after agent run")
	}
	if completedPayload["status_clean"] != false {
		t.Fatalf("status_clean: got %v want false", completedPayload["status_clean"])
	}
	if completedPayload["status_output"] != " M README.md\n?? scratch.txt\n" {
		t.Fatalf("status_output: got %q", completedPayload["status_output"])
	}
	if completedPayload["before_revision"] != "before-revision" {
		t.Fatalf("before_revision: got %v want %q", completedPayload["before_revision"], "before-revision")
	}
	if completedPayload["after_revision"] != "after-revision" {
		t.Fatalf("after_revision: got %v want %q", completedPayload["after_revision"], "after-revision")
	}
	changedFiles, ok := completedPayload["changed_files"].([]any)
	if !ok {
		t.Fatalf("changed_files has type %T, want []any", completedPayload["changed_files"])
	}
	if len(changedFiles) != 1 {
		t.Fatalf("changed_files len: got %d want 1", len(changedFiles))
	}
	firstFile, ok := changedFiles[0].(map[string]any)
	if !ok {
		t.Fatalf("changed_files[0] has type %T, want map[string]any", changedFiles[0])
	}
	if firstFile["path"] != "README.md" || firstFile["change_type"] != "modified" {
		t.Fatalf("changed_files[0]: got %#v", firstFile)
	}

	wantCalls := []fakeGitCall{
		{workdir: "/repo/worktree", args: []string{"status", "--porcelain"}},
		{workdir: "/repo/worktree", args: []string{"rev-parse", "HEAD"}},
		{workdir: "/repo/worktree", args: []string{"diff", "--name-status", "before-revision..after-revision"}},
	}
	if !reflect.DeepEqual(gitRunner.calls, wantCalls) {
		t.Fatalf("git calls:\n got %#v\nwant %#v", gitRunner.calls, wantCalls)
	}
}

func TestRunnerRunOnceAgentRunFailsWhenGitEvidenceFails(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-evidence-fail")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "before-revision",
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{
			State:   worker.AgentRunStateSucceeded,
			Message: "done",
		},
	}
	gitRunner := &fakeGitRunner{
		errs: []error{errors.New("git status failed")},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 0 || opStore.failedCalls != 1 {
		t.Fatalf("expected no succeed and one fail call, got succeed=%d fail=%d", opStore.succeededCalls, opStore.failedCalls)
	}
	if opStore.operation.State != core.OperationFailed {
		t.Fatalf("operation state mismatch: got %q want %q", opStore.operation.State, core.OperationFailed)
	}
	if len(eventStore.events) != 4 {
		t.Fatalf("expected 4 events, got %d", len(eventStore.events))
	}
	if eventStore.events[2].Event.Type != events.OperationFailed {
		t.Fatalf("completion event type: got %q want %q", eventStore.events[2].Event.Type, events.OperationFailed)
	}
	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["state"] != string(core.OperationFailed) {
		t.Fatalf("completion state mismatch: got %v want %q", completedPayload["state"], core.OperationFailed)
	}
	if completedPayload["message"] != "git evidence collection failed" {
		t.Fatalf("message: got %v want %q", completedPayload["message"], "git evidence collection failed")
	}
	if _, ok := completedPayload["status_clean"]; ok {
		t.Fatalf("did not expect git evidence fields on evidence failure payload: %#v", completedPayload)
	}
	wantCalls := []fakeGitCall{
		{workdir: "/repo/worktree", args: []string{"status", "--porcelain"}},
	}
	if !reflect.DeepEqual(gitRunner.calls, wantCalls) {
		t.Fatalf("git calls:\n got %#v\nwant %#v", gitRunner.calls, wantCalls)
	}
}

func TestRunnerRunOnceAgentRunCommitPushesWhenPolicyAllows(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-commit-push")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "before-revision",
			PolicyContext:    map[string]any{"commit_push": true},
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{
			State:   worker.AgentRunStateSucceeded,
			Message: "done",
		},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{
			" M file.go\n",     // status --porcelain (dirty)
			"after-revision\n", // rev-parse HEAD
			"M\tfile.go\n",     // diff --name-status
			"",                 // add -A
			"",                 // commit -m
			"",                 // push
		},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 1 || opStore.failedCalls != 0 {
		t.Fatalf("expected succeed=1 fail=0, got succeed=%d fail=%d", opStore.succeededCalls, opStore.failedCalls)
	}

	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["commit_push_allowed"] != true {
		t.Fatalf("commit_push_allowed: got %v want true", completedPayload["commit_push_allowed"])
	}
	if completedPayload["commit_attempted"] != true {
		t.Fatalf("commit_attempted: got %v want true", completedPayload["commit_attempted"])
	}
	if completedPayload["push_attempted"] != true {
		t.Fatalf("push_attempted: got %v want true", completedPayload["push_attempted"])
	}
	if completedPayload["push_remote"] != "origin" {
		t.Fatalf("push_remote: got %v want %q", completedPayload["push_remote"], "origin")
	}
	if completedPayload["push_branch"] != "main" {
		t.Fatalf("push_branch: got %v want %q", completedPayload["push_branch"], "main")
	}

	wantCalls := []fakeGitCall{
		{workdir: "/repo/worktree", args: []string{"status", "--porcelain"}},
		{workdir: "/repo/worktree", args: []string{"rev-parse", "HEAD"}},
		{workdir: "/repo/worktree", args: []string{"diff", "--name-status", "before-revision..after-revision"}},
		{workdir: "/repo/worktree", args: []string{"add", "-A"}},
		{workdir: "/repo/worktree", args: []string{"commit", "-m", "agent run result: agent-run-commit-push"}},
		{workdir: "/repo/worktree", args: []string{"push", "origin", "main"}},
	}
	if !reflect.DeepEqual(gitRunner.calls, wantCalls) {
		t.Fatalf("git calls:\n got %#v\nwant %#v", gitRunner.calls, wantCalls)
	}
}

func TestRunnerRunOnceAgentRunCommitPushViaAllowedActions(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-allowed-actions")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "feat",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "before-rev",
			PolicyContext: map[string]any{
				"allowed_result_actions": []any{"commit_push"},
				"commit_message":         "custom commit msg",
				"remote":                 "upstream",
			},
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{State: worker.AgentRunStateSucceeded, Message: "ok"},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{" M x.go\n", "after-rev\n", "M\tx.go\n", "", "", ""},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 1 {
		t.Fatalf("expected succeed=1, got %d", opStore.succeededCalls)
	}

	wantCalls := []fakeGitCall{
		{workdir: "/repo/worktree", args: []string{"status", "--porcelain"}},
		{workdir: "/repo/worktree", args: []string{"rev-parse", "HEAD"}},
		{workdir: "/repo/worktree", args: []string{"diff", "--name-status", "before-rev..after-rev"}},
		{workdir: "/repo/worktree", args: []string{"add", "-A"}},
		{workdir: "/repo/worktree", args: []string{"commit", "-m", "custom commit msg"}},
		{workdir: "/repo/worktree", args: []string{"push", "upstream", "feat"}},
	}
	if !reflect.DeepEqual(gitRunner.calls, wantCalls) {
		t.Fatalf("git calls:\n got %#v\nwant %#v", gitRunner.calls, wantCalls)
	}
}

func TestRunnerRunOnceAgentRunCommitPushViaAllowedActionsTypedSlice(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-allowed-actions-typed")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "typed-before",
			PolicyContext: map[string]any{
				"allowed_result_actions": []string{" commit_push "},
				"commit_message":         "typed commit msg",
			},
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{State: worker.AgentRunStateSucceeded, Message: "ok"},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{" M x.go\n", "typed-after\n", "M\tx.go\n", "", "", ""},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 1 {
		t.Fatalf("expected succeed=1, got %d", opStore.succeededCalls)
	}

	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["commit_push_allowed"] != true {
		t.Fatalf("commit_push_allowed: got %v want true", completedPayload["commit_push_allowed"])
	}
	if completedPayload["commit_attempted"] != true {
		t.Fatalf("commit_attempted: got %v want true", completedPayload["commit_attempted"])
	}
	if completedPayload["push_attempted"] != true {
		t.Fatalf("push_attempted: got %v want true", completedPayload["push_attempted"])
	}

	wantCalls := []fakeGitCall{
		{workdir: "/repo/worktree", args: []string{"status", "--porcelain"}},
		{workdir: "/repo/worktree", args: []string{"rev-parse", "HEAD"}},
		{workdir: "/repo/worktree", args: []string{"diff", "--name-status", "typed-before..typed-after"}},
		{workdir: "/repo/worktree", args: []string{"add", "-A"}},
		{workdir: "/repo/worktree", args: []string{"commit", "-m", "typed commit msg"}},
		{workdir: "/repo/worktree", args: []string{"push", "origin", "main"}},
	}
	if !reflect.DeepEqual(gitRunner.calls, wantCalls) {
		t.Fatalf("git calls:\n got %#v\nwant %#v", gitRunner.calls, wantCalls)
	}
}

func TestRunnerRunOnceAgentRunCommitFailsOperation(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-commit-fail")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "before-revision",
			PolicyContext:    map[string]any{"commit_push": true},
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{State: worker.AgentRunStateSucceeded, Message: "done"},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{" M file.go\n", "after-revision\n", "M\tfile.go\n", ""},
		errs:    []error{nil, nil, nil, nil, errors.New("nothing to commit")},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 0 || opStore.failedCalls != 1 {
		t.Fatalf("expected succeed=0 fail=1, got succeed=%d fail=%d", opStore.succeededCalls, opStore.failedCalls)
	}

	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["message"] != "git commit failed" {
		t.Fatalf("message: got %v want %q", completedPayload["message"], "git commit failed")
	}
	if completedPayload["commit_push_allowed"] != true {
		t.Fatalf("commit_push_allowed: got %v want true", completedPayload["commit_push_allowed"])
	}
	if completedPayload["commit_attempted"] != true {
		t.Fatalf("commit_attempted: got %v want true", completedPayload["commit_attempted"])
	}
	if _, ok := completedPayload["push_attempted"]; ok {
		t.Fatalf("push_attempted must not be set when commit failed")
	}
}

func TestRunnerRunOnceAgentRunPushFailsOperation(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-push-fail")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/repo/worktree",
			ExpectedRevision: "before-revision",
			PolicyContext:    map[string]any{"commit_push": true},
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{State: worker.AgentRunStateSucceeded, Message: "done"},
	}
	gitRunner := &fakeGitRunner{
		outputs: []string{" M file.go\n", "after-revision\n", "M\tfile.go\n", "", ""},
		errs:    []error{nil, nil, nil, nil, nil, errors.New("remote rejected")},
	}
	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if opStore.succeededCalls != 0 || opStore.failedCalls != 1 {
		t.Fatalf("expected succeed=0 fail=1, got succeed=%d fail=%d", opStore.succeededCalls, opStore.failedCalls)
	}

	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["message"] != "git push failed" {
		t.Fatalf("message: got %v want %q", completedPayload["message"], "git push failed")
	}
	if completedPayload["commit_push_allowed"] != true {
		t.Fatalf("commit_push_allowed: got %v want true", completedPayload["commit_push_allowed"])
	}
	if completedPayload["commit_attempted"] != true {
		t.Fatalf("commit_attempted: got %v want true", completedPayload["commit_attempted"])
	}
	if completedPayload["push_attempted"] != true {
		t.Fatalf("push_attempted: got %v want true", completedPayload["push_attempted"])
	}
	if completedPayload["push_remote"] != "origin" {
		t.Fatalf("push_remote: got %v want %q", completedPayload["push_remote"], "origin")
	}
}

func TestRunnerRunOnceAgentRunFailsWhenBackendFails(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC().Truncate(time.Microsecond)
	op := runningAgentRunOperation("agent-run-fail")
	picker := &fakePicker{
		pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
			return op, true, nil
		},
	}
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: op}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Instruction: "run-failing-task",
		},
	}
	invoker := &fakeAgentRunInvoker{
		result: worker.AgentRunResult{
			State:   worker.AgentRunStateFailed,
			Message: "should not be forwarded",
		},
		err: errors.New("backend transport issue"),
	}

	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
	})
	err := runner.RunOnce(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if invoker.calls != 1 || loader.calls != 1 {
		t.Fatalf("expected one input load and one invoke call; got load=%d invoke=%d", loader.calls, invoker.calls)
	}
	if opStore.startedCalls != 0 || opStore.failedCalls != 1 {
		t.Fatalf("expected no start and one fail call, got start=%d fail=%d", opStore.startedCalls, opStore.failedCalls)
	}
	if opStore.operation.State != core.OperationFailed {
		t.Fatalf("operation state mismatch: got %q want %q", opStore.operation.State, core.OperationFailed)
	}
	if len(eventStore.events) != 4 {
		t.Fatalf("expected 4 events, got %d", len(eventStore.events))
	}
	wantEventTypes := []string{
		events.OperationStarted,
		events.AgentRunStarted,
		events.OperationFailed,
		events.AgentRunCompleted,
	}
	for idx, ev := range eventStore.events {
		if ev.Event.Type != wantEventTypes[idx] {
			t.Fatalf("event[%d]: got %q want %q", idx, ev.Event.Type, wantEventTypes[idx])
		}
	}
	completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)
	if completedPayload["state"] != string(core.OperationFailed) {
		t.Fatalf("completion state mismatch: got %v want %q", completedPayload["state"], core.OperationFailed)
	}
	if completedPayload["message"] != "agent run invocation failed" {
		t.Fatalf("expected sanitized failure message, got %v", completedPayload["message"])
	}
}

func TestRunnerRunOnceAgentRunRevisionScan(t *testing.T) {
	now := time.Date(2026, 6, 16, 11, 0, 0, 0, time.UTC)
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))

	runTestCase := func(t *testing.T, scanResult worker.RevisionScanResult, scanErr error, expectSuccess bool, expectedEventID string) {
		op := runningAgentRunOperation("op-test-scan-1")
		picker := &fakePicker{
			pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
				return op, true, nil
			},
		}
		clock := fakeClock{now: now}
		opStore := &fakeOperationStore{operation: op}
		eventStore := &fakeOperationEventStore{}
		loader := &fakeAgentRunInputLoader{
			input: worker.AgentRunInput{
				Branch:           "main",
				WorkspacePath:    "/tmp/dummy",
				ExpectedRevision: "rev-before",
				PolicyContext: map[string]any{
					"commit_push": true,
					"provider":    "forgejo",
				},
			},
		}
		invoker := &fakeAgentRunInvoker{
			result: worker.AgentRunResult{
				State: worker.AgentRunStateSucceeded,
			},
		}
		gitRunner := &fakeGitRunner{
			outputs: []string{
				" M file1.txt\n", // status --porcelain
				"rev-before\n",   // rev-parse HEAD
				"A\tfile1.txt\n", // diff --name-status
				"",               // add -A
				"",               // commit -m
				"",               // push
			},
		}
		scanner := &fakeRevisionScanner{
			result: scanResult,
			err:    scanErr,
		}

		runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
			OperationStore:      opStore,
			OperationEventStore: eventStore,
			AgentRunInputLoader: loader,
			AgentRunInvoker:     invoker,
			GitRunner:           gitRunner,
			RevisionScanner:     scanner,
		})

		err := runner.RunOnce(context.Background())
		if err != nil {
			t.Fatalf("unexpected run error: %v", err)
		}

		if expectSuccess {
			if opStore.operation.State != core.OperationSucceeded {
				t.Fatalf("expected state succeeded, got %s", opStore.operation.State)
			}
		} else {
			if opStore.operation.State != core.OperationFailed {
				t.Fatalf("expected state failed, got %s", opStore.operation.State)
			}
		}

		// check payload in completed event (completed event is events.OperationCompleted or OperationFailed)
		// it is the 3rd event (idx 2)
		if len(eventStore.events) < 3 {
			t.Fatalf("expected at least 3 events, got %d", len(eventStore.events))
		}
		completedPayload := decodePayload(t, eventStore.events[2].Event.Payload)

		if scanErr != nil {
			if completedPayload["revision_scan_attempted"] != true {
				t.Errorf("expected revision_scan_attempted to be true on scanner error")
			}
			if completedPayload["revision_event_matched"] != false {
				t.Errorf("expected revision_event_matched to be false on scanner error")
			}
			msg, _ := completedPayload["message"].(string)
			if msg != "revision scan failed" {
				t.Errorf("expected sanitized message 'revision scan failed', got %q", msg)
			}
			if strings.Contains(msg, "scan service down") {
				t.Errorf("raw error message must not be exposed in completed payload")
			}
		} else {
			if completedPayload["revision_scan_attempted"] != scanResult.Attempted {
				t.Errorf("expected revision_scan_attempted=%v, got %v", scanResult.Attempted, completedPayload["revision_scan_attempted"])
			}
			if completedPayload["revision_event_matched"] != scanResult.Matched {
				t.Errorf("expected revision_event_matched=%v, got %v", scanResult.Matched, completedPayload["revision_event_matched"])
			}
			if expectedEventID != "" {
				if completedPayload["revision_event_id"] != expectedEventID {
					t.Errorf("expected revision_event_id=%s, got %v", expectedEventID, completedPayload["revision_event_id"])
				}
			} else {
				if _, ok := completedPayload["revision_event_id"]; ok {
					t.Errorf("expected no revision_event_id, got %v", completedPayload["revision_event_id"])
				}
			}

			// assert scanner requests
			if len(scanner.calls) != 1 {
				t.Fatalf("expected exactly 1 scanner call, got %d", len(scanner.calls))
			}
			req := scanner.calls[0]
			if req.Provider != "forgejo" {
				t.Errorf("expected provider 'forgejo', got %q", req.Provider)
			}
			if req.RepoID != op.RepoID {
				t.Errorf("expected RepoID %q, got %q", op.RepoID, req.RepoID)
			}
			if req.Branch != "main" {
				t.Errorf("expected Branch 'main', got %q", req.Branch)
			}
			if req.WorkDir != "/tmp/dummy" {
				t.Errorf("expected WorkDir '/tmp/dummy', got %q", req.WorkDir)
			}
			if req.Runner != gitRunner {
				t.Errorf("expected Runner to be gitRunner")
			}
			if !req.ObservedAt.Equal(now) {
				t.Errorf("expected ObservedAt %v, got %v", now, req.ObservedAt)
			}
		}
	}

	t.Run("Scan Matched", func(t *testing.T) {
		runTestCase(t, worker.RevisionScanResult{
			Attempted: true,
			Matched:   true,
			EventID:   "evt-123",
		}, nil, true, "evt-123")
	})

	t.Run("Scan Unmatched", func(t *testing.T) {
		runTestCase(t, worker.RevisionScanResult{
			Attempted: true,
			Matched:   false,
		}, nil, true, "")
	})

	t.Run("Scan Error", func(t *testing.T) {
		runTestCase(t, worker.RevisionScanResult{}, errors.New("scan service down"), false, "")
	})

	t.Run("Scan Matched but Publish Fails", func(t *testing.T) {
		runTestCase(t, worker.RevisionScanResult{}, errors.New("event matched but broadcaster is nil (not published)"), false, "")
	})

	t.Run("No Scan on Push Failure", func(t *testing.T) {
		op := runningAgentRunOperation("op-test-scan-fail-push")
		picker := &fakePicker{
			pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
				return op, true, nil
			},
		}
		clock := fakeClock{now: now}
		opStore := &fakeOperationStore{operation: op}
		eventStore := &fakeOperationEventStore{}
		loader := &fakeAgentRunInputLoader{
			input: worker.AgentRunInput{
				Branch:           "main",
				WorkspacePath:    "/tmp/dummy",
				ExpectedRevision: "rev-before",
				PolicyContext: map[string]any{
					"commit_push": true,
				},
			},
		}
		invoker := &fakeAgentRunInvoker{
			result: worker.AgentRunResult{
				State: worker.AgentRunStateSucceeded,
			},
		}
		gitRunner := &fakeGitRunner{
			outputs: []string{
				" M file1.txt\n", // status --porcelain
				"rev-before\n",   // rev-parse HEAD
				"A\tfile1.txt\n", // diff --name-status
				"",               // add -A
				"",               // commit -m
				"",               // push
			},
			errs: []error{nil, nil, nil, nil, nil, errors.New("remote rejected")},
		}
		scanner := &fakeRevisionScanner{}

		runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
			OperationStore:      opStore,
			OperationEventStore: eventStore,
			AgentRunInputLoader: loader,
			AgentRunInvoker:     invoker,
			GitRunner:           gitRunner,
			RevisionScanner:     scanner,
		})

		err := runner.RunOnce(context.Background())
		if err != nil {
			t.Fatalf("unexpected run error: %v", err)
		}

		if len(scanner.calls) != 0 {
			t.Errorf("scanner must not be called when push fails, got %d calls", len(scanner.calls))
		}
	})

	t.Run("No Scan on Policy Disabled", func(t *testing.T) {
		op := runningAgentRunOperation("op-test-scan-no-policy")
		picker := &fakePicker{
			pickFn: func(ctx context.Context, now time.Time) (core.Operation, bool, error) {
				return op, true, nil
			},
		}
		clock := fakeClock{now: now}
		opStore := &fakeOperationStore{operation: op}
		eventStore := &fakeOperationEventStore{}
		loader := &fakeAgentRunInputLoader{
			input: worker.AgentRunInput{
				Branch:           "main",
				WorkspacePath:    "/tmp/dummy",
				ExpectedRevision: "rev-before",
				PolicyContext: map[string]any{
					"commit_push": false,
				},
			},
		}
		invoker := &fakeAgentRunInvoker{
			result: worker.AgentRunResult{
				State: worker.AgentRunStateSucceeded,
			},
		}
		gitRunner := &fakeGitRunner{
			outputs: []string{
				" M file1.txt\n", // status --porcelain
				"rev-before\n",   // rev-parse HEAD
				"A\tfile1.txt\n", // diff --name-status
			},
		}
		scanner := &fakeRevisionScanner{}

		runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
			OperationStore:      opStore,
			OperationEventStore: eventStore,
			AgentRunInputLoader: loader,
			AgentRunInvoker:     invoker,
			GitRunner:           gitRunner,
			RevisionScanner:     scanner,
		})

		err := runner.RunOnce(context.Background())
		if err != nil {
			t.Fatalf("unexpected run error: %v", err)
		}

		if len(scanner.calls) != 0 {
			t.Errorf("scanner must not be called when commit/push policy is disabled, got %d calls", len(scanner.calls))
		}
	})
}

// fakeAgentRunPicker wraps fakeOperationStore to satisfy AgentRunPicker.
type fakeAgentRunPicker struct {
	op    core.Operation
	found bool
	err   error
	calls int
}

func (f *fakeAgentRunPicker) PickQueuedAgentRunOperation(_ context.Context, _ time.Time) (core.Operation, bool, error) {
	f.calls++
	return f.op, f.found, f.err
}

func TestRunnerRunOnceUsesAgentRunPickerWhenSet(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	clock := fakeClock{now: time.Now()}

	agentOp := core.Operation{
		ID:     "typed-pick-op",
		RepoID: "repo-1",
		Type:   core.OperationAgentRun,
		State:  core.OperationRunning,
	}
	agentPicker := &fakeAgentRunPicker{op: agentOp, found: true}
	genericPicker := &fakePicker{}
	opStore := &fakeOperationStore{operation: agentOp}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:           "main",
			WorkspacePath:    "/ws",
			ExpectedRevision: "rev-before",
		},
	}
	invoker := &fakeAgentRunInvoker{result: worker.AgentRunResult{State: worker.AgentRunStateSucceeded}}
	gitRunner := &fakeGitRunner{outputs: []string{"", "rev-before\n", ""}}

	runner := worker.NewRunnerWithDependencies(cfg, logger, genericPicker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunPicker:      agentPicker,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
	})

	if err := runner.RunOnce(context.Background()); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if agentPicker.calls != 1 {
		t.Errorf("expected AgentRunPicker to be called once, got %d", agentPicker.calls)
	}
	if genericPicker.calls != 0 {
		t.Errorf("generic picker must not be called when AgentRunPicker is set, got %d calls", genericPicker.calls)
	}
}

func TestRunnerRunOnceAgentRunRevisionScanIncludesBeforeAfter(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	now := time.Now().UTC()
	agentOp := runningAgentRunOperation("rev-scan-before-after")
	clock := fakeClock{now: now}
	opStore := &fakeOperationStore{operation: agentOp}
	eventStore := &fakeOperationEventStore{}
	loader := &fakeAgentRunInputLoader{
		input: worker.AgentRunInput{
			Branch:        "main",
			WorkspacePath: "/ws/slot",
			ExpectedRevision: "rev-aaa",
			PolicyContext: map[string]any{
				"commit_push": true,
				"provider":    "forgejo",
			},
		},
	}
	invoker := &fakeAgentRunInvoker{result: worker.AgentRunResult{State: worker.AgentRunStateSucceeded}}
	// git status (clean), HEAD rev (rev-bbb), changed files
	gitRunner := &fakeGitRunner{
		outputs: []string{
			"",
			"rev-bbb\n",
			"M\tREADME.md\n",
		},
	}
	scanner := &fakeRevisionScanner{result: worker.RevisionScanResult{Attempted: true, Matched: true, EventID: "evt-x"}}
	picker := &fakePicker{
		pickFn: func(_ context.Context, _ time.Time) (core.Operation, bool, error) {
			return agentOp, true, nil
		},
	}

	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
		GitRunner:           gitRunner,
		RevisionScanner:     scanner,
	})

	if err := runner.RunOnce(context.Background()); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(scanner.calls) != 1 {
		t.Fatalf("expected 1 scanner call, got %d", len(scanner.calls))
	}
	req := scanner.calls[0]
	if req.BeforeRevision != "rev-aaa" {
		t.Errorf("BeforeRevision: got %q want %q", req.BeforeRevision, "rev-aaa")
	}
	if req.AfterRevision != "rev-bbb" {
		t.Errorf("AfterRevision: got %q want %q", req.AfterRevision, "rev-bbb")
	}
}

func TestRunnerRunOnceAgentRunInputLoaderFailureFails(t *testing.T) {
	cfg := config.Config{WorkerEnabled: true}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agentOp := runningAgentRunOperation("missing-input-op")
	clock := fakeClock{now: time.Now()}
	opStore := &fakeOperationStore{operation: agentOp}
	eventStore := &fakeOperationEventStore{}
	picker := &fakePicker{
		pickFn: func(_ context.Context, _ time.Time) (core.Operation, bool, error) {
			return agentOp, true, nil
		},
	}
	loader := &fakeAgentRunInputLoader{err: storage.ErrAgentRunInputNotFound}
	invoker := &fakeAgentRunInvoker{}

	runner := worker.NewRunnerWithDependencies(cfg, logger, picker, clock, worker.RunnerDependencies{
		OperationStore:      opStore,
		OperationEventStore: eventStore,
		AgentRunInputLoader: loader,
		AgentRunInvoker:     invoker,
	})

	if err := runner.RunOnce(context.Background()); err != nil {
		t.Fatalf("expected nil error after input load failure (operation must be failed, not propagated): %v", err)
	}
	if opStore.failedCalls != 1 {
		t.Errorf("expected FailOperation called once, got %d", opStore.failedCalls)
	}
	if invoker.calls != 0 {
		t.Errorf("invoker must not be called when input loader fails, got %d calls", invoker.calls)
	}
	// started events (2) + completion events (2) = 4 total
	if len(eventStore.events) < 2 {
		t.Errorf("expected completion events appended, got %d events", len(eventStore.events))
	}
	// Last two events must be OperationFailed and AgentRunCompleted.
	n := len(eventStore.events)
	if n >= 2 {
		if eventStore.events[n-2].Event.Type != events.OperationFailed {
			t.Errorf("second-to-last event type: got %q want %q", eventStore.events[n-2].Event.Type, events.OperationFailed)
		}
		if eventStore.events[n-1].Event.Type != events.AgentRunCompleted {
			t.Errorf("last event type: got %q want %q", eventStore.events[n-1].Event.Type, events.AgentRunCompleted)
		}
	}
}
