package schedule

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

type scheduleFixture struct {
	UserID        string
	WorkspaceID   string
	HumanMemberID string
	AgentMemberID string
	ScopeID       string
}

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

	ctx := context.Background()
	db := startScheduleRollbackDB(t, ctx)
	fixture := seedScheduleFixture(t, db, "definition_crud", time.Date(2026, 5, 30, 5, 30, 0, 0, time.UTC))
	store := New(db)

	scheduledAt := time.Date(2026, 5, 31, 9, 0, 0, 0, time.UTC)
	nextAt := scheduledAt
	createdAt := time.Date(2026, 5, 30, 5, 35, 0, 0, time.UTC)
	staleRelativeSeconds := int64(600)
	staleAnchorAt := time.Date(2026, 5, 30, 5, 0, 0, 0, time.UTC)
	staleIntervalSeconds := int64(120)
	created, reused, err := store.CreateDefinition(ctx, DefinitionCreate{
		ID:                     "sch_definition_crud",
		WorkspaceID:            fixture.WorkspaceID,
		OwnerMemberID:          fixture.HumanMemberID,
		PrincipalMemberID:      fixture.HumanMemberID,
		Name:                   " Morning reminder ",
		SourceType:             SourceTypeOneShotAbsolute,
		ScheduledAt:            &scheduledAt,
		RelativeSeconds:        &staleRelativeSeconds,
		AnchorAt:               &staleAnchorAt,
		IntervalSeconds:        &staleIntervalSeconds,
		CronExpression:         "* * * * *",
		Timezone:               "UTC",
		TargetKind:             TargetKindChannel,
		TargetRef:              fixture.ScopeID,
		DeliveryScopeID:        fixture.ScopeID,
		PermissionCheckScopeID: fixture.ScopeID,
		TargetSnapshot:         []byte(`{"kind":"channel"}`),
		MessageTemplate:        " remind the team ",
		NextOccurrenceAt:       &nextAt,
		IdempotencyKey:         "schedule:create:definition_crud",
		CreatedByMemberID:      fixture.HumanMemberID,
		CreatedByActorType:     "human",
		CreatedByActorID:       fixture.UserID,
		CreatedVia:             "api",
		Metadata:               []byte(`{"source":"test"}`),
		Now:                    createdAt,
	})
	require.NoError(t, err)
	require.False(t, reused)
	require.Equal(t, "sch_definition_crud", created.ID)
	require.Equal(t, DefinitionStatusActive, created.Status)
	require.Equal(t, SourceKindTime, created.SourceKind)
	require.Equal(t, NotificationModeNone, created.NotificationMode)
	require.Equal(t, "Morning reminder", created.Name)
	require.Equal(t, "remind the team", created.MessageTemplate)
	require.Equal(t, scheduledAt, *created.ScheduledAt)
	require.Nil(t, created.RelativeSeconds)
	require.Nil(t, created.AnchorAt)
	require.Nil(t, created.IntervalSeconds)
	require.Empty(t, created.CronExpression)
	require.Empty(t, created.Timezone)
	require.Equal(t, nextAt, *created.NextOccurrenceAt)
	require.JSONEq(t, `{"kind":"channel"}`, string(created.TargetSnapshot))

	reusedDefinition, reused, err := store.CreateDefinition(ctx, DefinitionCreate{
		WorkspaceID:            fixture.WorkspaceID,
		OwnerMemberID:          fixture.HumanMemberID,
		PrincipalMemberID:      fixture.HumanMemberID,
		Name:                   "duplicate create",
		SourceType:             SourceTypeOneShotAbsolute,
		ScheduledAt:            &scheduledAt,
		TargetKind:             TargetKindChannel,
		TargetRef:              fixture.ScopeID,
		DeliveryScopeID:        fixture.ScopeID,
		PermissionCheckScopeID: fixture.ScopeID,
		MessageTemplate:        "duplicate",
		IdempotencyKey:         "schedule:create:definition_crud",
		CreatedByMemberID:      fixture.HumanMemberID,
		CreatedByActorType:     "human",
		CreatedByActorID:       fixture.UserID,
		CreatedVia:             "api",
	})
	require.NoError(t, err)
	require.True(t, reused)
	require.Equal(t, created.ID, reusedDefinition.ID)
	require.Equal(t, created.MessageTemplate, reusedDefinition.MessageTemplate)

	listed, err := store.ListDefinitions(ctx, DefinitionListQuery{WorkspaceID: fixture.WorkspaceID, OwnerMemberID: fixture.HumanMemberID, Limit: 10})
	require.NoError(t, err)
	require.Len(t, listed, 1)
	require.Equal(t, created.ID, listed[0].ID)

	anchorAt := time.Date(2026, 5, 30, 6, 0, 0, 0, time.UTC)
	intervalSeconds := int64(3600)
	nextIntervalAt := time.Date(2026, 5, 30, 7, 0, 0, 0, time.UTC)
	updated, err := store.UpdateDefinition(ctx, DefinitionUpdate{
		WorkspaceID:            fixture.WorkspaceID,
		ID:                     created.ID,
		Name:                   "Hourly reminder",
		Status:                 DefinitionStatusActive,
		SourceType:             SourceTypeInterval,
		AnchorAt:               &anchorAt,
		IntervalSeconds:        &intervalSeconds,
		TargetKind:             TargetKindReplyThread,
		TargetRef:              "chn_definition_crud:thread",
		DeliveryScopeID:        fixture.ScopeID,
		PermissionCheckScopeID: fixture.ScopeID,
		MessageTemplate:        "hourly ping",
		NotificationMode:       NotificationModeNone,
		NextOccurrenceAt:       &nextIntervalAt,
		Metadata:               []byte(`{"updated":true}`),
		Now:                    time.Date(2026, 5, 30, 5, 40, 0, 0, time.UTC),
	})
	require.NoError(t, err)
	require.Equal(t, SourceTypeInterval, updated.SourceType)
	require.Nil(t, updated.ScheduledAt)
	require.Equal(t, intervalSeconds, *updated.IntervalSeconds)
	require.Equal(t, nextIntervalAt, *updated.NextOccurrenceAt)
	require.Equal(t, TargetKindReplyThread, updated.TargetKind)
	require.JSONEq(t, `{"updated":true}`, string(updated.Metadata))

	pausedAgain, err := store.SetDefinitionStatus(ctx, DefinitionStatusUpdate{
		WorkspaceID: fixture.WorkspaceID,
		ID:          created.ID,
		Status:      DefinitionStatusPaused,
		Reason:      "user_request",
		Now:         time.Date(2026, 5, 30, 5, 45, 0, 0, time.UTC),
	})
	require.NoError(t, err)
	require.Equal(t, DefinitionStatusPaused, pausedAgain.Status)
	require.Equal(t, "user_request", pausedAgain.PausedReason)
	require.NotNil(t, pausedAgain.PausedAt)

	pausedUpdate, err := store.UpdateDefinition(ctx, DefinitionUpdate{
		WorkspaceID:            fixture.WorkspaceID,
		ID:                     created.ID,
		Name:                   "Paused edit",
		SourceType:             SourceTypeInterval,
		AnchorAt:               &anchorAt,
		IntervalSeconds:        &intervalSeconds,
		TargetKind:             TargetKindReplyThread,
		TargetRef:              "chn_definition_crud:thread",
		DeliveryScopeID:        fixture.ScopeID,
		PermissionCheckScopeID: fixture.ScopeID,
		MessageTemplate:        "paused edit",
		NotificationMode:       NotificationModeNone,
		NextOccurrenceAt:       &nextIntervalAt,
		Now:                    time.Date(2026, 5, 30, 5, 46, 0, 0, time.UTC),
	})
	require.NoError(t, err)
	require.Equal(t, DefinitionStatusPaused, pausedUpdate.Status)
	require.Equal(t, "user_request", pausedUpdate.PausedReason)
	require.NotNil(t, pausedUpdate.PausedAt)

	_, err = store.UpdateDefinition(ctx, DefinitionUpdate{
		WorkspaceID:            fixture.WorkspaceID,
		ID:                     created.ID,
		Name:                   "Invalid status edit",
		Status:                 DefinitionStatusActive,
		SourceType:             SourceTypeInterval,
		AnchorAt:               &anchorAt,
		IntervalSeconds:        &intervalSeconds,
		TargetKind:             TargetKindReplyThread,
		TargetRef:              "chn_definition_crud:thread",
		DeliveryScopeID:        fixture.ScopeID,
		PermissionCheckScopeID: fixture.ScopeID,
		MessageTemplate:        "invalid status edit",
		NotificationMode:       NotificationModeNone,
		NextOccurrenceAt:       &nextIntervalAt,
		Now:                    time.Date(2026, 5, 30, 5, 47, 0, 0, time.UTC),
	})
	require.ErrorIs(t, err, ErrBadRequest)

	active, err := store.SetDefinitionStatus(ctx, DefinitionStatusUpdate{
		WorkspaceID: fixture.WorkspaceID,
		ID:          created.ID,
		Status:      DefinitionStatusActive,
		Now:         time.Date(2026, 5, 30, 5, 48, 0, 0, time.UTC),
	})
	require.NoError(t, err)
	require.Equal(t, DefinitionStatusActive, active.Status)
	require.Empty(t, active.PausedReason)
	require.Nil(t, active.PausedAt)

	paused, err := store.SetDefinitionStatus(ctx, DefinitionStatusUpdate{
		WorkspaceID: fixture.WorkspaceID,
		ID:          created.ID,
		Status:      DefinitionStatusPaused,
		Reason:      "user_request",
		Now:         time.Date(2026, 5, 30, 5, 45, 0, 0, time.UTC),
	})
	require.NoError(t, err)
	require.Equal(t, DefinitionStatusPaused, paused.Status)
	require.Equal(t, "user_request", paused.PausedReason)
	require.NotNil(t, paused.PausedAt)

	deleted, err := store.DeleteDefinition(ctx, fixture.WorkspaceID, created.ID, time.Date(2026, 5, 30, 5, 50, 0, 0, time.UTC))
	require.NoError(t, err)
	require.NotNil(t, deleted.DeletedAt)

	_, err = store.LoadDefinition(ctx, fixture.WorkspaceID, created.ID)
	require.ErrorIs(t, err, ErrNotFound)
	visible, err := store.ListDefinitions(ctx, DefinitionListQuery{WorkspaceID: fixture.WorkspaceID})
	require.NoError(t, err)
	require.Empty(t, visible)
	withDeleted, err := store.ListDefinitions(ctx, DefinitionListQuery{WorkspaceID: fixture.WorkspaceID, IncludeDeleted: true})
	require.NoError(t, err)
	require.Len(t, withDeleted, 1)
	require.NotNil(t, withDeleted[0].DeletedAt)
}

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

	ctx := context.Background()
	db := startScheduleRollbackDB(t, ctx)
	createdAt := time.Date(2026, 5, 30, 5, 35, 0, 0, time.UTC)
	fixture := seedScheduleFixture(t, db, "definition_scope", createdAt)
	threadScopeID := "thr_schedule_definition_scope"
	mustExec(t, db, `
INSERT INTO scopes (id, workspace_id, type, identity_key, parent_scope_id, status, created_by_member_id, created_at, updated_at)
VALUES ($1, $2, 'thread', $3, $4, 'active', $5, $6, $6)
`, threadScopeID, fixture.WorkspaceID, "thread:"+threadScopeID, fixture.ScopeID, fixture.HumanMemberID, createdAt)

	store := New(db)
	scheduledAt := createdAt.Add(24 * time.Hour)
	for _, definition := range []DefinitionCreate{
		{
			ID:                     "sch_definition_channel",
			Name:                   "Channel reminder",
			TargetKind:             TargetKindChannel,
			TargetRef:              fixture.ScopeID,
			DeliveryScopeID:        fixture.ScopeID,
			PermissionCheckScopeID: fixture.ScopeID,
			MessageTemplate:        "channel reminder",
			IdempotencyKey:         "schedule:create:definition_channel",
		},
		{
			ID:                     "sch_definition_thread",
			Name:                   "Thread reminder",
			TargetKind:             TargetKindReplyThread,
			TargetRef:              threadScopeID,
			DeliveryScopeID:        threadScopeID,
			PermissionCheckScopeID: fixture.ScopeID,
			MessageTemplate:        "thread reminder",
			IdempotencyKey:         "schedule:create:definition_thread",
		},
	} {
		definition.WorkspaceID = fixture.WorkspaceID
		definition.OwnerMemberID = fixture.HumanMemberID
		definition.PrincipalMemberID = fixture.HumanMemberID
		definition.SourceType = SourceTypeOneShotAbsolute
		definition.ScheduledAt = &scheduledAt
		definition.CreatedByMemberID = fixture.HumanMemberID
		definition.CreatedByActorType = "human"
		definition.CreatedByActorID = fixture.UserID
		definition.CreatedVia = "api"
		definition.Now = createdAt
		_, _, err := store.CreateDefinition(ctx, definition)
		require.NoError(t, err)
	}

	threadOnly, err := store.ListDefinitions(ctx, DefinitionListQuery{
		WorkspaceID:   fixture.WorkspaceID,
		OwnerMemberID: fixture.HumanMemberID,
		TargetKind:    TargetKindReplyThread,
		TargetRef:     threadScopeID,
		Limit:         10,
	})
	require.NoError(t, err)
	require.Len(t, threadOnly, 1)
	require.Equal(t, "sch_definition_thread", threadOnly[0].ID)

	channelOnly, err := store.ListDefinitions(ctx, DefinitionListQuery{
		WorkspaceID:   fixture.WorkspaceID,
		OwnerMemberID: fixture.HumanMemberID,
		TargetKind:    TargetKindChannel,
		TargetRef:     fixture.ScopeID,
		Limit:         10,
	})
	require.NoError(t, err)
	require.Len(t, channelOnly, 1)
	require.Equal(t, "sch_definition_channel", channelOnly[0].ID)

	channelWithThreads, err := store.ListDefinitions(ctx, DefinitionListQuery{
		WorkspaceID:         fixture.WorkspaceID,
		OwnerMemberID:       fixture.HumanMemberID,
		TargetKind:          TargetKindChannel,
		TargetRef:           fixture.ScopeID,
		IncludeChildThreads: true,
		Limit:               10,
	})
	require.NoError(t, err)
	require.Len(t, channelWithThreads, 2)
}

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

	ctx := context.Background()
	db := startScheduleRollbackDB(t, ctx)
	fixture := seedScheduleFixture(t, db, "definition_validation", time.Date(2026, 5, 30, 6, 0, 0, 0, time.UTC))
	store := New(db)

	_, _, err := store.CreateDefinition(ctx, DefinitionCreate{
		WorkspaceID:            fixture.WorkspaceID,
		OwnerMemberID:          fixture.HumanMemberID,
		PrincipalMemberID:      fixture.HumanMemberID,
		Name:                   "bad cron",
		SourceType:             SourceTypeCron,
		TargetKind:             TargetKindChannel,
		TargetRef:              "chn_definition_validation",
		DeliveryScopeID:        fixture.ScopeID,
		PermissionCheckScopeID: fixture.ScopeID,
		MessageTemplate:        "ping",
		CreatedByMemberID:      fixture.HumanMemberID,
		CreatedByActorType:     "human",
		CreatedByActorID:       fixture.UserID,
		CreatedVia:             "api",
	})
	require.ErrorIs(t, err, ErrBadRequest)
	require.Contains(t, err.Error(), "cron")

	relativeSeconds := int64(0)
	_, _, err = store.CreateDefinition(ctx, DefinitionCreate{
		WorkspaceID:            fixture.WorkspaceID,
		OwnerMemberID:          fixture.HumanMemberID,
		PrincipalMemberID:      fixture.HumanMemberID,
		Name:                   "bad relative",
		SourceType:             SourceTypeOneShotRelative,
		RelativeSeconds:        &relativeSeconds,
		TargetKind:             TargetKindChannel,
		TargetRef:              "chn_definition_validation",
		DeliveryScopeID:        fixture.ScopeID,
		PermissionCheckScopeID: fixture.ScopeID,
		MessageTemplate:        "ping",
		CreatedByMemberID:      fixture.HumanMemberID,
		CreatedByActorType:     "human",
		CreatedByActorID:       fixture.UserID,
		CreatedVia:             "api",
	})
	require.ErrorIs(t, err, ErrBadRequest)

	_, err = store.LoadDefinition(ctx, fixture.WorkspaceID, "missing_schedule")
	require.True(t, errors.Is(err, ErrNotFound))
}

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

	now := time.Date(2026, 5, 30, 6, 30, 0, 0, time.UTC)
	scheduledAt := now.Add(time.Hour)
	base := DefinitionCreate{
		WorkspaceID:            "wsp_target_fields",
		OwnerMemberID:          "mem_owner",
		PrincipalMemberID:      "mem_owner",
		Name:                   "Target fields",
		SourceType:             SourceTypeOneShotAbsolute,
		ScheduledAt:            &scheduledAt,
		TargetKind:             TargetKindChannel,
		TargetRef:              "chn_target_fields",
		DeliveryScopeID:        "scp_target_fields",
		PermissionCheckScopeID: "scp_target_fields",
		MessageTemplate:        "ping",
		CreatedByMemberID:      "mem_owner",
		CreatedByActorType:     "human",
		CreatedByActorID:       "usr_owner",
		CreatedVia:             "api",
		Now:                    now,
	}

	missingMessageFields := base
	missingMessageFields.MessageTemplate = ""
	_, err := normalizeDefinitionCreate(missingMessageFields)
	require.ErrorIs(t, err, ErrBadRequest)
	require.Contains(t, err.Error(), "message targets require delivery scope")

	invalidTargetKind := base
	invalidTargetKind.TargetKind = "automation"
	invalidTargetKind.TargetRef = "ags:pingcap/agentteam:.agentteam/nightly.yml"
	_, err = normalizeDefinitionCreate(invalidTargetKind)
	require.ErrorIs(t, err, ErrBadRequest)
	require.Contains(t, err.Error(), "invalid schedule target kind")
}

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

	ctx := context.Background()
	db := startScheduleRollbackDB(t, ctx)
	fixture := seedScheduleFixture(t, db, "owner_with_role", time.Date(2026, 5, 30, 8, 0, 0, 0, time.UTC))
	store := New(db)

	owner, err := store.LoadHumanOwnerWithWorkspaceRole(ctx, fixture.WorkspaceID, fixture.HumanMemberID)
	require.NoError(t, err)

	require.Equal(t, fixture.UserID, owner.User.ID)
	require.Equal(t, "owner", owner.WorkspaceRole)
}

