package calendar

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	calendarstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/calendar"
	"github.com/tidbcloud/AgentTeam/internal/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/workflow/invocation"
)

type calendarRepository struct {
	runs         []calendarstore.Run
	sources      []calendarstore.ScheduleSource
	visitSources func(func(calendarstore.ScheduleSource) error) error
	params       calendarstore.ListSnapshotParams
	err          error
}

func (repository *calendarRepository) VisitSnapshot(
	_ context.Context,
	params calendarstore.ListSnapshotParams,
	visitor calendarstore.SnapshotVisitor,
) error {
	repository.params = params
	if repository.err != nil {
		return repository.err
	}
	if err := visitor.Runs(repository.runs); err != nil {
		return err
	}
	if repository.visitSources != nil {
		return repository.visitSources(visitor.Source)
	}
	for _, source := range repository.sources {
		if err := visitor.Source(source); err != nil {
			return err
		}
	}
	return nil
}

func TestListCombinesRunsAndProjectedOccurrencesInStableTimeOrder(t *testing.T) {
	from := time.Date(2026, 8, 17, 16, 0, 0, 0, time.UTC)
	to := from.Add(72 * time.Hour)
	anchor := from.Add(10 * time.Hour)
	interval := int64(24 * 60 * 60)
	source := scheduleSource(t, binding.ScheduleSource{
		Mode: binding.ScheduleModeInterval, IntervalSeconds: &interval, AnchorAt: &anchor,
	}, anchor)
	repository := &calendarRepository{
		runs: []calendarstore.Run{{
			InvocationID: "wiv_manual", RunID: "wri_manual", PlanID: "pln_manual",
			PlanVersionID: "plv_manual", WorkflowName: "Manual workflow",
			SourceKind: invocation.SourceKindManual, OccurrenceKey: "manual:key",
			OccursAt: from.Add(2 * time.Hour), QueuedAt: from.Add(2 * time.Hour), Status: "succeeded",
		}},
		sources: []calendarstore.ScheduleSource{source},
	}

	result, err := New(repository).List(context.Background(), ListInput{
		WorkspaceID: " wsp_1 ", From: from, To: to,
	})
	require.NoError(t, err)
	require.Equal(t, "wsp_1", repository.params.WorkspaceID)
	require.Equal(t, MaxItems+1, repository.params.RunLimit)
	require.Len(t, result.Items, 4)
	require.Equal(t, []string{ItemKindRun, ItemKindScheduled, ItemKindScheduled, ItemKindScheduled}, []string{
		result.Items[0].Kind, result.Items[1].Kind, result.Items[2].Kind, result.Items[3].Kind,
	})
	require.Equal(t, anchor, result.Items[1].OccursAt)
	require.Equal(t, "Daily report", result.Items[1].Workflow.DisplayName)
	require.Nil(t, result.Items[1].Run)
	require.Equal(t, invocation.ScheduleOccurrenceRef(source.ID, anchor), result.Items[1].Schedule.OccurrenceKey)
}

func TestListReplacesAcceptedScheduleOccurrenceWithItsRun(t *testing.T) {
	from := time.Date(2026, 8, 18, 0, 0, 0, 0, time.UTC)
	to := from.Add(3 * time.Hour)
	interval := int64(60 * 60)
	anchor := from
	source := scheduleSource(t, binding.ScheduleSource{
		Mode: binding.ScheduleModeInterval, IntervalSeconds: &interval, AnchorAt: &anchor,
	}, anchor)
	key := invocation.ScheduleOccurrenceRef(source.ID, anchor)
	repository := &calendarRepository{
		runs: []calendarstore.Run{{
			InvocationID: "wiv_accepted", RunID: "wri_accepted", PlanID: source.PlanID,
			PlanVersionID: source.PlanVersionID, WorkflowName: "Daily report",
			BindingID: source.BindingID, BindingRevisionID: source.BindingRevisionID,
			BindingSourceID: source.ID, SourceKind: invocation.SourceKindSchedule,
			OccurrenceKey: key, OccursAt: anchor, QueuedAt: anchor.Add(time.Minute), Status: "running",
		}},
		// A synthetic duplicate cursor exercises defensive de-duplication. A real
		// repeatable-read snapshot normally observes the cursor already advanced.
		sources: []calendarstore.ScheduleSource{source},
	}

	result, err := New(repository).List(context.Background(), ListInput{WorkspaceID: "wsp_1", From: from, To: to})
	require.NoError(t, err)
	require.Len(t, result.Items, 3)
	require.Equal(t, ItemKindRun, result.Items[0].Kind)
	require.Equal(t, "wri_accepted", result.Items[0].Run.ID)
	require.Equal(t, from.Add(time.Hour), result.Items[1].OccursAt)
}

func TestListProjectsCronAcrossDSTUsingScheduleTimezone(t *testing.T) {
	from := time.Date(2026, 11, 1, 4, 0, 0, 0, time.UTC)
	to := time.Date(2026, 11, 1, 8, 0, 0, 0, time.UTC)
	first := time.Date(2026, 11, 1, 5, 30, 0, 0, time.UTC)
	source := scheduleSource(t, binding.ScheduleSource{
		Mode: binding.ScheduleModeCron, CronExpression: "30 1 * * *", Timezone: "America/New_York",
	}, first)
	repository := &calendarRepository{sources: []calendarstore.ScheduleSource{source}}

	result, err := New(repository).List(context.Background(), ListInput{WorkspaceID: "wsp_1", From: from, To: to})
	require.NoError(t, err)
	require.Equal(t, []time.Time{
		time.Date(2026, 11, 1, 5, 30, 0, 0, time.UTC),
		time.Date(2026, 11, 1, 6, 30, 0, 0, time.UTC),
	}, []time.Time{result.Items[0].OccursAt, result.Items[1].OccursAt})
}

