package runtime

import (
	"context"
	"io"
	"log/slog"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/store/automation"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func TestAutomationReconcilerRecoversExpiredLeases(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&automation.AutomationRun{}, &automation.AutomationRepoStatusOutbox{}, &automation.AutomationRunSlot{}, &automation.AutomationJobRun{}, &automation.AutomationEvent{}, &automation.AutomationJob{}, &automation.AutomationWait{}))
	store := automation.New(db)
	now := time.Date(2026, 6, 2, 22, 0, 0, 0, time.UTC)
	leaseUntil := now.Add(-time.Second)
	_, err = store.CreateRun(ctx, automation.AutomationRun{ID: "wfr_reconcile", WorkspaceID: "ws-1", Name: "Reconcile", Status: automation.AutomationRunStatusRunning, CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.NoError(t, store.CreateJobRuns(ctx, []automation.AutomationJobRun{{ID: "wfj_reconcile", WorkspaceID: "ws-1", RunID: "wfr_reconcile", JobID: "build", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusRunning, Attempt: 1, MaxAttempts: 3, ScheduledAt: now, CreatedAt: now, UpdatedAt: now}}))
	_, err = store.EnqueueJob(ctx, automation.AutomationJob{WorkspaceID: "ws-1", RunID: "wfr_reconcile", JobRunID: "wfj_reconcile", QueueName: DefaultQueueName, Status: automation.AutomationJobStatusLeased, LeaseOwner: "runner-1", LeaseUntil: &leaseUntil, Attempt: 1, MaxAttempts: 3, ScheduledAt: now, IdempotencyKey: "wfr_reconcile:build", ShardBucket: 0, CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reconciler := NewReconciler(store, func() time.Time { return now }, logger)
	result, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 10})
	require.NoError(t, err)
	require.EqualValues(t, 1, result.RecoveredExpiredJobs)
	status := reconciler.Status()
	require.Equal(t, now, status.LastStartedAt)
	require.Equal(t, now, status.LastSuccessAt)
	require.Equal(t, now, status.LastCompletedAt)
	require.Zero(t, status.ErrorCount)
	require.EqualValues(t, 1, status.LastResult.RecoveredExpiredJobs)

	jobRun, err := store.GetJobRun(ctx, "ws-1", "wfj_reconcile")
	require.NoError(t, err)
	require.Equal(t, automation.AutomationJobRunStatusQueued, jobRun.Status)
	require.Nil(t, jobRun.LeaseUntil)
	events, err := store.ListEvents(ctx, "ws-1", "wfr_reconcile", 1, 10)
	require.NoError(t, err)
	require.Len(t, events, 1)
	require.Equal(t, "job_retry_scheduled", events[0].EventType)
}

func TestAutomationReconcilerExpiredLeaseExhaustionBlocksDownstream(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&automation.AutomationRun{}, &automation.AutomationRepoStatusOutbox{}, &automation.AutomationRunSlot{}, &automation.AutomationJobRun{}, &automation.AutomationEvent{}, &automation.AutomationJob{}, &automation.AutomationWait{}))
	store := automation.New(db)
	now := time.Date(2026, 6, 3, 8, 30, 0, 0, time.UTC)
	leaseUntil := now.Add(-time.Second)

	_, err = store.CreateRun(ctx, automation.AutomationRun{ID: "wfr_reconcile_exhausted", WorkspaceID: "ws-1", Name: "Reconcile", Status: automation.AutomationRunStatusRunning, CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Hour)})
	require.NoError(t, err)
	require.NoError(t, store.CreateJobRuns(ctx, []automation.AutomationJobRun{
		{ID: "wfj_exhausted_build", WorkspaceID: "ws-1", RunID: "wfr_reconcile_exhausted", JobID: "build", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusRunning, Attempt: 1, MaxAttempts: 1, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute)},
		{ID: "wfj_exhausted_test", WorkspaceID: "ws-1", RunID: "wfr_reconcile_exhausted", JobID: "test", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusPending, MaxAttempts: 1, DependsOnJSON: `["wfj_exhausted_build"]`, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute)},
	}))
	_, err = store.EnqueueJob(ctx, automation.AutomationJob{WorkspaceID: "ws-1", RunID: "wfr_reconcile_exhausted", JobRunID: "wfj_exhausted_build", QueueName: DefaultQueueName, Status: automation.AutomationJobStatusLeased, LeaseOwner: "runner-1", LeaseUntil: &leaseUntil, Attempt: 1, MaxAttempts: 1, ScheduledAt: now.Add(-time.Hour), IdempotencyKey: "wfr_reconcile_exhausted:build", ShardBucket: 0, CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute)})
	require.NoError(t, err)

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reconciler := NewReconciler(store, func() time.Time { return now }, logger)
	result, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 10})
	require.NoError(t, err)
	require.EqualValues(t, 1, result.RecoveredExpiredJobs)
	require.EqualValues(t, 0, result.ReconciledDependencies)

	build, err := store.GetJobRun(ctx, "ws-1", "wfj_exhausted_build")
	require.NoError(t, err)
	require.Equal(t, automation.AutomationJobRunStatusFailed, build.Status)
	test, err := store.GetJobRun(ctx, "ws-1", "wfj_exhausted_test")
	require.NoError(t, err)
	require.Equal(t, automation.AutomationJobRunStatusBlocked, test.Status)
	run, err := store.GetRun(ctx, "ws-1", "wfr_reconcile_exhausted")
	require.NoError(t, err)
	require.Equal(t, automation.AutomationRunStatusFailed, run.Status)
}