func seedScheduleFixture(t testing.TB, db coredb.QueryExecer, suffix string, now time.Time) scheduleFixture {
	t.Helper()
	fixture := scheduleFixture{
		UserID:        "usr_schedule_" + suffix,
		WorkspaceID:   "wsp_schedule_" + suffix,
		HumanMemberID: "mem_schedule_human_" + suffix,
		AgentMemberID: "mem_schedule_agent_" + suffix,
		ScopeID:       "scp_schedule_" + suffix,
	}
	orgID := "org_schedule_" + suffix
	mustExec(t, db, `
INSERT INTO users (id, email, name, display_name, password_hash, created_at, updated_at)
VALUES ($1, $2, $3, $3, 'hash', $4, $4)
`, fixture.UserID, suffix+"@example.com", suffix, now)
	mustExec(t, db, `
INSERT INTO organizations (id, slug, name, created_by_user_id, created_at, updated_at)
VALUES ($1, $2, $2, $3, $4, $4)
`, orgID, suffix, fixture.UserID, now)
	mustExec(t, db, `
INSERT INTO workspaces (id, organization_id, slug, name, created_at, updated_at)
VALUES ($1, $2, $3, $3, $4, $4)
`, fixture.WorkspaceID, orgID, suffix, now)
	mustExec(t, db, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'Human', $3, 'owner', 'active', $4, $4)
`, fixture.HumanMemberID, fixture.WorkspaceID, fixture.UserID, now)
	mustExec(t, db, `
INSERT INTO members (id, workspace_id, type, display_name, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'agent', 'Agent', NULL, 'active', $3, $3)
`, fixture.AgentMemberID, fixture.WorkspaceID, now)
	mustExec(t, db, `
INSERT INTO scopes (id, workspace_id, type, identity_key, status, created_by_member_id, created_at, updated_at)
VALUES ($1, $2, 'channel', $3, 'active', $4, $5, $5)
`, fixture.ScopeID, fixture.WorkspaceID, "channel:"+fixture.ScopeID, fixture.HumanMemberID, now)
	return fixture
}

func seedWorkspacePlacement(t testing.TB, db coredb.QueryExecer, fixture scheduleFixture, groupID string, state string, now time.Time) {
	t.Helper()
	mustExec(t, db, `
INSERT IGNORE INTO workspace_groups (workspace_group_id, display_name, state, resource_profile, service_ref, created_at, updated_at)
VALUES ($1, $1, 'active', '', $1, $2, $2)
`, groupID, now)
	mustExec(t, db, `
INSERT INTO workspace_placements (workspace_id, workspace_group_id, state, created_at, updated_at)
VALUES ($1, $2, $3, $4, $4)
`, fixture.WorkspaceID, groupID, state, now)
}