func TestListDoesNotRecreateConsumedOneShotBeforeWindow(t *testing.T) {
	from := time.Date(2026, 8, 18, 0, 0, 0, 0, time.UTC)
	scheduledAt := from.Add(-time.Hour)
	source := scheduleSource(t, binding.ScheduleSource{
		Mode: binding.ScheduleModeAbsolute, ScheduledAt: &scheduledAt,
	}, scheduledAt)
	repository := &calendarRepository{sources: []calendarstore.ScheduleSource{source}}

	result, err := New(repository).List(context.Background(), ListInput{
		WorkspaceID: "wsp_1", From: from, To: from.Add(24 * time.Hour),
	})
	require.NoError(t, err)
	require.Empty(t, result.Items)
}

func TestListCountsProjectedItemsInsteadOfRawScheduleSources(t *testing.T) {
	from := time.Date(2026, 8, 18, 0, 0, 0, 0, time.UTC)
	to := from.Add(time.Hour)
	anchor := from.Add(-time.Hour)
	interval := int64((24 * time.Hour) / time.Second)
	longPeriod := scheduleSource(t, binding.ScheduleSource{
		Mode: binding.ScheduleModeInterval, IntervalSeconds: &interval, AnchorAt: &anchor,
	}, anchor)
	scheduledAt := from.Add(30 * time.Minute)
	due := scheduleSource(t, binding.ScheduleSource{
		Mode: binding.ScheduleModeAbsolute, ScheduledAt: &scheduledAt,
	}, scheduledAt)
	due.ID = "wbs_due"
	visited := 0
	repository := &calendarRepository{visitSources: func(yield func(calendarstore.ScheduleSource) error) error {
		for index := 0; index <= MaxItems; index++ {
			source := longPeriod
			source.ID = fmt.Sprintf("wbs_%d", index)
			visited++
			if err := yield(source); err != nil {
				return err
			}
		}
		visited++
		return yield(due)
	}}

	result, err := New(repository).List(context.Background(), ListInput{
		WorkspaceID: "wsp_1", From: from, To: to,
	})
	require.NoError(t, err)
	require.Len(t, result.Items, 1)
	require.Equal(t, scheduledAt, result.Items[0].OccursAt)
	require.Equal(t, MaxItems+2, visited)
}

func TestListRejectsOversizedAndDenseWindowsWithoutTruncation(t *testing.T) {
	from := time.Date(2026, 8, 18, 0, 0, 0, 0, time.UTC)
	service := New(&calendarRepository{})
	_, err := service.List(context.Background(), ListInput{
		WorkspaceID: "wsp_1", From: from, To: from.Add(MaxWindow + time.Microsecond),
	})
	require.Error(t, err)
	code, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeInvalidWindow, code)

	dense := make([]calendarstore.Run, MaxItems+1)
	repository := &calendarRepository{runs: dense}
	_, err = New(repository).List(context.Background(), ListInput{
		WorkspaceID: "wsp_1", From: from, To: from.Add(time.Hour),
	})
	require.Error(t, err)
	code, ok = ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeWindowTooDense, code)
}

func TestListStopsStreamingAtFirstProjectedItemOverLimit(t *testing.T) {
	from := time.Date(2026, 8, 18, 0, 0, 0, 0, time.UTC)
	scheduledAt := from.Add(30 * time.Minute)
	due := scheduleSource(t, binding.ScheduleSource{
		Mode: binding.ScheduleModeAbsolute, ScheduledAt: &scheduledAt,
	}, scheduledAt)
	visited := 0
	repository := &calendarRepository{visitSources: func(yield func(calendarstore.ScheduleSource) error) error {
		for index := 0; index < MaxItems+100; index++ {
			source := due
			source.ID = fmt.Sprintf("wbs_%d", index)
			visited++
			if err := yield(source); err != nil {
				return err
			}
		}
		return nil
	}}

	_, err := New(repository).List(context.Background(), ListInput{
		WorkspaceID: "wsp_1", From: from, To: from.Add(time.Hour),
	})
	require.Error(t, err)
	code, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeWindowTooDense, code)
	require.Equal(t, MaxItems+1, visited)
}

func scheduleSource(t *testing.T, schedule binding.ScheduleSource, next time.Time) calendarstore.ScheduleSource {
	t.Helper()
	source, err := json.Marshal(binding.Source{Type: binding.SourceTypeSchedule, Schedule: &schedule})
	require.NoError(t, err)
	return calendarstore.ScheduleSource{
		ID: "wbs_1", BindingID: "wbd_1", BindingRevisionID: "wbr_1", Activation: 1,
		NextOccurrenceAt: next, PlanID: "pln_1", PlanVersionID: "plv_1", SourceJSON: source,
		BindingKey: "daily", BindingDisplayName: "Daily schedule",
		PlanKey: "daily-report", PlanDisplayName: "Daily report",
	}
}
