package automation

import (
	"context"
	"fmt"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func newAutomationStoreForTest(t *testing.T) *Store {
	t.Helper()
	return newAutomationStoreForTestDSN(t, "file:"+t.Name()+"?mode=memory&cache=shared")
}

func newAutomationStoreForConcurrentTest(t *testing.T) *Store {
	t.Helper()
	return newAutomationStoreForTestDSN(t, "file:"+filepath.Join(t.TempDir(), "automation.db")+"?_journal_mode=WAL&_busy_timeout=5000")
}

func newAutomationStoreForTestDSN(t *testing.T, dsn string) *Store {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&AutomationRun{}, &AutomationRepoStatusOutbox{}, &AutomationRunSlot{}, &AutomationJobRun{}, &AutomationEvent{}, &AutomationJob{}, &AutomationWait{}, &AutomationRunner{}, &AutomationRunnerToken{}, &AutomationRunnerSession{}, &AutomationActionsRunnerCredential{}, &AutomationLogChunk{}, &AutomationArtifact{}, &AutomationSecret{}, &AutomationPinnedRepo{}))
	return New(db)
}

func mustCreateRun(t *testing.T, store *Store, run AutomationRun) AutomationRun {
	t.Helper()
	result, err := store.CreateRun(context.Background(), run)
	require.NoError(t, err)
	return result
}

func createLeasedAutomationJob(t *testing.T, ctx context.Context, store *Store, workspaceID string, runID string, jobRunID string, leaseOwner string, now time.Time, leaseUntil time.Time) AutomationJob {
	t.Helper()
	_, err := store.CreateRun(ctx, AutomationRun{ID: runID, WorkspaceID: workspaceID, Name: "Queue", Status: AutomationRunStatusRunning, CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.NoError(t, store.CreateJobRuns(ctx, []AutomationJobRun{{ID: jobRunID, WorkspaceID: workspaceID, RunID: runID, JobID: "build", JobType: "github_actions_job", Status: AutomationJobRunStatusRunning, ScheduledAt: now, CreatedAt: now, UpdatedAt: now}}))
	job, err := store.EnqueueJob(ctx, AutomationJob{
		WorkspaceID:    workspaceID,
		RunID:          runID,
		JobRunID:       jobRunID,
		QueueName:      "default",
		Status:         AutomationJobStatusLeased,
		LeaseOwner:     leaseOwner,
		LeaseUntil:     &leaseUntil,
		ScheduledAt:    now,
		Attempt:        1,
		MaxAttempts:    3,
		IdempotencyKey: runID + ":build",
		ShardBucket:    0,
		CreatedAt:      now,
		UpdatedAt:      now,
	})
	require.NoError(t, err)
	return job
}

func TestListRunsBasic(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-1", WorkspaceID: "ws-1", Name: "Run 1", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", CreatedAt: now, UpdatedAt: now})
	mustCreateRun(t, store, AutomationRun{ID: "run-2", WorkspaceID: "ws-1", Name: "Run 2", Status: AutomationRunStatusRunning, RepositoryFullName: "org/repo-b", CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)})

	result, err := store.ListRuns(ctx, "ws-1", "", "", "", 1, 10)
	require.NoError(t, err)
	require.False(t, result.HasMore)
	require.Len(t, result.Runs, 2)
	require.Equal(t, "run-2", result.Runs[0].ID, "should be ordered by updated_at DESC")
	require.Equal(t, "run-1", result.Runs[1].ID)
}

func TestListRunsWorkspaceScoped(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-1", WorkspaceID: "ws-1", Name: "Run 1", Status: AutomationRunStatusQueued, CreatedAt: now, UpdatedAt: now})
	mustCreateRun(t, store, AutomationRun{ID: "run-2", WorkspaceID: "ws-2", Name: "Run 2", Status: AutomationRunStatusQueued, CreatedAt: now, UpdatedAt: now})

	result, err := store.ListRuns(ctx, "ws-1", "", "", "", 1, 10)
	require.NoError(t, err)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-1", result.Runs[0].ID)
}

