package manage

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/runeffect"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/schedule"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	schedulestore "github.com/tidbcloud/AgentTeam/internal/store/schedule"
	"github.com/tidbcloud/AgentTeam/internal/store/testkit"
)

func newTriggerTestStore(t *testing.T) (context.Context, coredb.DB, *testkit.Store) {
	t.Helper()

	ctx := context.Background()
	db := startTriggerRollbackDB(t, ctx)
	return ctx, db, testkit.New(db, nil, 30)
}

func mustExec(t *testing.T, db coredb.QueryExecer, query string, args ...any) {
	t.Helper()
	_, err := db.Exec(context.Background(), query, args...)
	require.NoError(t, err)
}

type scheduleMessagePort interface {
	SendAppMessageForUserWithOptions(ctx context.Context, user model.User, workspaceID string, targetRef string, content string, attachmentIDs []string, options app.SendAppMessageOptions) (message.AppMessage, message.MessageDelivery, error)
}

type testScheduleEffectWriter struct {
	port       scheduleMessagePort
	owner      model.User
	afterWrite func(context.Context, schedule.EffectRequest, string) error
}

func (writer testScheduleEffectWriter) WriteScheduleEffect(ctx context.Context, request schedule.EffectRequest) (runeffect.Effect, error) {
	_, delivery, err := writer.port.SendAppMessageForUserWithOptions(ctx, writer.owner, request.WorkspaceID, request.TargetRef, request.Content, nil, app.SendAppMessageOptions{
		Source:           request.Source,
		ClientMessageID:  request.ClientMessageID,
		StartAgentThread: request.AlsoStartThread,
	})
	if err != nil {
		return runeffect.Effect{}, err
	}
	if writer.afterWrite != nil {
		if err := writer.afterWrite(ctx, request, delivery.MessageID); err != nil {
			return runeffect.Effect{}, err
		}
	}
	return runeffect.MessageEffect(delivery.MessageID), nil
}

type ownedChannelFixture struct {
	User        model.User
	WorkspaceID string
	MemberID    string
	ChannelID   string
}

