package manage

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

func TestCreateScheduleForOwnerReusesMatchingIdempotencyKey(t *testing.T) {
	t.Parallel()

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_create_idempotency")
	current := now
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return current },
	})

	scheduledAt := now.Add(2 * time.Hour)
	input := UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:  SourceKindTime,
			TimeMode:    TimeModeOneShotAbsolute,
			ScheduledAt: &scheduledAt,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "Status update?",
		IdempotencyKey:  "create:schedule_create_idempotency",
	}

	first, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, input)
	require.NoError(t, err)

	current = current.Add(time.Minute)
	replayed, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, input)
	require.NoError(t, err)

	require.Equal(t, first.ID, replayed.ID)
	require.Equal(t, first.UpdatedAt, replayed.UpdatedAt)

	var definitions int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM schedule_definitions
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&definitions))
	require.Equal(t, 1, definitions)
}

func TestCreateScheduleForOwnerRejectsConflictingIdempotencyKeyReplay(t *testing.T) {
	t.Parallel()

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_create_idempotency_conflict")
	current := now
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return current },
	})

	scheduledAt := now.Add(3 * time.Hour)
	input := UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:  SourceKindTime,
			TimeMode:    TimeModeOneShotAbsolute,
			ScheduledAt: &scheduledAt,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "Status update?",
		IdempotencyKey:  "create:schedule_create_idempotency_conflict",
	}

	_, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, input)
	require.NoError(t, err)

	current = current.Add(time.Minute)
	input.MessageTemplate = "Different payload"
	_, err = service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, input)
	require.ErrorIs(t, err, storeerr.ErrConflict)
	require.Contains(t, err.Error(), "idempotency key")
}

func TestCreateScheduleForOwnerReusesMatchingRelativeOneShotIdempotencyKey(t *testing.T) {
	t.Parallel()

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 6, 1, 10, 30, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_create_relative_idempotency")
	current := now
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return current },
	})

	relativeSeconds := int64(90)
	input := UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:      SourceKindTime,
			TimeMode:        TimeModeOneShotRelative,
			RelativeSeconds: &relativeSeconds,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "Status update?",
		IdempotencyKey:  "create:schedule_create_relative_idempotency",
	}

	first, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, input)
	require.NoError(t, err)
	require.Equal(t, TimeModeOneShotRelative, first.TimeMode)
	require.NotNil(t, first.ScheduledAt)

	current = current.Add(time.Minute)
	replayed, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, input)
	require.NoError(t, err)

	require.Equal(t, first.ID, replayed.ID)
	require.Equal(t, first.UpdatedAt, replayed.UpdatedAt)
	require.NotNil(t, replayed.ScheduledAt)
	require.WithinDuration(t, *first.ScheduledAt, *replayed.ScheduledAt, time.Second)
	require.Equal(t, 1, countScheduleDefinitions(t, ctx, db, fixture.WorkspaceID))
}

func TestUpdateScheduleForOwnerSupportsReplayAndPreconditions(t *testing.T) {
	t.Parallel()

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 6, 1, 11, 0, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_update_idempotency")
	current := now
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return current },
	})

	scheduledAt := now.Add(4 * time.Hour)
	created, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:  SourceKindTime,
			TimeMode:    TimeModeOneShotAbsolute,
			ScheduledAt: &scheduledAt,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "Status update?",
	})
	require.NoError(t, err)

	originalUpdatedAt := created.UpdatedAt
	updateInput := UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:  SourceKindTime,
			TimeMode:    TimeModeOneShotAbsolute,
			ScheduledAt: &scheduledAt,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate:   "Updated status update?",
		IdempotencyKey:    "update:schedule_update_idempotency",
		ExpectedUpdatedAt: &originalUpdatedAt,
	}

	current = current.Add(2 * time.Minute)
	updated, err := service.UpdateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID, updateInput)
	require.NoError(t, err)
	require.Equal(t, "Updated status update?", updated.MessageTemplate)

	current = current.Add(2 * time.Minute)
	replayed, err := service.UpdateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID, updateInput)
	require.NoError(t, err)
	require.Equal(t, updated.ID, replayed.ID)
	require.Equal(t, updated.UpdatedAt, replayed.UpdatedAt)

	current = current.Add(2 * time.Minute)
	conflicting := updateInput
	conflicting.MessageTemplate = "Conflicting update"
	_, err = service.UpdateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID, conflicting)
	require.ErrorIs(t, err, storeerr.ErrConflict)
	require.Contains(t, err.Error(), "idempotency key")

	current = current.Add(2 * time.Minute)
	staleExpectedAt := originalUpdatedAt
	_, err = service.UpdateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID, UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:  SourceKindTime,
			TimeMode:    TimeModeOneShotAbsolute,
			ScheduledAt: &scheduledAt,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate:   "Another update",
		IdempotencyKey:    "update:schedule_update_idempotency_stale",
		ExpectedUpdatedAt: &staleExpectedAt,
	})
	require.ErrorIs(t, err, storeerr.ErrConflict)
	require.Contains(t, err.Error(), "expectedUpdatedAt")
}

