package schedule

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/runeffect"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	schedulestore "github.com/tidbcloud/AgentTeam/internal/store/schedule"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type fakeStore struct {
	dueIDs                   []string
	retryIDs                 []string
	staleIDs                 []string
	abandonedRetryRunCount   int
	reserve                  schedulestore.ReservedRun
	claim                    schedulestore.ReservedRun
	reserveCalls             []string
	completed                []schedulestore.AutomationRunCompletion
	retryUpdates             []schedulestore.AutomationRunRetryUpdate
	recovered                []string
	abandonInactiveRetryPass int
}

func (store *fakeStore) ListDueDefinitionIDs(_ context.Context, _ schedulestore.DueDefinitionListQuery) ([]string, error) {
	return append([]string(nil), store.dueIDs...), nil
}

func (store *fakeStore) ReserveDueAutomationRun(_ context.Context, definitionID string, _ time.Time) (schedulestore.ReservedRun, bool, error) {
	store.reserveCalls = append(store.reserveCalls, definitionID)
	if definitionID == store.reserve.Definition.ID {
		return store.reserve, true, nil
	}
	return schedulestore.ReservedRun{}, false, nil
}

func (store *fakeStore) ListRetryRunIDs(context.Context, time.Time, int) ([]string, error) {
	return append([]string(nil), store.retryIDs...), nil
}

func (store *fakeStore) ClaimRetryAutomationRun(_ context.Context, runID string, _ time.Time) (schedulestore.ReservedRun, bool, error) {
	if runID == store.claim.Run.ID {
		return store.claim, true, nil
	}
	return schedulestore.ReservedRun{}, false, nil
}

func (store *fakeStore) AbandonInactiveRetryRuns(context.Context, time.Time, int) (int, error) {
	store.abandonInactiveRetryPass++
	return store.abandonedRetryRunCount, nil
}

func (store *fakeStore) MarkAutomationRunRetryWaiting(_ context.Context, params schedulestore.AutomationRunRetryUpdate) (schedulestore.AutomationRun, error) {
	store.retryUpdates = append(store.retryUpdates, params)
	return schedulestore.AutomationRun{ID: params.ID, Status: string(runeffect.StatusRetryWaiting)}, nil
}

func (store *fakeStore) CompleteAutomationRun(_ context.Context, params schedulestore.AutomationRunCompletion) (schedulestore.AutomationRun, error) {
	store.completed = append(store.completed, params)
	return schedulestore.AutomationRun{ID: params.ID, Status: string(params.Status)}, nil
}

func (store *fakeStore) ListStaleRunningRunIDs(context.Context, time.Time, int) ([]string, error) {
	return append([]string(nil), store.staleIDs...), nil
}

func (store *fakeStore) RecoverStaleRunningRun(_ context.Context, runID string, _ time.Time, _ time.Time, _ int) (bool, error) {
	store.recovered = append(store.recovered, runID)
	return true, nil
}

type fakeWriter struct {
	requests []EffectRequest
	effect   runeffect.Effect
	err      error
}

func (writer *fakeWriter) WriteScheduleEffect(_ context.Context, request EffectRequest) (runeffect.Effect, error) {
	writer.requests = append(writer.requests, request)
	if writer.err != nil {
		return runeffect.Effect{}, writer.err
	}
	if writer.effect.Kind != "" {
		return writer.effect, nil
	}
	return runeffect.MessageEffect("msg_schedule_effect"), nil
}