func TestListRunsStatusFilter(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-1", WorkspaceID: "ws-1", Name: "Run 1", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", CreatedAt: now, UpdatedAt: now})
	mustCreateRun(t, store, AutomationRun{ID: "run-2", WorkspaceID: "ws-1", Name: "Run 2", Status: AutomationRunStatusFailed, RepositoryFullName: "org/repo-b", CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)})
	mustCreateRun(t, store, AutomationRun{ID: "run-3", WorkspaceID: "ws-1", Name: "Run 3", Status: AutomationRunStatusRunning, RepositoryFullName: "org/repo-a", CreatedAt: now.Add(2 * time.Second), UpdatedAt: now.Add(2 * time.Second)})

	result, err := store.ListRuns(ctx, "ws-1", AutomationRunStatusSucceeded, "", "", 1, 10)
	require.NoError(t, err)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-1", result.Runs[0].ID)
}

func TestListRunsRepositoryFilter(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-1", WorkspaceID: "ws-1", Name: "Run 1", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", CreatedAt: now, UpdatedAt: now})
	mustCreateRun(t, store, AutomationRun{ID: "run-2", WorkspaceID: "ws-1", Name: "Run 2", Status: AutomationRunStatusFailed, RepositoryFullName: "org/repo-b", CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)})
	mustCreateRun(t, store, AutomationRun{ID: "run-3", WorkspaceID: "ws-1", Name: "Run 3", Status: AutomationRunStatusRunning, RepositoryFullName: "org/repo-a", CreatedAt: now.Add(2 * time.Second), UpdatedAt: now.Add(2 * time.Second)})

	result, err := store.ListRuns(ctx, "ws-1", "", "org/repo-a", "", 1, 10)
	require.NoError(t, err)
	require.Len(t, result.Runs, 2)
	require.Equal(t, "run-3", result.Runs[0].ID)
	require.Equal(t, "run-1", result.Runs[1].ID)
}

func TestListRunsStatusAndRepositoryCombined(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-1", WorkspaceID: "ws-1", Name: "Run 1", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", CreatedAt: now, UpdatedAt: now})
	mustCreateRun(t, store, AutomationRun{ID: "run-2", WorkspaceID: "ws-1", Name: "Run 2", Status: AutomationRunStatusFailed, RepositoryFullName: "org/repo-a", CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)})

	result, err := store.ListRuns(ctx, "ws-1", AutomationRunStatusSucceeded, "org/repo-a", "", 1, 10)
	require.NoError(t, err)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-1", result.Runs[0].ID)
}

func TestListRunsPaginationHasMore(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	for i := 0; i < 5; i++ {
		mustCreateRun(t, store, AutomationRun{
			ID:                 "run-" + string(rune('a'+i)),
			WorkspaceID:        "ws-1",
			Name:               "Run " + string(rune('A'+i)),
			Status:             AutomationRunStatusQueued,
			RepositoryFullName: "org/repo",
			CreatedAt:          now.Add(time.Duration(i) * time.Second),
			UpdatedAt:          now.Add(time.Duration(i) * time.Second),
		})
	}

	result, err := store.ListRuns(ctx, "ws-1", "", "", "", 1, 2)
	require.NoError(t, err)
	require.True(t, result.HasMore)
	require.Len(t, result.Runs, 2)
	require.Equal(t, "run-e", result.Runs[0].ID, "first page should have most recent runs")
	require.Equal(t, "run-d", result.Runs[1].ID)

	result, err = store.ListRuns(ctx, "ws-1", "", "", "", 2, 2)
	require.NoError(t, err)
	require.True(t, result.HasMore)
	require.Len(t, result.Runs, 2)
	require.Equal(t, "run-c", result.Runs[0].ID)
	require.Equal(t, "run-b", result.Runs[1].ID)

	result, err = store.ListRuns(ctx, "ws-1", "", "", "", 3, 2)
	require.NoError(t, err)
	require.False(t, result.HasMore)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-a", result.Runs[0].ID)
}

func TestListRunsEmptyResult(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()

	result, err := store.ListRuns(ctx, "ws-nonexistent", "", "", "", 1, 10)
	require.NoError(t, err)
	require.False(t, result.HasMore)
	require.Empty(t, result.Runs)
}

