package workflow

import (
	"context"
	"encoding/json"
	"errors"
	"testing"

	"github.com/jackc/pgx/v5"
	"github.com/nomadcode/nomadcode-core/internal/storage"
)

func TestNormalizeExternalRefDefaultsMetadata(t *testing.T) {
	ref, err := NormalizeExternalRef(&ExternalRefInput{
		Provider: " plane ",
		ID:       " work-item-1 ",
		URL:      " https://plane.example/work-item-1 ",
	})
	if err != nil {
		t.Fatalf("NormalizeExternalRef returned error: %v", err)
	}
	if ref.Provider == nil || *ref.Provider != "plane" {
		t.Fatalf("unexpected provider: %#v", ref.Provider)
	}
	if ref.ID == nil || *ref.ID != "work-item-1" {
		t.Fatalf("unexpected id: %#v", ref.ID)
	}
	if ref.URL == nil || *ref.URL != "https://plane.example/work-item-1" {
		t.Fatalf("unexpected url: %#v", ref.URL)
	}
	if string(ref.Metadata) != "{}" {
		t.Fatalf("unexpected metadata: %s", ref.Metadata)
	}
}

func TestNormalizeExternalRefRejectsInvalidMetadata(t *testing.T) {
	_, err := NormalizeExternalRef(&ExternalRefInput{
		Provider: "plane",
		Metadata: json.RawMessage(`{"bad"`),
	})
	if err == nil {
		t.Fatal("expected invalid metadata error")
	}
}

func TestNormalizeExternalRefRequiresProvider(t *testing.T) {
	_, err := NormalizeExternalRef(&ExternalRefInput{
		ID: "work-item-1",
	})
	if err == nil {
		t.Fatal("expected provider error")
	}
}

func TestNormalizeTaskMetadataDefaultsAndKeepsValidJSON(t *testing.T) {
	metadata, err := NormalizeTaskMetadata(nil)
	if err != nil {
		t.Fatalf("NormalizeTaskMetadata returned error: %v", err)
	}
	if string(metadata) != "{}" {
		t.Fatalf("unexpected default metadata: %s", metadata)
	}

	metadata, err = NormalizeTaskMetadata(json.RawMessage(`{"agent_phase":0}`))
	if err != nil {
		t.Fatalf("NormalizeTaskMetadata returned error: %v", err)
	}
	if string(metadata) != `{"agent_phase":0}` {
		t.Fatalf("unexpected metadata: %s", metadata)
	}
}

func TestNormalizeTaskMetadataRejectsInvalidJSON(t *testing.T) {
	_, err := NormalizeTaskMetadata(json.RawMessage(`{"agent_phase"`))
	if err == nil {
		t.Fatal("expected invalid metadata error")
	}
}

func TestLifecycleStartTaskAllowsQueuedAndMarksRunning(t *testing.T) {
	tests := []struct {
		from   TaskStatus
		to     TaskStatus
		wanted bool
	}{
		{StatusPending, StatusRunning, true},
		{StatusQueued, StatusRunning, true},
	}

	for _, tt := range tests {
		got := canTransition(tt.from, tt.to)
		if got != tt.wanted {
			t.Errorf("canTransition(%s, %s) = %v; want %v", tt.from, tt.to, got, tt.wanted)
		}
	}
}

func TestCanTransitionRejectsInvalidLifecycleMove(t *testing.T) {
	tests := []struct {
		from   TaskStatus
		to     TaskStatus
		wanted bool
	}{
		{StatusCompleted, StatusRunning, false},
		{StatusPending, StatusCompleted, false},
		{StatusCanceled, StatusRunning, false},
		{StatusRunning, StatusQueued, false},
	}

	for _, tt := range tests {
		got := canTransition(tt.from, tt.to)
		if got != tt.wanted {
			t.Errorf("canTransition(%s, %s) = %v; want %v", tt.from, tt.to, got, tt.wanted)
		}
	}
}