func TestServiceRunOnceCompletesDueScheduleRun(t *testing.T) {
	now := time.Date(2026, 5, 30, 10, 0, 0, 0, time.UTC)
	reserved := reservedScheduleRun("sch_due", "arun_due", now)
	mentionHints := []message.MessageMentionHint{{
		Type:  "agent",
		ID:    "agt_due",
		Token: "before-rename",
	}}
	metadata, err := marshalMessageOptionsMetadata(MessageOptions{AlsoStartThread: true, MentionHints: mentionHints})
	require.NoError(t, err)
	reserved.Definition.Metadata = metadata
	reserved.Run.Metadata = metadata
	store := &fakeStore{dueIDs: []string{"sch_due"}, reserve: reserved}
	writer := &fakeWriter{effect: runeffect.MessageEffect("msg_due")}
	service := New(nil, store, ServiceOptions{Clock: func() time.Time { return now }, Writer: writer})

	result, err := service.RunOnce(context.Background())
	require.NoError(t, err)
	require.Equal(t, 1, result.Due)
	require.Len(t, writer.requests, 1)
	require.Equal(t, "schedule:sch_due:"+now.Format(time.RFC3339Nano), writer.requests[0].ClientMessageID)
	require.Equal(t, message.ContributionSourceKindScheduleRun, writer.requests[0].Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeAutomationRun, writer.requests[0].Source.RefType)
	require.Equal(t, "arun_due", writer.requests[0].Source.RefID)
	require.True(t, writer.requests[0].AlsoStartThread)
	require.Equal(t, mentionHints, writer.requests[0].MentionHints)
	require.Len(t, store.completed, 1)
	require.Equal(t, runeffect.StatusSucceeded, store.completed[0].Status)
	require.Equal(t, "msg_due", store.completed[0].Effect.MessageID)
}

func TestServiceRunOnceRetriesTransientFailure(t *testing.T) {
	now := time.Date(2026, 5, 30, 10, 5, 0, 0, time.UTC)
	reserved := reservedScheduleRun("sch_retry", "arun_retry", now)
	reserved.Run.AttemptCount = 1
	store := &fakeStore{dueIDs: []string{"sch_retry"}, reserve: reserved}
	writer := &fakeWriter{err: errors.New("database timeout")}
	service := New(nil, store, ServiceOptions{Clock: func() time.Time { return now }, Writer: writer, RetryBackoff: 10 * time.Second, MaxAttempts: 3})

	result, err := service.RunOnce(context.Background())
	require.NoError(t, err)
	require.Equal(t, 1, result.Due)
	require.Empty(t, store.completed)
	require.Len(t, store.retryUpdates, 1)
	require.Equal(t, "arun_retry", store.retryUpdates[0].ID)
	require.Equal(t, schedulestore.FailureCodeTransientWriteError, store.retryUpdates[0].ErrorCode)
	require.Equal(t, now.Add(10*time.Second), store.retryUpdates[0].NextRetryAt)
}

func TestServiceRunOnceBlocksDefinitionOnPermissionFailure(t *testing.T) {
	now := time.Date(2026, 5, 30, 10, 10, 0, 0, time.UTC)
	reserved := reservedScheduleRun("sch_block", "arun_block", now)
	reserved.Run.AttemptCount = 3
	store := &fakeStore{dueIDs: []string{"sch_block"}, reserve: reserved}
	writer := &fakeWriter{err: storeerr.Forbidden("no target access")}
	service := New(nil, store, ServiceOptions{Clock: func() time.Time { return now }, Writer: writer, MaxAttempts: 3})

	result, err := service.RunOnce(context.Background())
	require.NoError(t, err)
	require.Equal(t, 1, result.Due)
	require.Len(t, store.completed, 1)
	require.Equal(t, runeffect.StatusFailed, store.completed[0].Status)
	require.Equal(t, schedulestore.FailureCodeForbiddenCurrentPerms, store.completed[0].Error.Code)
	require.True(t, store.completed[0].BlockDefinition)
}

func TestServiceRunOnceClaimsRetryAndRecoversStale(t *testing.T) {
	now := time.Date(2026, 5, 30, 10, 15, 0, 0, time.UTC)
	claimed := reservedScheduleRun("sch_retry", "arun_retry", now)
	store := &fakeStore{staleIDs: []string{"arun_stale"}, retryIDs: []string{"arun_retry"}, claim: claimed}
	writer := &fakeWriter{effect: runeffect.MessageEffect("msg_retry")}
	service := New(nil, store, ServiceOptions{Clock: func() time.Time { return now }, Writer: writer})

	result, err := service.RunOnce(context.Background())
	require.NoError(t, err)
	require.Equal(t, 1, result.RecoveredStale)
	require.Equal(t, 1, result.Retried)
	require.Equal(t, []string{"arun_stale"}, store.recovered)
	require.Len(t, store.completed, 1)
	require.Equal(t, "arun_retry", store.completed[0].ID)
}

