package orchestrator

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	workflowrunstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
)

type reducerRunnerStub struct {
	calls int
	err   error
}

func (stub *reducerRunnerStub) RunOnce(context.Context) (RunOnceOutcome, error) {
	stub.calls++
	return RunOnceOutcome{Claimed: 2, Succeeded: 1, Failed: 1}, stub.err
}

type effectRunnerStub struct {
	calls int
	err   error
}

func (stub *effectRunnerStub) DispatchOnce(context.Context) (DispatchOutcome, error) {
	stub.calls++
	return DispatchOutcome{Candidates: 3, Claimed: 2, Resolved: 2}, stub.err
}

type artifactMaintenanceStub struct {
	calls int
	last  time.Time
}

func (stub *artifactMaintenanceStub) RunArtifactMaintenance(_ context.Context, now time.Time, limit int) (workflowrunstore.ArtifactMaintenanceResult, error) {
	stub.calls++
	stub.last = now
	if limit != 100 {
		return workflowrunstore.ArtifactMaintenanceResult{}, fmt.Errorf("unexpected maintenance limit %d", limit)
	}
	return workflowrunstore.ArtifactMaintenanceResult{AssignedExpiry: 1, Deleted: 2, OrphanedBlobs: 3}, nil
}

func TestWorkerRunsReducerThenEffectsAndArtifactMaintenance(t *testing.T) {
	reducer := &reducerRunnerStub{}
	effects := &effectRunnerStub{}
	maintenance := &artifactMaintenanceStub{}
	worker := NewWorker(reducer, effects, WorkerOptions{ArtifactMaintenance: maintenance})
	outcome, err := worker.RunOnce(context.Background())
	require.NoError(t, err)
	require.Equal(t, 1, maintenance.calls)
	require.False(t, maintenance.last.IsZero())
	require.Equal(t, int64(1), outcome.Artifacts.AssignedExpiry)
	require.Equal(t, int64(2), outcome.Artifacts.Deleted)
	require.Equal(t, int64(3), outcome.Artifacts.OrphanedBlobs)
}

func TestWorkerRunsReducerThenEffectsAndJoinsErrors(t *testing.T) {
	reducer := &reducerRunnerStub{err: errors.New("run error")}
	effects := &effectRunnerStub{err: errors.New("effect error")}
	worker := NewWorker(reducer, effects, WorkerOptions{})
	outcome, err := worker.RunOnce(context.Background())
	require.ErrorContains(t, err, "run error")
	require.ErrorContains(t, err, "effect error")
	require.Equal(t, 1, reducer.calls)
	require.Equal(t, 1, effects.calls)
	require.Equal(t, 2, outcome.Runs.Claimed)
	require.Equal(t, 3, outcome.Effects.Candidates)
}

// TestWorkerDefaultTickIsOneSecond is the 1-second worker tick regression: the
// production worker is constructed without a PollInterval, so NewWorker must keep
// the default 1-second tick. A future change that silently alters the cadence is
// caught here.
func TestWorkerDefaultTickIsOneSecond(t *testing.T) {
	require.Equal(t, time.Second, defaultRuntimeWorkerPollInterval,
		"the default worker tick must stay exactly one second")
	reducer := &reducerRunnerStub{}
	effects := &effectRunnerStub{}
	worker := NewWorker(reducer, effects, WorkerOptions{})
	require.Equal(t, time.Second, worker.interval, "an unset PollInterval must default to the 1-second tick")
	explicit := NewWorker(reducer, effects, WorkerOptions{PollInterval: 500 * time.Millisecond})
	require.Equal(t, 500*time.Millisecond, explicit.interval, "an explicit PollInterval must be honored")
}