func TestMergeTaskMetadataPreservesExistingKeys(t *testing.T) {
	current := json.RawMessage(`{"plan_ref":"plan-123","attempt":2}`)
	updates := map[string]any{
		"attempt":         3,
		"agent_run_state": "running",
	}
	merged, err := mergeTaskMetadata(current, updates)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	var res map[string]any
	if err := json.Unmarshal(merged, &res); err != nil {
		t.Fatalf("failed to unmarshal: %v", err)
	}

	if res["plan_ref"] != "plan-123" {
		t.Errorf("plan_ref was modified: %v", res["plan_ref"])
	}
	if res["attempt"] != float64(3) && res["attempt"] != 3 {
		t.Errorf("attempt was not updated: %v", res["attempt"])
	}
	if res["agent_run_state"] != "running" {
		t.Errorf("agent_run_state was not set: %v", res["agent_run_state"])
	}
}

func TestLifecycleMetadataStartIncrementsAttempt(t *testing.T) {
	current1 := json.RawMessage(`{"plan_ref":"plan-123"}`)
	attempt1, err := nextTaskAttempt(current1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if attempt1 != 1 {
		t.Errorf("expected attempt 1, got: %d", attempt1)
	}

	current2 := json.RawMessage(`{"attempt":2}`)
	attempt2, err := nextTaskAttempt(current2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if attempt2 != 3 {
		t.Errorf("expected attempt 3, got: %d", attempt2)
	}
}

func TestLifecycleMetadataRejectsInvalidExistingMetadata(t *testing.T) {
	current := json.RawMessage(`{invalid_json}`)
	_, err := nextTaskAttempt(current)
	if err == nil {
		t.Fatal("expected error on invalid metadata, got nil")
	}
	if err != ErrInvalidTaskInput {
		t.Errorf("expected ErrInvalidTaskInput, got: %v", err)
	}
}

func TestCanTransitionAllowsRetryFromFailedToRunning(t *testing.T) {
	if !canTransition(StatusFailed, StatusRunning) {
		t.Error("expected canTransition(StatusFailed, StatusRunning) to be true, got false")
	}
}

func TestTaskStatusHelpersClassifyLifecycleStates(t *testing.T) {
	// 1. validTaskStatus
	validStatuses := []TaskStatus{StatusPending, StatusQueued, StatusRunning, StatusCompleted, StatusFailed, StatusCanceled}
	for _, status := range validStatuses {
		if !validTaskStatus(status) {
			t.Errorf("expected %s to be valid", status)
		}
	}
	if validTaskStatus(TaskStatus("invalid")) {
		t.Errorf("expected 'invalid' status to be invalid")
	}

	// 2. terminalTaskStatus
	terminalStatuses := map[TaskStatus]bool{
		StatusPending:   false,
		StatusQueued:    false,
		StatusRunning:   false,
		StatusCompleted: true,
		StatusFailed:    true,
		StatusCanceled:  true,
	}
	for status, expected := range terminalStatuses {
		got := terminalTaskStatus(status)
		if got != expected {
			t.Errorf("terminalTaskStatus(%s) = %v; want %v", status, got, expected)
		}
	}

	// 3. canEnqueueStatus
	enqueueStatuses := map[TaskStatus]bool{
		StatusPending:   true,
		StatusQueued:    false,
		StatusRunning:   false,
		StatusCompleted: false,
		StatusFailed:    true,
		StatusCanceled:  false,
	}
	for status, expected := range enqueueStatuses {
		got := canEnqueueStatus(status)
		if got != expected {
			t.Errorf("canEnqueueStatus(%s) = %v; want %v", status, got, expected)
		}
	}
}

type fakeTaskStore struct {
	tasks         map[string]storage.Task
	operations    []string
	metadataCalls []metadataCall
	statusCalls   []statusCall
	completeCalls []completeCall
	failCalls     []failCall
}

type metadataCall struct {
	id       string
	metadata json.RawMessage
}

type statusCall struct {
	id     string
	status string
}

type completeCall struct {
	id     string
	result json.RawMessage
}

type failCall struct {
	id      string
	message string
}

func newFakeTaskStore() *fakeTaskStore {
	return &fakeTaskStore{
		tasks: make(map[string]storage.Task),
	}
}

func (f *fakeTaskStore) CreateTask(ctx context.Context, input storage.CreateTaskInput) (storage.Task, error) {
	return storage.Task{}, nil
}

func (f *fakeTaskStore) GetTaskByExternalRef(ctx context.Context, provider, id string) (storage.Task, error) {
	for _, t := range f.tasks {
		if t.ExternalProvider != nil && *t.ExternalProvider == provider &&
			t.ExternalID != nil && *t.ExternalID == id {
			return t, nil
		}
	}
	return storage.Task{}, pgx.ErrNoRows
}

func (f *fakeTaskStore) GetTask(ctx context.Context, id string) (storage.Task, error) {
	t, ok := f.tasks[id]
	if !ok {
		return storage.Task{}, errors.New("task not found")
	}
	return t, nil
}

func (f *fakeTaskStore) ListTasks(ctx context.Context, limit int32) ([]storage.Task, error) {
	return nil, nil
}

func (f *fakeTaskStore) UpdateMetadata(ctx context.Context, id string, metadata json.RawMessage) (storage.Task, error) {
	f.operations = append(f.operations, "metadata:"+id)
	f.metadataCalls = append(f.metadataCalls, metadataCall{id: id, metadata: metadata})
	t, ok := f.tasks[id]
	if !ok {
		return storage.Task{}, errors.New("task not found")
	}
	t.Metadata = metadata
	f.tasks[id] = t
	return t, nil
}

func (f *fakeTaskStore) UpdateStatus(ctx context.Context, id, status string) (storage.Task, error) {
	f.operations = append(f.operations, "status:"+id+":"+status)
	f.statusCalls = append(f.statusCalls, statusCall{id: id, status: status})
	t, ok := f.tasks[id]
	if !ok {
		return storage.Task{}, errors.New("task not found")
	}
	t.Status = status
	f.tasks[id] = t
	return t, nil
}

func (f *fakeTaskStore) CompleteTask(ctx context.Context, id string, result json.RawMessage) (storage.Task, error) {
	f.operations = append(f.operations, "complete:"+id)
	f.completeCalls = append(f.completeCalls, completeCall{id: id, result: result})
	t, ok := f.tasks[id]
	if !ok {
		return storage.Task{}, errors.New("task not found")
	}
	t.Status = string(StatusCompleted)
	t.Result = result
	f.tasks[id] = t
	return t, nil
}

func (f *fakeTaskStore) FailTask(ctx context.Context, id, message string) (storage.Task, error) {
	f.operations = append(f.operations, "fail:"+id)
	f.failCalls = append(f.failCalls, failCall{id: id, message: message})
	t, ok := f.tasks[id]
	if !ok {
		return storage.Task{}, errors.New("task not found")
	}
	t.Status = string(StatusFailed)
	t.Error = &message
	f.tasks[id] = t
	return t, nil
}

func TestLifecycleStartCompleteFailTransitionsWithStore(t *testing.T) {
	ctx := context.Background()

	// 1. Pending or Queued task can start and returns running.
	t.Run("start task from pending", func(t *testing.T) {
		store := newFakeTaskStore()
		store.tasks["task-1"] = storage.Task{
			ID:       "task-1",
			Status:   string(StatusPending),
			Metadata: json.RawMessage(`{}`),
		}
		l := &Lifecycle{store: store}

		task, err := l.StartTask(ctx, "task-1")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if task.Status != string(StatusRunning) {
			t.Errorf("expected status running, got: %s", task.Status)
		}

		// Verify metadata gets agent_run_state, last_heartbeat_at, and attempt=1 on start
		var meta map[string]any
		if err := json.Unmarshal(task.Metadata, &meta); err != nil {
			t.Fatalf("failed to unmarshal metadata: %v", err)
		}
		if meta[MetadataKeyAgentRunState] != "running" {
			t.Errorf("expected agent_run_state running, got: %v", meta[MetadataKeyAgentRunState])
		}
		if meta[MetadataKeyAttempt] != float64(1) {
			t.Errorf("expected attempt 1, got: %v", meta[MetadataKeyAttempt])
		}
		if _, ok := meta[MetadataKeyLastHeartbeat]; !ok {
			t.Errorf("expected last_heartbeat_at to be set")
		}

		// Verify store calls in order: UpdateMetadata, then UpdateStatus using global operations log
		if len(store.operations) != 2 {
			t.Fatalf("expected exactly 2 store operations, got: %d", len(store.operations))
		}
		if store.operations[0] != "metadata:task-1" {
			t.Errorf("expected first operation to be metadata:task-1, got: %s", store.operations[0])
		}
		if store.operations[1] != "status:task-1:running" {
			t.Errorf("expected second operation to be status:task-1:running, got: %s", store.operations[1])
		}
	})

	t.Run("start task from queued (increments attempt)", func(t *testing.T) {
		store := newFakeTaskStore()
		store.tasks["task-2"] = storage.Task{
			ID:       "task-2",
			Status:   string(StatusQueued),
			Metadata: json.RawMessage(`{"attempt":1}`),
		}
		l := &Lifecycle{store: store}

		task, err := l.StartTask(ctx, "task-2")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if task.Status != string(StatusRunning) {
			t.Errorf("expected status running, got: %s", task.Status)
		}

		var meta map[string]any
		if err := json.Unmarshal(task.Metadata, &meta); err != nil {
			t.Fatalf("failed to unmarshal metadata: %v", err)
		}
		if meta[MetadataKeyAttempt] != float64(2) {
			t.Errorf("expected attempt 2, got: %v", meta[MetadataKeyAttempt])
		}
	})

	// 2. Running task can complete and returns completed.
	t.Run("complete running task", func(t *testing.T) {
		store := newFakeTaskStore()
		store.tasks["task-3"] = storage.Task{
			ID:       "task-3",
			Status:   string(StatusRunning),
			Metadata: json.RawMessage(`{"attempt":1,"agent_run_state":"running"}`),
		}
		l := &Lifecycle{store: store}

		result := json.RawMessage(`{"summary":"success detail"}`)
		task, err := l.CompleteTask(ctx, "task-3", result)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if task.Status != string(StatusCompleted) {
			t.Errorf("expected status completed, got: %s", task.Status)
		}

		var meta map[string]any
		if err := json.Unmarshal(task.Metadata, &meta); err != nil {
			t.Fatalf("failed to unmarshal metadata: %v", err)
		}
		if meta[MetadataKeyAgentRunState] != "completed" {
			t.Errorf("expected agent_run_state completed, got: %v", meta[MetadataKeyAgentRunState])
		}
		if meta[MetadataKeyStatusReason] != "success detail" {
			t.Errorf("expected status_reason 'success detail', got: %v", meta[MetadataKeyStatusReason])
		}

		// Verify store calls in order: UpdateMetadata, then CompleteTask using global operations log
		if len(store.operations) != 2 {
			t.Fatalf("expected exactly 2 store operations, got: %d", len(store.operations))
		}
		if store.operations[0] != "metadata:task-3" {
			t.Errorf("expected first operation to be metadata:task-3, got: %s", store.operations[0])
		}
		if store.operations[1] != "complete:task-3" {
			t.Errorf("expected second operation to be complete:task-3, got: %s", store.operations[1])
		}
	})

	// 3. Running task can fail and returns failed.
	t.Run("fail running task", func(t *testing.T) {
		store := newFakeTaskStore()
		store.tasks["task-4"] = storage.Task{
			ID:       "task-4",
			Status:   string(StatusRunning),
			Metadata: json.RawMessage(`{"attempt":1,"agent_run_state":"running"}`),
		}
		l := &Lifecycle{store: store}

		task, err := l.FailTask(ctx, "task-4", "something went wrong")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if task.Status != string(StatusFailed) {
			t.Errorf("expected status failed, got: %s", task.Status)
		}

		var meta map[string]any
		if err := json.Unmarshal(task.Metadata, &meta); err != nil {
			t.Fatalf("failed to unmarshal metadata: %v", err)
		}
		if meta[MetadataKeyAgentRunState] != "failed" {
			t.Errorf("expected agent_run_state failed, got: %v", meta[MetadataKeyAgentRunState])
		}
		if meta[MetadataKeyStatusReason] != "something went wrong" {
			t.Errorf("expected status_reason 'something went wrong', got: %v", meta[MetadataKeyStatusReason])
		}

		// Verify store calls in order: UpdateMetadata, then FailTask using global operations log
		if len(store.operations) != 2 {
			t.Fatalf("expected exactly 2 store operations, got: %d", len(store.operations))
		}
		if store.operations[0] != "metadata:task-4" {
			t.Errorf("expected first operation to be metadata:task-4, got: %s", store.operations[0])
		}
		if store.operations[1] != "fail:task-4" {
			t.Errorf("expected second operation to be fail:task-4, got: %s", store.operations[1])
		}
	})

	// 4. Completed task cannot start.
	t.Run("cannot start completed task", func(t *testing.T) {
		store := newFakeTaskStore()
		store.tasks["task-5"] = storage.Task{
			ID:       "task-5",
			Status:   string(StatusCompleted),
			Metadata: json.RawMessage(`{}`),
		}
		l := &Lifecycle{store: store}

		_, err := l.StartTask(ctx, "task-5")
		if err != ErrInvalidTaskTransition {
			t.Errorf("expected ErrInvalidTaskTransition, got: %v", err)
		}
	})
}

func TestLifecycleFailTaskStoresFailureMetadataAndError(t *testing.T) {
	ctx := context.Background()
	store := newFakeTaskStore()
	store.tasks["task-fail-meta"] = storage.Task{
		ID:       "task-fail-meta",
		Status:   string(StatusRunning),
		Metadata: json.RawMessage(`{"attempt":1,"agent_run_state":"running"}`),
	}
	l := &Lifecycle{store: store}

	// Fail with FailTaskWithMetadata
	task, err := l.FailTaskWithMetadata(ctx, "task-fail-meta", FailureInput{
		Message: "timeout occurred",
		Type:    FailureTypeTimeout,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if task.Status != string(StatusFailed) {
		t.Errorf("expected status failed, got: %s", task.Status)
	}
	if task.Error == nil || *task.Error != "timeout occurred" {
		t.Errorf("expected error message 'timeout occurred', got: %v", task.Error)
	}

	var meta map[string]any
	if err := json.Unmarshal(task.Metadata, &meta); err != nil {
		t.Fatalf("failed to unmarshal metadata: %v", err)
	}
	if meta[MetadataKeyAgentRunState] != "failed" {
		t.Errorf("expected agent_run_state failed, got: %v", meta[MetadataKeyAgentRunState])
	}
	if meta[MetadataKeyStatusReason] != "timeout occurred" {
		t.Errorf("expected status_reason 'timeout occurred', got: %v", meta[MetadataKeyStatusReason])
	}
	if meta[MetadataKeyFailureType] != string(FailureTypeTimeout) {
		t.Errorf("expected failure_type 'timeout', got: %v", meta[MetadataKeyFailureType])
	}
	if _, ok := meta[MetadataKeyFailedAt]; !ok {
		t.Errorf("expected failed_at to be set in metadata")
	}
	if meta[MetadataKeyRetryable] != true {
		t.Errorf("expected retryable to be true, got: %v", meta[MetadataKeyRetryable])
	}

	// Test when attempt is equal to DefaultTaskMaxAttempts (not retryable)
	store.tasks["task-fail-max-attempts"] = storage.Task{
		ID:       "task-fail-max-attempts",
		Status:   string(StatusRunning),
		Metadata: json.RawMessage(`{"attempt":3,"agent_run_state":"running"}`),
	}
	taskMax, err := l.FailTaskWithMetadata(ctx, "task-fail-max-attempts", FailureInput{
		Message: "max attempts reached",
		Type:    FailureTypeExecution,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	var metaMax map[string]any
	if err := json.Unmarshal(taskMax.Metadata, &metaMax); err != nil {
		t.Fatalf("failed to unmarshal metadata: %v", err)
	}
	if metaMax[MetadataKeyRetryable] != false {
		t.Errorf("expected retryable to be false, got: %v", metaMax[MetadataKeyRetryable])
	}

	// Test trimming and empty/default behavior
	store.tasks["task-fail-empty"] = storage.Task{
		ID:       "task-fail-empty",
		Status:   string(StatusRunning),
		Metadata: json.RawMessage(`{}`),
	}
	task2, err := l.FailTaskWithMetadata(ctx, "task-fail-empty", FailureInput{
		Message: "   ",
		Type:    "",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	var meta2 map[string]any
	if err := json.Unmarshal(task2.Metadata, &meta2); err != nil {
		t.Fatalf("failed to unmarshal metadata: %v", err)
	}
	if meta2[MetadataKeyStatusReason] != "unknown failure" {
		t.Errorf("expected status_reason fallback 'unknown failure', got: %v", meta2[MetadataKeyStatusReason])
	}
	if meta2[MetadataKeyFailureType] != string(FailureTypeExecution) {
		t.Errorf("expected default failure_type 'execution', got: %v", meta2[MetadataKeyFailureType])
	}
}

func TestLifecycleFailTaskClearsWaitType(t *testing.T) {
	ctx := context.Background()
	store := newFakeTaskStore()
	store.tasks["task-wait"] = storage.Task{
		ID:       "task-wait",
		Status:   string(StatusRunning),
		Metadata: json.RawMessage(`{"attempt":1,"agent_run_state":"running","wait_type":"user_input"}`),
	}
	l := &Lifecycle{store: store}

	task, err := l.FailTask(ctx, "task-wait", "failed by agent")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	var meta map[string]any
	if err := json.Unmarshal(task.Metadata, &meta); err != nil {
		t.Fatalf("failed to unmarshal metadata: %v", err)
	}
	if val, ok := meta[MetadataKeyWaitType]; ok && val != nil {
		t.Errorf("expected wait_type to be cleared, got: %v", val)
	}
}

type fakeTaskEnqueuer struct {
	err   error
	calls []string
}

func (f *fakeTaskEnqueuer) EnqueueTask(ctx context.Context, taskID string) error {
	f.calls = append(f.calls, taskID)
	return f.err
}

func TestServiceEnqueueTaskUsesLifecycleQueueTransition(t *testing.T) {
	ctx := context.Background()
	store := newFakeTaskStore()
	store.tasks["task-1"] = storage.Task{
		ID:     "task-1",
		Status: string(StatusPending),
	}

	enqueuer := &fakeTaskEnqueuer{}
	service := NewService(nil, enqueuer, nil)
	service.lifecycle = &Lifecycle{store: store}

	task, err := service.EnqueueTask(ctx, "task-1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if task.Status != string(StatusQueued) {
		t.Errorf("expected status queued, got: %s", task.Status)
	}
	if len(enqueuer.calls) != 1 || enqueuer.calls[0] != "task-1" {
		t.Errorf("expected enqueuer to be called with task-1, got: %v", enqueuer.calls)
	}

	if len(store.statusCalls) != 1 || store.statusCalls[0].status != string(StatusQueued) {
		t.Errorf("expected status transition to queued, got: %v", store.statusCalls)
	}
}

func TestServiceEnqueueFailureMarksTaskFailedThroughLifecycle(t *testing.T) {
	ctx := context.Background()
	store := newFakeTaskStore()
	store.tasks["task-1"] = storage.Task{
		ID:       "task-1",
		Status:   string(StatusPending),
		Metadata: json.RawMessage(`{}`),
	}

	enqueuer := &fakeTaskEnqueuer{err: errors.New("enqueue error")}
	service := NewService(nil, enqueuer, nil)
	service.lifecycle = &Lifecycle{store: store}

	_, err := service.EnqueueTask(ctx, "task-1")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if err.Error() != "enqueue error" {
		t.Errorf("expected 'enqueue error', got: %v", err)
	}

	task, err := store.GetTask(ctx, "task-1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if task.Status != string(StatusFailed) {
		t.Errorf("expected task to be marked failed, got status: %s", task.Status)
	}
	if task.Error == nil || *task.Error != "enqueue error" {
		t.Errorf("expected error message 'enqueue error', got: %v", task.Error)
	}

	var meta map[string]any
	if err := json.Unmarshal(task.Metadata, &meta); err != nil {
		t.Fatalf("failed to unmarshal metadata: %v", err)
	}
	if meta[MetadataKeyAgentRunState] != "failed" {
		t.Errorf("expected agent_run_state failed, got: %v", meta[MetadataKeyAgentRunState])
	}
}

func TestServiceEnqueueTaskWithoutEnqueuerMarksTaskFailedThroughLifecycle(t *testing.T) {
	ctx := context.Background()
	store := newFakeTaskStore()
	store.tasks["task-1"] = storage.Task{
		ID:       "task-1",
		Status:   string(StatusPending),
		Metadata: json.RawMessage(`{}`),
	}

	service := NewService(nil, nil, nil) // enqueuer is nil
	service.lifecycle = &Lifecycle{store: store}

	_, err := service.EnqueueTask(ctx, "task-1")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if err.Error() != "task enqueuer is not configured" {
		t.Errorf("expected 'task enqueuer is not configured', got: %v", err)
	}

	task, err := store.GetTask(ctx, "task-1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if task.Status != string(StatusFailed) {
		t.Errorf("expected task to be marked failed, got status: %s", task.Status)
	}
	if task.Error == nil || *task.Error != "task enqueuer is not configured" {
		t.Errorf("expected error message 'task enqueuer is not configured', got: %v", task.Error)
	}

	var meta map[string]any
	if err := json.Unmarshal(task.Metadata, &meta); err != nil {
		t.Fatalf("failed to unmarshal metadata: %v", err)
	}
	if meta[MetadataKeyAgentRunState] != "failed" {
		t.Errorf("expected agent_run_state failed, got: %v", meta[MetadataKeyAgentRunState])
	}
	if meta[MetadataKeyStatusReason] != "task enqueuer is not configured" {
		t.Errorf("expected status_reason 'task enqueuer is not configured', got: %v", meta[MetadataKeyStatusReason])
	}
}

func TestServiceGetTaskByExternalRef(t *testing.T) {
	ctx := context.Background()
	service := NewService(nil, nil, nil)

	_, err := service.GetTaskByExternalRef(ctx, "", "work-1")
	if !errors.Is(err, ErrInvalidTaskInput) {
		t.Errorf("expected ErrInvalidTaskInput for empty provider, got %v", err)
	}

	_, err = service.GetTaskByExternalRef(ctx, "plane", "   ")
	if !errors.Is(err, ErrInvalidTaskInput) {
		t.Errorf("expected ErrInvalidTaskInput for empty external ID, got %v", err)
	}
}

func TestServiceCompleteTaskByExternalRefPromotesAuthoringMetadata(t *testing.T) {
	ctx := context.Background()
	store := newFakeTaskStore()

	provider := "plane"
	extID := "NOMAD-42"

	store.tasks["task-auth-complete"] = storage.Task{
		ID:               "task-auth-complete",
		Status:           string(StatusRunning),
		ExternalProvider: &provider,
		ExternalID:       &extID,
		Metadata:         json.RawMessage(`{"attempt":1,"agent_run_state":"running","wait_type":"develop_match","authoring_run_state":"in_progress"}`),
	}

	service := NewService(nil, nil, nil)
	service.lifecycle = &Lifecycle{store: store}

	result := json.RawMessage(`{"summary":"completed via scan","authoring_run_state":"succeeded"}`)
	completedTask, transitioned, err := service.CompleteTaskByExternalRef(ctx, "plane", "NOMAD-42", result)
	if err != nil {
		t.Fatalf("CompleteTaskByExternalRef returned error: %v", err)
	}
	if !transitioned {
		t.Error("expected transitioned to be true for running task, got false")
	}
	if completedTask.ID != "task-auth-complete" || completedTask.Status != string(StatusCompleted) {
		t.Fatalf("unexpected returned task: %#v", completedTask)
	}

	// Try completing again, should return transitioned = false
	_, transitioned2, err := service.CompleteTaskByExternalRef(ctx, "plane", "NOMAD-42", result)
	if err != nil {
		t.Fatalf("second CompleteTaskByExternalRef returned error: %v", err)
	}
	if transitioned2 {
		t.Error("expected transitioned to be false for already completed task, got true")
	}

	task, err := store.GetTask(ctx, "task-auth-complete")
	if err != nil {
		t.Fatalf("GetTask returned error: %v", err)
	}

	if task.Status != string(StatusCompleted) {
		t.Errorf("expected status completed, got %s", task.Status)
	}

	var meta map[string]any
	if err := json.Unmarshal(task.Metadata, &meta); err != nil {
		t.Fatalf("failed to unmarshal metadata: %v", err)
	}

	if meta[MetadataKeyAgentRunState] != "completed" {
		t.Errorf("expected agent_run_state completed, got %v", meta[MetadataKeyAgentRunState])
	}
	if meta[MetadataKeyAuthoringRunState] != "succeeded" {
		t.Errorf("expected authoring_run_state succeeded, got %v", meta[MetadataKeyAuthoringRunState])
	}
	if val, ok := meta[MetadataKeyWaitType]; ok && val != nil {
		t.Errorf("expected wait_type to be cleared, got %v", val)
	}
	if meta[MetadataKeyStatusReason] != "completed via scan" {
		t.Errorf("expected status_reason 'completed via scan', got %v", meta[MetadataKeyStatusReason])
	}
}