func TestListRunsSearchFilter(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-release", WorkspaceID: "ws-1", Name: "Release", RunName: "Ship v1", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", DefinitionPath: ".agentteam/release.yml", CreatedAt: now, UpdatedAt: now})
	mustCreateRun(t, store, AutomationRun{ID: "run-build", WorkspaceID: "ws-1", Name: "Build", Summary: "compile backend", Status: AutomationRunStatusRunning, RepositoryFullName: "org/repo-b", DefinitionPath: ".agentteam/build.yml", CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)})
	mustCreateRun(t, store, AutomationRun{ID: "run-deploy", WorkspaceID: "ws-2", Name: "Deploy", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", DefinitionPath: ".agentteam/release.yml", CreatedAt: now.Add(2 * time.Second), UpdatedAt: now.Add(2 * time.Second)})

	result, err := store.ListRuns(ctx, "ws-1", "", "", "release", 1, 10)
	require.NoError(t, err)
	require.False(t, result.HasMore)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-release", result.Runs[0].ID)

	result, err = store.ListRuns(ctx, "ws-1", AutomationRunStatusRunning, "", "backend", 1, 10)
	require.NoError(t, err)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-build", result.Runs[0].ID)

	result, err = store.ListRuns(ctx, "ws-1", "", "org/repo-a", "deploy", 1, 10)
	require.NoError(t, err)
	require.Empty(t, result.Runs)
}

func TestListRunsSearchTreatsLikeMetacharactersLiterally(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-percent", WorkspaceID: "ws-1", Name: "Deploy 100% complete", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", DefinitionPath: ".agentteam/percent.yml", CreatedAt: now, UpdatedAt: now})
	mustCreateRun(t, store, AutomationRun{ID: "run-underscore", WorkspaceID: "ws-1", Name: "Deploy candidate", Summary: "release_candidate", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", DefinitionPath: ".agentteam/underscore.yml", CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)})
	mustCreateRun(t, store, AutomationRun{ID: "run-plain", WorkspaceID: "ws-1", Name: "Plain deploy", Summary: "release candidate", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", DefinitionPath: ".agentteam/plain.yml", CreatedAt: now.Add(2 * time.Second), UpdatedAt: now.Add(2 * time.Second)})

	result, err := store.ListRuns(ctx, "ws-1", "", "", "%", 1, 10)
	require.NoError(t, err)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-percent", result.Runs[0].ID)

	result, err = store.ListRuns(ctx, "ws-1", "", "", "_", 1, 10)
	require.NoError(t, err)
	require.Len(t, result.Runs, 1)
	require.Equal(t, "run-underscore", result.Runs[0].ID)
}

func TestListRunsSearchUsesRecentCandidateWindow(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)

	mustCreateRun(t, store, AutomationRun{ID: "run-old-needle", WorkspaceID: "ws-1", Name: "Ancient needle", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", CreatedAt: now, UpdatedAt: now})
	for i := 0; i < automationRunSearchCandidateLimit; i++ {
		runID := fmt.Sprintf("run-noise-%04d", i)
		mustCreateRun(t, store, AutomationRun{ID: runID, WorkspaceID: "ws-1", Name: "Noise", Status: AutomationRunStatusSucceeded, RepositoryFullName: "org/repo-a", CreatedAt: now.Add(time.Duration(i+1) * time.Second), UpdatedAt: now.Add(time.Duration(i+1) * time.Second)})
	}

	result, err := store.ListRuns(ctx, "ws-1", "", "", "needle", 1, 10)
	require.NoError(t, err)
	require.Empty(t, result.Runs, "interactive run search should stay inside the recent candidate window")
}

func TestResolveWaitKeepsRunWaitingUntilLastActiveWait(t *testing.T) {
	ctx := context.Background()
	store := newAutomationStoreForTest(t)
	now := time.Date(2026, 6, 6, 10, 0, 0, 0, time.UTC)
	activeKey1 := "ws-1:run-multi-wait:job-wait-1"
	activeKey2 := "ws-1:run-multi-wait:job-wait-2"
	run := AutomationRun{ID: "run-multi-wait", WorkspaceID: "ws-1", Name: "Waits", Status: AutomationRunStatusWaiting, CreatedAt: now, UpdatedAt: now}
	jobRuns := []AutomationJobRun{
		{ID: "job-wait-1", WorkspaceID: "ws-1", RunID: "run-multi-wait", JobID: "wait-1", JobType: "github_actions_job", Status: AutomationJobRunStatusWaiting, ScheduledAt: now, CreatedAt: now, UpdatedAt: now},
		{ID: "job-wait-2", WorkspaceID: "ws-1", RunID: "run-multi-wait", JobID: "wait-2", JobType: "github_actions_job", Status: AutomationJobRunStatusWaiting, ScheduledAt: now, CreatedAt: now, UpdatedAt: now},
	}
	waits := []AutomationWait{
		{ID: "wait-1", WorkspaceID: "ws-1", RunID: "run-multi-wait", JobRunID: "job-wait-1", WaitKind: AutomationWaitKindAgentTask, Status: AutomationWaitStatusActive, ActiveKey: &activeKey1, CreatedAt: now, UpdatedAt: now},
		{ID: "wait-2", WorkspaceID: "ws-1", RunID: "run-multi-wait", JobRunID: "job-wait-2", WaitKind: AutomationWaitKindAgentTask, Status: AutomationWaitStatusActive, ActiveKey: &activeKey2, CreatedAt: now, UpdatedAt: now},
	}
	_, err := store.CreateRunWithRuntime(ctx, run, jobRuns, nil, nil, waits)
	require.NoError(t, err)

	_, _, err = store.ResolveWait(ctx, "ws-1", "wait-1", "user-1", `{"ok":true}`, now.Add(time.Minute))
	require.NoError(t, err)
	updatedRun, err := store.GetRun(ctx, "ws-1", "run-multi-wait")
	require.NoError(t, err)
	require.Equal(t, AutomationRunStatusWaiting, updatedRun.Status)
	activeWaits, err := store.ListWaits(ctx, "ws-1", "run-multi-wait", AutomationWaitStatusActive)
	require.NoError(t, err)
	require.Len(t, activeWaits, 1)

	_, _, err = store.ResolveWait(ctx, "ws-1", "wait-2", "user-1", `{"ok":true}`, now.Add(2*time.Minute))
	require.NoError(t, err)
	updatedRun, err = store.GetRun(ctx, "ws-1", "run-multi-wait")
	require.NoError(t, err)
	require.Equal(t, AutomationRunStatusRunning, updatedRun.Status)
}

func TestGetWaitBySignalUsesExactWorkspaceResourceIdentity(t *testing.T) {
	ctx := context.Background()
	store := newAutomationStoreForTest(t)
	now := time.Date(2026, 6, 7, 9, 0, 0, 0, time.UTC)
	signal := AutomationWaitSignal{WaitKind: AutomationWaitKindWaitEvent, EventType: "provider.ready", ResourceType: "provider", ResourceID: "installation-1"}

	mustCreateRun(t, store, AutomationRun{ID: "run-signal-active", WorkspaceID: "ws-1", Name: "Active", Status: AutomationRunStatusWaiting, CreatedAt: now, UpdatedAt: now})
	activeKey := "ws-1:run-signal-active:job-wait-active"
	active, err := store.CreateWait(ctx,
		AutomationWait{ID: "wait-signal-active", WorkspaceID: "ws-1", RunID: "run-signal-active", JobRunID: "job-wait-active", WaitKind: signal.WaitKind, Status: AutomationWaitStatusActive, ActiveKey: &activeKey, ResourceType: signal.ResourceType, ResourceID: signal.ResourceID, ResumeEventType: signal.EventType, CreatedAt: now, UpdatedAt: now},
		AutomationJobRun{ID: "job-wait-active", WorkspaceID: "ws-1", RunID: "run-signal-active", JobID: "wait", Status: AutomationJobRunStatusWaiting, ScheduledAt: now, CreatedAt: now, UpdatedAt: now},
		AutomationEvent{ID: "event-wait-active", WorkspaceID: "ws-1", RunID: "run-signal-active", EventType: "wait_started", CreatedAt: now})
	require.NoError(t, err)
	require.NotEmpty(t, active.SignalKey)

	mustCreateRun(t, store, AutomationRun{ID: "run-signal-resolved", WorkspaceID: "ws-1", Name: "Resolved", Status: AutomationRunStatusRunning, CreatedAt: now, UpdatedAt: now})
	resolvedAt := now.Add(time.Minute)
	_, err = store.CreateWait(ctx,
		AutomationWait{ID: "wait-signal-resolved", WorkspaceID: "ws-1", RunID: "run-signal-resolved", JobRunID: "job-wait-resolved", WaitKind: signal.WaitKind, Status: AutomationWaitStatusResolved, ResourceType: signal.ResourceType, ResourceID: signal.ResourceID, ResumeEventType: signal.EventType, ResultJSON: `{"ok":true}`, ResolvedAt: &resolvedAt, CreatedAt: now, UpdatedAt: resolvedAt},
		AutomationJobRun{ID: "job-wait-resolved", WorkspaceID: "ws-1", RunID: "run-signal-resolved", JobID: "wait", Status: AutomationJobRunStatusSucceeded, ScheduledAt: now, CreatedAt: now, UpdatedAt: resolvedAt},
		AutomationEvent{ID: "event-wait-resolved", WorkspaceID: "ws-1", RunID: "run-signal-resolved", EventType: "wait_started", CreatedAt: now})
	require.NoError(t, err)

	found, err := store.GetWaitBySignal(ctx, "ws-1", signal)
	require.NoError(t, err)
	require.Equal(t, "wait-signal-active", found.ID, "active wait should win over resolved idempotency record")

	_, err = store.GetWaitBySignal(ctx, "ws-2", signal)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)
	mismatched := signal
	mismatched.ResourceID = "installation-2"
	_, err = store.GetWaitBySignal(ctx, "ws-1", mismatched)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)

	_, _, err = store.ResolveWait(ctx, "ws-1", "wait-signal-active", "automation-eventer", `{"ok":true}`, now.Add(2*time.Minute))
	require.NoError(t, err)
	found, err = store.GetWaitBySignal(ctx, "ws-1", signal)
	require.NoError(t, err)
	require.Equal(t, "wait-signal-active", found.ID)
	require.Equal(t, AutomationWaitStatusResolved, found.Status)
}

func TestListActiveWaitsBySignalReturnsAllSubscribers(t *testing.T) {
	ctx := context.Background()
	store := newAutomationStoreForTest(t)
	now := time.Date(2026, 6, 7, 9, 15, 0, 0, time.UTC)
	signal := AutomationWaitSignal{WaitKind: AutomationWaitKindAgentTask, EventType: "agent_task_completed", ResourceType: "agent_task", ResourceID: "tsk-shared"}

	mustCreateRun(t, store, AutomationRun{ID: "run-signal-active-a", WorkspaceID: "ws-1", Name: "Active A", Status: AutomationRunStatusWaiting, CreatedAt: now, UpdatedAt: now})
	activeKeyA := "ws-1:run-signal-active-a:job-wait"
	_, err := store.CreateWait(ctx,
		AutomationWait{ID: "wait-signal-active-a", WorkspaceID: "ws-1", RunID: "run-signal-active-a", JobRunID: "job-wait-a", WaitKind: signal.WaitKind, Status: AutomationWaitStatusActive, ActiveKey: &activeKeyA, ResourceType: signal.ResourceType, ResourceID: signal.ResourceID, ResumeEventType: signal.EventType, CreatedAt: now, UpdatedAt: now},
		AutomationJobRun{ID: "job-wait-a", WorkspaceID: "ws-1", RunID: "run-signal-active-a", JobID: "wait", Status: AutomationJobRunStatusWaiting, ScheduledAt: now, CreatedAt: now, UpdatedAt: now},
		AutomationEvent{ID: "event-wait-a", WorkspaceID: "ws-1", RunID: "run-signal-active-a", EventType: "wait_started", CreatedAt: now})
	require.NoError(t, err)

	mustCreateRun(t, store, AutomationRun{ID: "run-signal-active-b", WorkspaceID: "ws-1", Name: "Active B", Status: AutomationRunStatusWaiting, CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)})
	activeKeyB := "ws-1:run-signal-active-b:job-wait"
	_, err = store.CreateWait(ctx,
		AutomationWait{ID: "wait-signal-active-b", WorkspaceID: "ws-1", RunID: "run-signal-active-b", JobRunID: "job-wait-b", WaitKind: signal.WaitKind, Status: AutomationWaitStatusActive, ActiveKey: &activeKeyB, ResourceType: signal.ResourceType, ResourceID: signal.ResourceID, ResumeEventType: signal.EventType, CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second)},
		AutomationJobRun{ID: "job-wait-b", WorkspaceID: "ws-1", RunID: "run-signal-active-b", JobID: "wait", Status: AutomationJobRunStatusWaiting, ScheduledAt: now, CreatedAt: now, UpdatedAt: now},
		AutomationEvent{ID: "event-wait-b", WorkspaceID: "ws-1", RunID: "run-signal-active-b", EventType: "wait_started", CreatedAt: now})
	require.NoError(t, err)

	mustCreateRun(t, store, AutomationRun{ID: "run-signal-resolved", WorkspaceID: "ws-1", Name: "Resolved", Status: AutomationRunStatusRunning, CreatedAt: now, UpdatedAt: now})
	resolvedAt := now.Add(2 * time.Second)
	_, err = store.CreateWait(ctx,
		AutomationWait{ID: "wait-signal-resolved", WorkspaceID: "ws-1", RunID: "run-signal-resolved", JobRunID: "job-wait-resolved", WaitKind: signal.WaitKind, Status: AutomationWaitStatusResolved, ResourceType: signal.ResourceType, ResourceID: signal.ResourceID, ResumeEventType: signal.EventType, ResultJSON: `{"ok":true}`, ResolvedAt: &resolvedAt, CreatedAt: now, UpdatedAt: resolvedAt},
		AutomationJobRun{ID: "job-wait-resolved", WorkspaceID: "ws-1", RunID: "run-signal-resolved", JobID: "wait", Status: AutomationJobRunStatusSucceeded, ScheduledAt: now, CreatedAt: now, UpdatedAt: resolvedAt},
		AutomationEvent{ID: "event-wait-resolved", WorkspaceID: "ws-1", RunID: "run-signal-resolved", EventType: "wait_started", CreatedAt: now})
	require.NoError(t, err)

	waits, err := store.ListActiveWaitsBySignal(ctx, "ws-1", signal)
	require.NoError(t, err)
	require.Len(t, waits, 2)
	require.Equal(t, "wait-signal-active-a", waits[0].ID)
	require.Equal(t, "wait-signal-active-b", waits[1].ID)
}