func TestAutomationReconcilerBlocksFailedDependenciesAfterRestart(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&automation.AutomationRun{}, &automation.AutomationRepoStatusOutbox{}, &automation.AutomationRunSlot{}, &automation.AutomationJobRun{}, &automation.AutomationEvent{}, &automation.AutomationJob{}, &automation.AutomationWait{}))
	store := automation.New(db)
	now := time.Date(2026, 6, 3, 9, 0, 0, 0, time.UTC)
	completedAt := now.Add(-time.Minute)
	_, err = store.CreateRun(ctx, automation.AutomationRun{ID: "wfr_restart_failed", WorkspaceID: "ws-1", Name: "Restart", Status: automation.AutomationRunStatusFailed, ErrorJSON: `{"message":"old failure"}`, CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute), CompletedAt: &completedAt})
	require.NoError(t, err)
	require.NoError(t, store.CreateJobRuns(ctx, []automation.AutomationJobRun{
		{ID: "wfj_restart_build", WorkspaceID: "ws-1", RunID: "wfr_restart_failed", JobID: "build", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusFailed, ErrorJSON: `{"message":"old failure"}`, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute), CompletedAt: &completedAt},
		{ID: "wfj_restart_test", WorkspaceID: "ws-1", RunID: "wfr_restart_failed", JobID: "test", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusPending, DependsOnJSON: `["wfj_restart_build"]`, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute)},
		{ID: "wfj_restart_deploy", WorkspaceID: "ws-1", RunID: "wfr_restart_failed", JobID: "deploy", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusPending, DependsOnJSON: `["wfj_restart_test"]`, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute)},
	}))

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reconciler := NewReconciler(store, func() time.Time { return now }, logger)
	result, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 10})
	require.NoError(t, err)
	require.EqualValues(t, 1, result.ReconciledDependencies)

	jobRuns, err := store.ListJobRuns(ctx, "ws-1", "wfr_restart_failed")
	require.NoError(t, err)
	statusByJobID := map[string]string{}
	for _, jobRun := range jobRuns {
		statusByJobID[jobRun.JobID] = jobRun.Status
	}
	require.Equal(t, automation.AutomationJobRunStatusFailed, statusByJobID["build"])
	require.Equal(t, automation.AutomationJobRunStatusBlocked, statusByJobID["test"])
	require.Equal(t, automation.AutomationJobRunStatusBlocked, statusByJobID["deploy"])
	again, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 10})
	require.NoError(t, err)
	require.Zero(t, again.ReconciledDependencies)
}