func TestServiceRunOnceAbandonsInactiveRetryRuns(t *testing.T) {
	now := time.Date(2026, 5, 30, 10, 20, 0, 0, time.UTC)
	store := &fakeStore{abandonedRetryRunCount: 2}
	writer := &fakeWriter{}
	service := New(nil, store, ServiceOptions{Clock: func() time.Time { return now }, Writer: writer})

	result, err := service.RunOnce(context.Background())
	require.NoError(t, err)
	require.Equal(t, 2, result.AbandonedRetries)
	require.Equal(t, 1, store.abandonInactiveRetryPass)
}

func TestValidateEffectRequestRequiresNativeScheduleSource(t *testing.T) {
	request := EffectRequest{
		WorkspaceID:     "wsp_1",
		TargetRef:       "chn_1",
		Content:         "hello",
		ClientMessageID: "schedule:one",
		Source: message.ContributionSource{
			Kind:    message.ContributionSourceKindScheduleRun,
			RefType: message.ContributionSourceRefTypeAutomationRun,
			RefID:   "arun_1",
		},
	}
	require.NoError(t, ValidateEffectRequest(request))
	request.Source.RefType = message.ContributionSourceRefTypeServiceEventDelivery
	require.Error(t, ValidateEffectRequest(request))
}

func reservedScheduleRun(definitionID string, runID string, occurrence time.Time) schedulestore.ReservedRun {
	definition := schedulestore.Definition{
		ID:                     definitionID,
		WorkspaceID:            "wsp_schedule",
		OwnerMemberID:          "mem_owner",
		PrincipalMemberID:      "mem_owner",
		Name:                   "Scheduled message",
		SourceKind:             schedulestore.SourceKindTime,
		SourceType:             schedulestore.SourceTypeOneShotAbsolute,
		TargetKind:             schedulestore.TargetKindChannel,
		TargetRef:              "chn_schedule",
		DeliveryScopeID:        "scp_schedule",
		PermissionCheckScopeID: "scp_schedule",
		MessageTemplate:        "scheduled hello",
		NotificationMode:       schedulestore.NotificationModeNone,
	}
	return schedulestore.ReservedRun{
		Definition: definition,
		Run: schedulestore.AutomationRun{
			ID:                        runID,
			WorkspaceID:               definition.WorkspaceID,
			AutomationKind:            schedulestore.AutomationKindSchedule,
			AutomationID:              definition.ID,
			IdempotencyKey:            "schedule:" + definition.ID + ":" + occurrence.Format(time.RFC3339Nano),
			SourceOccurrenceRef:       occurrence.Format(time.RFC3339Nano),
			SourceKind:                definition.SourceKind,
			SourceType:                definition.SourceType,
			Status:                    string(runeffect.StatusRunning),
			ScheduledFor:              &occurrence,
			OccurredAt:                &occurrence,
			AttemptCount:              1,
			AttemptedAt:               &occurrence,
			TargetKind:                definition.TargetKind,
			TargetRef:                 definition.TargetRef,
			DeliveryScopeID:           definition.DeliveryScopeID,
			PermissionCheckScopeID:    definition.PermissionCheckScopeID,
			OwnerMemberIDSnapshot:     definition.OwnerMemberID,
			PrincipalMemberIDSnapshot: definition.PrincipalMemberID,
			AuthorMemberIDSnapshot:    definition.PrincipalMemberID,
			AutomationNameSnapshot:    definition.Name,
			NotificationModeSnapshot:  definition.NotificationMode,
			MessageTemplateSnapshot:   definition.MessageTemplate,
		},
		CompleteDefinitionAfterSuccess: true,
	}
}