func TestListActiveWaitRunsBySignalHydratesRunsInBatch(t *testing.T) {
	ctx := context.Background()
	store := newAutomationStoreForTest(t)
	now := time.Date(2026, 6, 7, 9, 18, 0, 0, time.UTC)
	signal := AutomationWaitSignal{WaitKind: AutomationWaitKindAgentTask, EventType: "agent_task_completed", ResourceType: "agent_task", ResourceID: "tsk-batch"}

	for i := 0; i < 5; i++ {
		runID := fmt.Sprintf("run-signal-batch-%d", i)
		waitID := fmt.Sprintf("wait-signal-batch-%d", i)
		jobID := fmt.Sprintf("job-signal-batch-%d", i)
		activeKey := fmt.Sprintf("ws-1:%s:%s", runID, jobID)
		mustCreateRun(t, store, AutomationRun{ID: runID, WorkspaceID: "ws-1", Name: runID, Status: AutomationRunStatusWaiting, CreatedAt: now, UpdatedAt: now.Add(time.Duration(i) * time.Second)})
		_, err := store.CreateWait(ctx,
			AutomationWait{ID: waitID, WorkspaceID: "ws-1", RunID: runID, JobRunID: jobID, WaitKind: signal.WaitKind, Status: AutomationWaitStatusActive, ActiveKey: &activeKey, ResourceType: signal.ResourceType, ResourceID: signal.ResourceID, ResumeEventType: signal.EventType, CreatedAt: now, UpdatedAt: now.Add(time.Duration(i) * time.Second)},
			AutomationJobRun{ID: jobID, WorkspaceID: "ws-1", RunID: runID, JobID: "task", Status: AutomationJobRunStatusWaiting, ScheduledAt: now, CreatedAt: now, UpdatedAt: now},
			AutomationEvent{ID: fmt.Sprintf("event-signal-batch-%d", i), WorkspaceID: "ws-1", RunID: runID, EventType: "wait_started", CreatedAt: now})
		require.NoError(t, err)
	}

	counter := &automationSQLCounter{Interface: logger.Default.LogMode(logger.Silent)}
	store.db = store.db.Session(&gorm.Session{Logger: counter})
	waitRuns, err := store.ListActiveWaitRunsBySignal(ctx, "ws-1", signal, AutomationWaitSignalCursor{}, 5)
	require.NoError(t, err)
	require.Len(t, waitRuns, 5)
	require.Equal(t, "wait-signal-batch-0", waitRuns[0].Wait.ID)
	require.Equal(t, "run-signal-batch-0", waitRuns[0].Run.ID)
	require.Equal(t, 1, counter.automationRunSelects, "signal delivery should hydrate matching runs in one batch query")
}