func TestAutomationReconcilerCompletesRunsWithTerminalJobLedger(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&automation.AutomationRun{}, &automation.AutomationRepoStatusOutbox{}, &automation.AutomationRunSlot{}, &automation.AutomationJobRun{}, &automation.AutomationEvent{}, &automation.AutomationJob{}, &automation.AutomationWait{}))
	store := automation.New(db)
	now := time.Date(2026, 6, 3, 9, 30, 0, 0, time.UTC)
	_, err = store.CreateRun(ctx, automation.AutomationRun{ID: "wfr_terminal", WorkspaceID: "ws-1", Name: "Terminal", Status: automation.AutomationRunStatusRunning, CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Hour)})
	require.NoError(t, err)
	require.NoError(t, store.CreateJobRuns(ctx, []automation.AutomationJobRun{
		{ID: "wfj_terminal_build", WorkspaceID: "ws-1", RunID: "wfr_terminal", JobID: "build", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusSucceeded, Attempt: 1, MaxAttempts: 3, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute), CompletedAt: &now},
		{ID: "wfj_terminal_test", WorkspaceID: "ws-1", RunID: "wfr_terminal", JobID: "test", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusSkipped, Attempt: 0, MaxAttempts: 1, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Minute), CompletedAt: &now},
	}))
	metricsBefore, err := store.RunMetrics(ctx, "")
	require.NoError(t, err)
	require.EqualValues(t, 1, metricsBefore.TerminalInconsistent)

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reconciler := NewReconciler(store, func() time.Time { return now }, logger)
	result, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 10})
	require.NoError(t, err)
	require.EqualValues(t, 1, result.ReconciledRuns)

	run, err := store.GetRun(ctx, "ws-1", "wfr_terminal")
	require.NoError(t, err)
	require.Equal(t, automation.AutomationRunStatusSucceeded, run.Status)
	require.NotNil(t, run.CompletedAt)
	metricsAfter, err := store.RunMetrics(ctx, "")
	require.NoError(t, err)
	require.EqualValues(t, 0, metricsAfter.TerminalInconsistent)
	events, err := store.ListEvents(ctx, "ws-1", "wfr_terminal", 1, 10)
	require.NoError(t, err)
	require.Len(t, events, 1)
	require.Equal(t, "run_reconciled", events[0].EventType)
	require.Contains(t, events[0].MetadataJSON, "total_jobs")
}

func TestAutomationReconcilerTerminalRunSelectionDoesNotStarveBehindActiveRuns(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&automation.AutomationRun{}, &automation.AutomationRepoStatusOutbox{}, &automation.AutomationRunSlot{}, &automation.AutomationJobRun{}, &automation.AutomationEvent{}, &automation.AutomationJob{}, &automation.AutomationWait{}))
	store := automation.New(db)
	now := time.Date(2026, 6, 3, 10, 30, 0, 0, time.UTC)
	old := now.Add(-2 * time.Hour)
	completedAt := now.Add(-time.Minute)

	for _, id := range []string{"wfr_active_1", "wfr_active_2"} {
		_, err = store.CreateRun(ctx, automation.AutomationRun{ID: id, WorkspaceID: "ws-1", Name: "Active", Status: automation.AutomationRunStatusRunning, CreatedAt: old, UpdatedAt: old})
		require.NoError(t, err)
		require.NoError(t, store.CreateJobRuns(ctx, []automation.AutomationJobRun{
			{ID: id + "_job", WorkspaceID: "ws-1", RunID: id, JobID: "build", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusRunning, ScheduledAt: old, CreatedAt: old, UpdatedAt: old},
		}))
	}
	_, err = store.CreateRun(ctx, automation.AutomationRun{ID: "wfr_terminal_later", WorkspaceID: "ws-1", Name: "Terminal", Status: automation.AutomationRunStatusRunning, CreatedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Hour)})
	require.NoError(t, err)
	require.NoError(t, store.CreateJobRuns(ctx, []automation.AutomationJobRun{
		{ID: "wfj_terminal_later", WorkspaceID: "ws-1", RunID: "wfr_terminal_later", JobID: "test", JobType: "github_actions_job", Status: automation.AutomationJobRunStatusSucceeded, ScheduledAt: now.Add(-time.Hour), CreatedAt: now.Add(-time.Hour), UpdatedAt: completedAt, CompletedAt: &completedAt},
	}))

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reconciler := NewReconciler(store, func() time.Time { return now }, logger)
	result, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 1})
	require.NoError(t, err)
	require.EqualValues(t, 1, result.ReconciledRuns)

	terminalRun, err := store.GetRun(ctx, "ws-1", "wfr_terminal_later")
	require.NoError(t, err)
	require.Equal(t, automation.AutomationRunStatusSucceeded, terminalRun.Status)
	activeRun, err := store.GetRun(ctx, "ws-1", "wfr_active_1")
	require.NoError(t, err)
	require.Equal(t, automation.AutomationRunStatusRunning, activeRun.Status)
}