func seedOwnedChannelFixture(t *testing.T, db coredb.QueryExecer, now time.Time, prefix string) ownedChannelFixture {
	t.Helper()

	fixture := ownedChannelFixture{
		User: model.User{
			ID:          "usr_" + prefix,
			Email:       prefix + "@example.com",
			Name:        prefix,
			DisplayName: prefix + " display",
		},
		WorkspaceID: "wsp_" + prefix,
		MemberID:    "mem_" + prefix,
		ChannelID:   "chn_" + prefix,
	}
	organizationID := "org_" + prefix

	mustExec(t, db, `
INSERT INTO users (id, email, name, display_name, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'hash', $5, $5, $5)
`, fixture.User.ID, fixture.User.Email, fixture.User.Name, fixture.User.DisplayName, 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)
`, organizationID, fixture.WorkspaceID, fixture.User.ID, now)
	mustExec(t, db, `
INSERT INTO workspaces (id, organization_id, name, slug, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $5)
`, fixture.WorkspaceID, organizationID, fixture.WorkspaceID, fixture.WorkspaceID, now)
	mustExec(t, db, `
INSERT INTO members (id, workspace_id, user_id, type, display_name, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, $3, 'human', $4, 'admin', 'active', $5, $5)
`, fixture.MemberID, fixture.WorkspaceID, fixture.User.ID, fixture.User.DisplayName, 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.ChannelID, fixture.WorkspaceID, "channel:"+fixture.ChannelID, fixture.MemberID, now)
	mustExec(t, db, `
INSERT INTO scope_channel_profiles (scope_id, workspace_id, slug, display_name, description, visibility, owner_member_id, created_at, updated_at)
VALUES ($1, $2, $3, $3, '', 'public', $4, $5, $5)
`, fixture.ChannelID, fixture.WorkspaceID, prefix, fixture.MemberID, now)
	mustExec(t, db, `
INSERT INTO scope_memberships (scope_id, member_id, visibility, last_read_seq, created_at, updated_at)
VALUES ($1, $2, 'public', 0, $3, $3)
`, fixture.ChannelID, fixture.MemberID, now)

	fixture.User.CreatedAt = now
	fixture.User.UpdatedAt = now
	fixture.User.EmailVerifiedAt = &now
	return fixture
}

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

	ctx, db, dataStore := newTriggerTestStore(t)
	fixture := seedOwnedChannelFixture(t, db, time.Now().UTC(), "trigger_create_relative_wall_clock")
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) },
	})

	relativeSeconds := int64(90)
	before := time.Now().UTC()
	created, err := service.CreateForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, UpsertInput{
		Name: "Relative reminder",
		Source: SourceInput{
			SourceKind:      SourceKindTime,
			TimeMode:        TimeModeOneShotRelative,
			RelativeSeconds: &relativeSeconds,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "Check deployment status.",
	})
	after := time.Now().UTC()
	require.NoError(t, err)
	require.NotNil(t, created.ScheduledAt)
	require.NotNil(t, created.NextOccurrenceAt)
	require.WithinDuration(t, *created.ScheduledAt, *created.NextOccurrenceAt, time.Second)
	require.True(t, created.ScheduledAt.After(before.Add(80*time.Second)), "scheduledAt=%s before=%s", created.ScheduledAt, before)
	require.True(t, created.ScheduledAt.Before(after.Add(100*time.Second)), "scheduledAt=%s after=%s", created.ScheduledAt, after)
	require.NotEqual(t, time.Date(2000, 1, 1, 0, 1, 30, 0, time.UTC), *created.ScheduledAt)

	var storedScheduledAt time.Time
	var storedNextOccurrenceAt time.Time
	require.NoError(t, db.QueryRow(ctx, `
SELECT scheduled_at, next_occurrence_at
FROM schedule_definitions
WHERE id = $1
`, created.ID).Scan(&storedScheduledAt, &storedNextOccurrenceAt))
	require.WithinDuration(t, storedScheduledAt.UTC(), *created.ScheduledAt, time.Second)
	require.WithinDuration(t, storedNextOccurrenceAt.UTC(), *created.NextOccurrenceAt, time.Second)
}

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

	ctx, db, _ := newTriggerTestStore(t)
	now := time.Date(2026, 5, 30, 9, 30, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_message_template_required")
	service := New(nil, db, nil, ServiceOptions{Clock: func() time.Time { return now }})

	scheduledAt := now.Add(10 * time.Minute)
	_, err := service.CreateScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, UpsertInput{
		Name: "Missing message",
		Source: SourceInput{
			SourceKind:  SourceKindTime,
			TimeMode:    TimeModeOneShotAbsolute,
			ScheduledAt: &scheduledAt,
		},
		Target: TargetInput{Kind: TargetKindChannel, Ref: fixture.ChannelID},
	})
	require.EqualError(t, err, "messageTemplate is required")
}

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

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 4, 18, 12, 0, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "trigger_phone_only_owner")
	mustExec(t, db, `
UPDATE users
SET email = NULL,
    mobile = '+8613800138000',
    mobile_verified_at = $2
WHERE id = $1
`, fixture.User.ID, now)

	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return now },
	})

	relativeSeconds := int64(90)
	created, err := service.CreateForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, UpsertInput{
		Name: "Phone-only reminder",
		Source: SourceInput{
			SourceKind:      SourceKindTime,
			TimeMode:        TimeModeOneShotRelative,
			RelativeSeconds: &relativeSeconds,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "phone-only owner reminder",
	})
	require.NoError(t, err)
	require.NotEmpty(t, created.ID)

	mustExec(t, db, `
UPDATE schedule_definitions
SET next_occurrence_at = $2
WHERE id = $1
`, created.ID, now.Add(-time.Second))

	scheduleService := schedule.New(nil, schedulestore.New(db), schedule.ServiceOptions{
		Clock:  func() time.Time { return now },
		Writer: testScheduleEffectWriter{port: dataStore.App(), owner: fixture.User},
	})
	result, err := scheduleService.RunOnce(ctx)
	require.NoError(t, err)
	require.Equal(t, 1, result.Due)

	page, err := dataStore.App().ListMessagesForUser(ctx, fixture.User.ID, fixture.WorkspaceID, fixture.ChannelID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, page.Messages, 1)
	require.Equal(t, "phone-only owner reminder", page.Messages[0].Content)
	require.Equal(t, message.ContributionSourceKindScheduleRun, page.Messages[0].Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeAutomationRun, page.Messages[0].Source.RefType)
}

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

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 4, 18, 12, 30, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_source_before_completion")
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return now },
	})

	relativeSeconds := int64(90)
	created, err := service.CreateForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, UpsertInput{
		Name: "Immediate source reminder",
		Source: SourceInput{
			SourceKind:      SourceKindTime,
			TimeMode:        TimeModeOneShotRelative,
			RelativeSeconds: &relativeSeconds,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "source must not flicker",
	})
	require.NoError(t, err)

	mustExec(t, db, `
UPDATE schedule_definitions
SET next_occurrence_at = $2
WHERE id = $1
`, created.ID, now.Add(-time.Second))

	inspectedBeforeCompletion := false
	scheduleService := schedule.New(nil, schedulestore.New(db), schedule.ServiceOptions{
		Clock: func() time.Time { return now },
		Writer: testScheduleEffectWriter{
			port:  dataStore.App(),
			owner: fixture.User,
			afterWrite: func(ctx context.Context, request schedule.EffectRequest, messageID string) error {
				inspectedBeforeCompletion = true
				run, err := schedulestore.New(db).LoadAutomationRun(ctx, fixture.WorkspaceID, request.Source.RunID)
				require.NoError(t, err)
				require.Equal(t, string(runeffect.StatusRunning), run.Status)
				require.Equal(t, messageID, run.MessageID)

				page, err := dataStore.App().ListMessagesForUser(ctx, fixture.User.ID, fixture.WorkspaceID, fixture.ChannelID, 20, nil, nil)
				require.NoError(t, err)
				require.Len(t, page.Messages, 1)
				require.Equal(t, messageID, page.Messages[0].ID)
				require.Equal(t, message.ContributionSourceKindScheduleRun, page.Messages[0].Source.Kind)
				require.Equal(t, message.ContributionSourceRefTypeAutomationRun, page.Messages[0].Source.RefType)
				require.Equal(t, request.Source.RunID, page.Messages[0].Source.RunID)
				return nil
			},
		},
	})
	result, err := scheduleService.RunOnce(ctx)
	require.NoError(t, err)
	require.Equal(t, 1, result.Due)
	require.True(t, inspectedBeforeCompletion)
}

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

	ctx, db, dataStore := newTriggerTestStore(t)
	now := time.Date(2026, 4, 18, 13, 0, 0, 0, time.UTC)
	fixture := seedOwnedChannelFixture(t, db, now, "schedule_also_start_thread")
	service := New(nil, db, dataStore.App(), ServiceOptions{
		Clock: func() time.Time { return now },
	})

	relativeSeconds := int64(60)
	created, err := service.CreateForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, UpsertInput{
		Name: "Threaded channel reminder",
		Source: SourceInput{
			SourceKind:      SourceKindTime,
			TimeMode:        TimeModeOneShotRelative,
			RelativeSeconds: &relativeSeconds,
		},
		Target: TargetInput{
			Kind: TargetKindChannel,
			Ref:  fixture.ChannelID,
		},
		MessageTemplate: "open a schedule thread",
		AlsoStartThread: true,
	})
	require.NoError(t, err)
	require.True(t, created.AlsoStartThread)

	loaded, err := service.GetScheduleForOwner(ctx, fixture.User.ID, fixture.WorkspaceID, created.ID)
	require.NoError(t, err)
	require.True(t, loaded.AlsoStartThread)

	mustExec(t, db, `
UPDATE schedule_definitions
SET next_occurrence_at = $2
WHERE id = $1
`, created.ID, now.Add(-time.Second))

	scheduleService := schedule.New(nil, schedulestore.New(db), schedule.ServiceOptions{
		Clock:  func() time.Time { return now },
		Writer: testScheduleEffectWriter{port: dataStore.App(), owner: fixture.User},
	})
	result, err := scheduleService.RunOnce(ctx)
	require.NoError(t, err)
	require.Equal(t, 1, result.Due)

	page, err := dataStore.App().ListMessagesForUser(ctx, fixture.User.ID, fixture.WorkspaceID, fixture.ChannelID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, page.Messages, 1)
	require.Equal(t, "open a schedule thread", page.Messages[0].Content)

	var threadChannelID string
	require.NoError(t, db.QueryRow(ctx, `
SELECT id
FROM scopes
WHERE workspace_id = $1
  AND type = 'thread'
  AND parent_scope_id = $2
  AND root_message_id = $3
`, fixture.WorkspaceID, fixture.ChannelID, page.Messages[0].ID).Scan(&threadChannelID))
	require.NotEmpty(t, threadChannelID)

	var threadMessageCount int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, threadChannelID).Scan(&threadMessageCount))
	require.Zero(t, threadMessageCount)
}