func TestUpdateScheduleForOwnerRelativeOneShotSupportsReplayAndPreconditions(t *testing.T) {
	t.Parallel()

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 6, 1, 11, 30, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_update_relative_idempotency")
	current := now
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return current },
	})

	scheduledAt := now.Add(4 * time.Hour)
	created, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:  SourceKindTime,
			TimeMode:    TimeModeOneShotAbsolute,
			ScheduledAt: &scheduledAt,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "Status update?",
	})
	require.NoError(t, err)

	originalUpdatedAt := created.UpdatedAt
	relativeSeconds := int64(120)
	updateInput := UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:      SourceKindTime,
			TimeMode:        TimeModeOneShotRelative,
			RelativeSeconds: &relativeSeconds,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate:   "Updated status update?",
		IdempotencyKey:    "update:schedule_update_relative_idempotency",
		ExpectedUpdatedAt: &originalUpdatedAt,
	}

	current = current.Add(2 * time.Minute)
	updated, err := service.UpdateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID, updateInput)
	require.NoError(t, err)
	require.Equal(t, TimeModeOneShotRelative, updated.TimeMode)
	require.NotNil(t, updated.ScheduledAt)

	current = current.Add(2 * time.Minute)
	replayed, err := service.UpdateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID, updateInput)
	require.NoError(t, err)
	require.Equal(t, updated.ID, replayed.ID)
	require.Equal(t, updated.UpdatedAt, replayed.UpdatedAt)
	require.NotNil(t, replayed.ScheduledAt)
	require.WithinDuration(t, *updated.ScheduledAt, *replayed.ScheduledAt, time.Second)

	current = current.Add(2 * time.Minute)
	staleExpectedAt := originalUpdatedAt
	_, err = service.UpdateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID, UpsertInput{
		Name: "Friday follow-up",
		Source: SourceInput{
			SourceKind:      SourceKindTime,
			TimeMode:        TimeModeOneShotRelative,
			RelativeSeconds: &relativeSeconds,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate:   "Another update",
		IdempotencyKey:    "update:schedule_update_relative_idempotency_stale",
		ExpectedUpdatedAt: &staleExpectedAt,
	})
	require.ErrorIs(t, err, storeerr.ErrConflict)
	require.Contains(t, err.Error(), "expectedUpdatedAt")
}

func TestScheduleMutationFingerprintIncludesMentionMetadata(t *testing.T) {
	t.Parallel()

	record := definitionRecord{
		Name:            "Team follow-up",
		SourceKind:      SourceKindTime,
		TimeMode:        TimeModeOneShotAbsolute,
		TargetKind:      TargetKindChannel,
		TargetRef:       "chn_1",
		MessageTemplate: "hello @core",
		MentionHints: []message.MessageMentionHint{{
			Type:     "team",
			ID:       "team_core",
			Token:    "core",
			Metadata: map[string]any{"source": "composer"},
		}},
	}

	first, err := scheduleMutationFingerprint(record)
	require.NoError(t, err)

	record.MentionHints[0].Metadata = map[string]any{"source": "scheduler"}
	second, err := scheduleMutationFingerprint(record)
	require.NoError(t, err)

	require.NotEqual(t, first, second)
}

func countScheduleDefinitions(t *testing.T, ctx context.Context, db coredb.QueryRower, workspaceID string) int {
	t.Helper()

	var count int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM schedule_definitions
WHERE workspace_id = $1
`, workspaceID).Scan(&count))
	return count
}