func TestAutomationReconcilerRepairsLeakedActiveRunSlots(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&automation.AutomationRun{}, &automation.AutomationRepoStatusOutbox{}, &automation.AutomationRunSlot{}, &automation.AutomationJobRun{}, &automation.AutomationEvent{}, &automation.AutomationJob{}, &automation.AutomationWait{}))
	store := automation.New(db)
	now := time.Date(2026, 6, 3, 11, 0, 0, 0, time.UTC)
	completedAt := now.Add(-time.Minute)
	require.NoError(t, db.Create([]automation.AutomationRun{
		{ID: "wfr_active_slot", WorkspaceID: "ws-1", Name: "Active", Status: automation.AutomationRunStatusRunning, CreatedAt: now, UpdatedAt: now},
		{ID: "wfr_terminal_slot", WorkspaceID: "ws-1", Name: "Terminal", Status: automation.AutomationRunStatusFailed, CompletedAt: &completedAt, CreatedAt: now, UpdatedAt: now},
	}).Error)
	require.NoError(t, db.Create([]automation.AutomationRunSlot{
		{WorkspaceID: "ws-1", SlotIndex: 0, RunID: "wfr_active_slot", CreatedAt: now, UpdatedAt: now},
		{WorkspaceID: "ws-1", SlotIndex: 1, RunID: "wfr_terminal_slot", CreatedAt: now, UpdatedAt: now},
		{WorkspaceID: "ws-1", SlotIndex: 2, RunID: "wfr_missing_slot", CreatedAt: now, UpdatedAt: now},
	}).Error)

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reconciler := NewReconciler(store, func() time.Time { return now }, logger)
	result, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 10})
	require.NoError(t, err)
	require.EqualValues(t, 2, result.RepairedActiveRunSlots)

	metrics, err := store.RunMetrics(ctx, "ws-1")
	require.NoError(t, err)
	require.EqualValues(t, 1, metrics.ActiveSlots)
	require.Zero(t, metrics.LeakedActiveSlots)
}

func TestAutomationReconcilerStatusRecordsFailures(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 6, 3, 11, 15, 0, 0, time.UTC)
	reconciler := NewReconciler(nil, func() time.Time { return now }, slog.New(slog.NewTextHandler(io.Discard, nil)))

	_, err := reconciler.Reconcile(ctx, ReconcileOptions{Limit: 10})
	require.Error(t, err)
	status := reconciler.Status()
	require.Equal(t, now, status.LastStartedAt)
	require.Equal(t, now, status.LastFailureAt)
	require.Equal(t, now, status.LastCompletedAt)
	require.EqualValues(t, 1, status.ErrorCount)
	require.EqualValues(t, 1, status.ConsecutiveFailures)
	require.Contains(t, status.LastError, "store is nil")
}