func TestListResolvedWaitsBySignalNeedingSchedulerReplaySkipsHistoricalResolved(t *testing.T) {
	ctx := context.Background()
	store := newAutomationStoreForTest(t)
	now := time.Date(2026, 6, 7, 9, 20, 0, 0, time.UTC)
	signal := AutomationWaitSignal{WaitKind: AutomationWaitKindAgentTask, EventType: "agent_task_completed", ResourceType: "agent_task", ResourceID: "tsk-replay"}

	createRun := func(runID, waitID, waitJobID, downstreamJobID, waitStatus, waitJobStatus, downstreamStatus string, updatedAt time.Time) {
		t.Helper()
		var activeKey *string
		var resolvedAt *time.Time
		if waitStatus == AutomationWaitStatusActive {
			key := "ws-1:" + runID + ":" + waitJobID
			activeKey = &key
		} else {
			resolvedAt = &updatedAt
		}
		runStatus := AutomationRunStatusRunning
		if waitStatus == AutomationWaitStatusActive {
			runStatus = AutomationRunStatusWaiting
		}
		_, err := store.CreateRunWithRuntime(ctx,
			AutomationRun{ID: runID, WorkspaceID: "ws-1", Name: runID, Status: runStatus, CreatedAt: now, UpdatedAt: updatedAt},
			[]AutomationJobRun{
				{ID: waitJobID, WorkspaceID: "ws-1", RunID: runID, JobID: "task", JobType: "github_actions_job", Status: waitJobStatus, ScheduledAt: now, CreatedAt: now, UpdatedAt: updatedAt},
				{ID: downstreamJobID, WorkspaceID: "ws-1", RunID: runID, JobID: "deploy", JobType: "github_actions_job", Status: downstreamStatus, DependsOnJSON: `["` + waitJobID + `"]`, ScheduledAt: now, CreatedAt: now, UpdatedAt: updatedAt},
			},
			[]AutomationEvent{{ID: "event-" + waitID, WorkspaceID: "ws-1", RunID: runID, JobRunID: waitJobID, EventType: "wait_started", CreatedAt: now}},
			nil,
			[]AutomationWait{{
				ID:              waitID,
				WorkspaceID:     "ws-1",
				RunID:           runID,
				JobRunID:        waitJobID,
				WaitKind:        signal.WaitKind,
				Status:          waitStatus,
				ActiveKey:       activeKey,
				ResourceType:    signal.ResourceType,
				ResourceID:      signal.ResourceID,
				ResumeEventType: signal.EventType,
				ResultJSON:      `{"ok":true}`,
				ResolvedAt:      resolvedAt,
				CreatedAt:       now,
				UpdatedAt:       updatedAt,
			}})
		require.NoError(t, err)
	}

	createRun("run-replay-needed", "wait-replay-needed", "job-wait-replay-needed", "job-deploy-replay-needed", AutomationWaitStatusResolved, AutomationJobRunStatusSucceeded, AutomationJobRunStatusPending, now.Add(time.Minute))
	createRun("run-historical-leased", "wait-historical-leased", "job-wait-historical-leased", "job-deploy-historical-leased", AutomationWaitStatusResolved, AutomationJobRunStatusSucceeded, AutomationJobRunStatusLeased, now.Add(2*time.Minute))
	createRun("run-historical-succeeded", "wait-historical-succeeded", "job-wait-historical-succeeded", "job-deploy-historical-succeeded", AutomationWaitStatusResolved, AutomationJobRunStatusSucceeded, AutomationJobRunStatusSucceeded, now.Add(3*time.Minute))
	createRun("run-queued-missing", "wait-queued-missing", "job-wait-queued-missing", "job-deploy-queued-missing", AutomationWaitStatusResolved, AutomationJobRunStatusSucceeded, AutomationJobRunStatusQueued, now.Add(4*time.Minute))
	createRun("run-queued-existing", "wait-queued-existing", "job-wait-queued-existing", "job-deploy-queued-existing", AutomationWaitStatusResolved, AutomationJobRunStatusSucceeded, AutomationJobRunStatusQueued, now.Add(5*time.Minute))
	_, err := store.EnqueueJob(ctx, AutomationJob{WorkspaceID: "ws-1", RunID: "run-queued-existing", JobRunID: "job-deploy-queued-existing", QueueName: "default", Status: AutomationJobStatusReady, ScheduledAt: now, MaxAttempts: 3, IdempotencyKey: "run-queued-existing:deploy", ShardBucket: 0, CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	createRun("run-active", "wait-active", "job-wait-active", "job-deploy-active", AutomationWaitStatusActive, AutomationJobRunStatusWaiting, AutomationJobRunStatusPending, now.Add(6*time.Minute))

	waits, err := store.ListResolvedWaitsBySignalNeedingSchedulerReplay(ctx, "ws-1", signal)
	require.NoError(t, err)
	require.Len(t, waits, 2)
	require.Equal(t, "wait-replay-needed", waits[0].ID)
	require.Equal(t, "wait-queued-missing", waits[1].ID)
}

type automationSQLCounter struct {
	logger.Interface
	automationRunSelects int
}

func (c *automationSQLCounter) Trace(_ context.Context, _ time.Time, fc func() (string, int64), _ error) {
	sql, _ := fc()
	normalized := strings.ToUpper(strings.TrimSpace(sql))
	if strings.HasPrefix(normalized, "SELECT") && strings.Contains(normalized, "AUTOMATION_RUNS") {
		c.automationRunSelects++
	}
}

func TestGetActiveRunnerSessionByTokenRequiresMatchingSessionID(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 6, 12, 0, 0, 0, time.UTC)
	expiresAt := now.Add(time.Hour)

	require.NoError(t, store.db.Create(&AutomationRunner{
		WorkspaceID:  "ws-1",
		RunnerID:     "runner-1",
		Name:         "runner-1",
		Status:       AutomationRunnerStatusActive,
		RegisteredAt: now,
		CreatedAt:    now,
		UpdatedAt:    now,
	}).Error)
	require.NoError(t, store.db.Create(&[]AutomationRunnerSession{
		{WorkspaceID: "ws-1", RunnerID: "runner-1", SessionID: "session-1", LeaseOwner: "lease-1", Status: AutomationRunnerSessionStatusActive, HeartbeatAt: now, ExpiresAt: expiresAt, CreatedAt: now, UpdatedAt: now},
		{WorkspaceID: "ws-1", RunnerID: "runner-1", SessionID: "session-2", LeaseOwner: "lease-2", Status: AutomationRunnerSessionStatusActive, HeartbeatAt: now, ExpiresAt: expiresAt, CreatedAt: now, UpdatedAt: now},
	}).Error)
	require.NoError(t, store.db.Create(&AutomationRunnerToken{
		TokenHash:   "token-session-1",
		WorkspaceID: "ws-1",
		RunnerID:    "runner-1",
		TokenKind:   AutomationRunnerTokenKindSession,
		SessionID:   "session-1",
		ExpiresAt:   expiresAt,
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)

	session, err := store.GetActiveRunnerSessionByToken(ctx, "token-session-1", "ws-1", "runner-1", "session-1", now)
	require.NoError(t, err)
	require.Equal(t, "session-1", session.SessionID)

	_, err = store.GetActiveRunnerSessionByToken(ctx, "token-session-1", "ws-1", "runner-1", "session-2", now)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)

	require.NoError(t, store.db.Create(&AutomationRunnerToken{
		TokenHash:   "token-legacy",
		WorkspaceID: "ws-1",
		RunnerID:    "runner-1",
		TokenKind:   AutomationRunnerTokenKindSession,
		ExpiresAt:   expiresAt,
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)
	legacy, err := store.GetActiveRunnerSessionByToken(ctx, "token-legacy", "ws-1", "runner-1", "session-2", now)
	require.NoError(t, err)
	require.Equal(t, "session-2", legacy.SessionID)
}
