package onboarding

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

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

	ctx := context.Background()
	db := startOnboardingRollbackDB(t, ctx)
	fixture := seedOnboardingConversation(t, ctx, db, "reply_first")
	store := New(db)

	require.NoError(t, store.RecordVisibleReply(ctx, fixture.evidence))
	state, err := store.GetForUser(ctx, fixture.userID, fixture.workspaceID)
	require.NoError(t, err)
	require.Equal(t, fixture.replyMessageID, state.Progress.ReplyMessageID)
	require.Nil(t, state.CompletedAt)
	channels, err := message.ListChannelsForMember(ctx, db, fixture.ownerMemberID, fixture.workspaceID)
	require.NoError(t, err)
	require.Len(t, channels, 1)
	require.Equal(t, message.ChannelSubsystemTutorial, channels[0].Subsystem)
	require.True(t, channels[0].Tutorial)

	state, err = store.AcknowledgeVisibleReply(ctx, fixture.userID, fixture.workspaceID, fixture.channelID, fixture.replyMessageID)
	require.NoError(t, err)
	require.Nil(t, state.CompletedAt)

	markOnboardingReplyRead(t, ctx, db, fixture, 2)
	state, err = store.AcknowledgeVisibleReply(ctx, fixture.userID, fixture.workspaceID, fixture.channelID, fixture.replyMessageID)
	require.NoError(t, err)
	require.NotNil(t, state.CompletedAt)
	require.EqualValues(t, 2, state.Progress.VisibleThroughSeq)
	channels, err = message.ListChannelsForMember(ctx, db, fixture.ownerMemberID, fixture.workspaceID)
	require.NoError(t, err)
	require.Len(t, channels, 1)
	require.Equal(t, message.ChannelSubsystemIM, channels[0].Subsystem)
	require.True(t, channels[0].Tutorial)
}

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

	ctx := context.Background()
	db := startOnboardingRollbackDB(t, ctx)
	fixture := seedOnboardingThreadConversation(t, ctx, db, "mock_reply")
	store := New(db)
	evidence := fixture.evidence
	evidence.CompleteImmediately = true

	require.NoError(t, store.RecordVisibleReply(ctx, evidence))
	state, err := store.GetForUser(ctx, fixture.userID, fixture.workspaceID)
	require.NoError(t, err)
	require.Equal(t, StatusCompleted, state.Status)
	require.Equal(t, StepDone, state.CurrentStep)
	require.NotNil(t, state.CompletedAt)
	require.Equal(t, fixture.replyMessageID, state.Progress.ReplyMessageID)
	require.Equal(t, fixture.threadID, state.Progress.ReplyScopeID)
	require.Zero(t, state.Progress.VisibleThroughSeq)
}

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

	ctx := context.Background()
	db := startOnboardingRollbackDB(t, ctx)
	fixture := seedOnboardingThreadConversation(t, ctx, db, "thread_reply")
	store := New(db)

	require.NoError(t, store.RecordVisibleReply(ctx, fixture.evidence))
	state, err := store.GetForUser(ctx, fixture.userID, fixture.workspaceID)
	require.NoError(t, err)
	require.Equal(t, fixture.replyMessageID, state.Progress.ReplyMessageID)
	require.Equal(t, fixture.threadID, state.Progress.ReplyScopeID)
	require.Nil(t, state.CompletedAt)

	markOnboardingScopeRead(t, ctx, db, fixture.channelID, fixture.ownerMemberID, 2)
	state, err = store.AcknowledgeVisibleReply(ctx, fixture.userID, fixture.workspaceID, fixture.channelID, fixture.replyMessageID)
	require.NoError(t, err)
	require.Nil(t, state.CompletedAt)

	markOnboardingScopeRead(t, ctx, db, fixture.threadID, fixture.ownerMemberID, 2)
	state, err = store.AcknowledgeVisibleReply(ctx, fixture.userID, fixture.workspaceID, fixture.threadID, fixture.replyMessageID)
	require.NoError(t, err)
	require.NotNil(t, state.CompletedAt)
	require.EqualValues(t, 2, state.Progress.VisibleThroughSeq)
}

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

	ctx := context.Background()
	db := startOnboardingRollbackDB(t, ctx)
	fixture := seedOnboardingConversation(t, ctx, db, "read_first")
	store := New(db)

	markOnboardingReplyRead(t, ctx, db, fixture, 2)
	require.NoError(t, store.RecordVisibleReply(ctx, fixture.evidence))
	state, err := store.GetForUser(ctx, fixture.userID, fixture.workspaceID)
	require.NoError(t, err)
	require.Nil(t, state.CompletedAt)

	state, err = store.AcknowledgeVisibleReply(ctx, fixture.userID, fixture.workspaceID, fixture.channelID, fixture.replyMessageID)
	require.NoError(t, err)
	require.NotNil(t, state.CompletedAt)
	require.EqualValues(t, 2, state.Progress.VisibleThroughSeq)
	completedAt := *state.CompletedAt

	require.NoError(t, store.RecordVisibleReply(ctx, VisibleReplyEvidence{
		WorkspaceID:    fixture.workspaceID,
		AgentID:        "agt_missing",
		MessageID:      fixture.messageID,
		ReplyMessageID: fixture.replyMessageID,
	}))
	state, err = store.GetForUser(ctx, fixture.userID, fixture.workspaceID)
	require.NoError(t, err)
	require.Equal(t, completedAt, *state.CompletedAt)
}

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

	ctx := context.Background()
	db := startOnboardingRollbackDB(t, ctx)
	fixture := seedOnboardingConversation(t, ctx, db, "wrong_reply_ack")
	store := New(db)

	require.NoError(t, store.RecordVisibleReply(ctx, fixture.evidence))
	markOnboardingReplyRead(t, ctx, db, fixture, 2)
	state, err := store.AcknowledgeVisibleReply(ctx, fixture.userID, fixture.workspaceID, fixture.channelID, fixture.messageID)
	require.NoError(t, err)
	require.Nil(t, state.CompletedAt)
	require.Zero(t, state.Progress.VisibleThroughSeq)
}

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

	ctx := context.Background()
	db := startOnboardingRollbackDB(t, ctx)
	fixture := seedOnboardingConversation(t, ctx, db, "wrong_mention")
	store := New(db)

	mustOnboardingExec(t, ctx, db, `
UPDATE scope_messages
SET metadata = JSON_OBJECT('mentions', JSON_ARRAY(JSON_OBJECT('type', 'agent', 'id', 'agt_other', 'token', 'other')))
WHERE id = $1
`, fixture.messageID)
	require.NoError(t, store.RecordVisibleReply(ctx, fixture.evidence))
	state, err := store.GetForUser(ctx, fixture.userID, fixture.workspaceID)
	require.NoError(t, err)
	require.Empty(t, state.Progress.ReplyMessageID)
	require.Nil(t, state.CompletedAt)
}

type onboardingConversationFixture struct {
	userID         string
	workspaceID    string
	ownerMemberID  string
	channelID      string
	threadID       string
	messageID      string
	replyMessageID string
	agentID        string
	agentMemberID  string
	evidence       VisibleReplyEvidence
}

func seedOnboardingConversation(t *testing.T, ctx context.Context, db coredb.DB, suffix string) onboardingConversationFixture {
	t.Helper()
	now := time.Now().UTC()
	fixture := onboardingConversationFixture{
		userID:         "usr_onboarding_" + suffix,
		workspaceID:    "ws_onboarding_" + suffix,
		ownerMemberID:  "mem_onboarding_" + suffix,
		messageID:      "msg_onboarding_" + suffix,
		replyMessageID: "reply_onboarding_" + suffix,
	}
	organizationID := "org_onboarding_" + suffix
	agentID := "agt_onboarding_" + suffix
	agentMemberID := "mem_agent_onboarding_" + suffix
	machineID := "mch_onboarding_" + suffix
	seedOnboardingWorkspace(t, ctx, db, fixture.userID, organizationID, fixture.workspaceID, fixture.ownerMemberID)
	store := New(db)
	state, err := store.EnsureForUser(ctx, fixture.userID, fixture.workspaceID, "en")
	require.NoError(t, err)
	fixture.channelID = state.State.TutorialChannelID

	mustOnboardingExec(t, ctx, db, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES ($1, $2, 'agent', $1, 'active', $3, $3)
`, agentMemberID, fixture.workspaceID, now)
	mustOnboardingExec(t, ctx, db, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $3, $1, $4, $5, $5)
`, machineID, fixture.workspaceID, fixture.userID, "key-"+suffix, now)
	mustOnboardingExec(t, ctx, db, `
INSERT INTO agents (id, workspace_id, machine_id, member_id, name, runtime, model, description, activity_detail, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $1, 'builtin', 'agentteam/deepseek-v4-flash', '', '', 'active', $5, $5)
`, agentID, fixture.workspaceID, machineID, agentMemberID, now)
	mustOnboardingExec(t, ctx, db, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq,
  author_member_id, author_type, content, metadata, created_at, updated_at
) VALUES
  ($1, $2, $3, 1, 1, $4, 'human', 'hello', JSON_OBJECT('mentions', JSON_ARRAY(JSON_OBJECT('type', 'agent', 'id', $5, 'token', 'assistant'))), $8, $8),
  ($6, $2, $3, 2, 2, $7, 'agent', 'reply', NULL, $8, $8)
`, fixture.messageID, fixture.workspaceID, fixture.channelID, fixture.ownerMemberID, agentID, fixture.replyMessageID, agentMemberID, now)
	fixture.evidence = VisibleReplyEvidence{
		WorkspaceID:    fixture.workspaceID,
		AgentID:        agentID,
		MessageID:      fixture.messageID,
		ReplyMessageID: fixture.replyMessageID,
	}
	fixture.agentID = agentID
	fixture.agentMemberID = agentMemberID
	return fixture
}

func seedOnboardingThreadConversation(t *testing.T, ctx context.Context, db coredb.DB, suffix string) onboardingConversationFixture {
	t.Helper()
	now := time.Now().UTC()
	fixture := seedOnboardingConversation(t, ctx, db, suffix)
	threadID := "thr_onboarding_" + suffix
	threadMessageID := "msg_thread_onboarding_" + suffix
	threadReplyMessageID := "reply_thread_onboarding_" + suffix
	mustOnboardingExec(t, ctx, db, `
INSERT INTO scopes (
  id, workspace_id, type, parent_scope_id, root_message_id, root_message_short_id,
  identity_key, latest_seq, latest_global_seq, latest_message_at, archived_until_seq,
  status, created_by_member_id, created_at, updated_at, archived_at
) VALUES ($1, $2, 'thread', $3, $4, NULL, $5, 2, 4, $6, 0, 'active', $7, $6, $6, NULL)
`, threadID, fixture.workspaceID, fixture.channelID, fixture.messageID, "scope/thread/"+threadID, now, fixture.ownerMemberID)
	mustOnboardingExec(t, ctx, db, `
INSERT INTO scope_memberships (
  scope_id, member_id, visibility, owner_member_id, last_read_seq, last_read_global_seq,
  last_activity_global_seq, owner_context_dismissed_through_global_seq, effective_from_seq,
  effective_to_seq, created_at, updated_at, removed_at, archived_at, unread_generation
) VALUES ($1, $2, 'public', NULL, 0, 0, 0, 0, 0, NULL, $3, $3, NULL, NULL, 0)
`, threadID, fixture.ownerMemberID, now)
	mustOnboardingExec(t, ctx, db, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq,
  author_member_id, author_type, content, metadata, created_at, updated_at
) VALUES
  ($1, $2, $3, 1, 3, $4, 'human', 'please implement this', JSON_OBJECT('mentions', JSON_ARRAY(JSON_OBJECT('type', 'agent', 'id', $5, 'token', 'assistant'))), $8, $8),
  ($6, $2, $3, 2, 4, $7, 'agent', 'done', NULL, $8, $8)
`, threadMessageID, fixture.workspaceID, threadID, fixture.ownerMemberID, fixture.agentID, threadReplyMessageID, "mem_agent_onboarding_"+suffix, now)

	fixture.threadID = threadID
	fixture.replyMessageID = threadReplyMessageID
	fixture.evidence = VisibleReplyEvidence{
		WorkspaceID:    fixture.workspaceID,
		AgentID:        fixture.agentID,
		MessageID:      threadMessageID,
		ReplyMessageID: threadReplyMessageID,
	}
	return fixture
}

func markOnboardingReplyRead(t *testing.T, ctx context.Context, db coredb.DB, fixture onboardingConversationFixture, seq int64) {
	t.Helper()
	markOnboardingScopeRead(t, ctx, db, fixture.channelID, fixture.ownerMemberID, seq)
}

func markOnboardingScopeRead(t *testing.T, ctx context.Context, db coredb.DB, scopeID string, memberID string, seq int64) {
	t.Helper()
	mustOnboardingExec(t, ctx, db, `
UPDATE scope_memberships
SET last_read_seq = $3, updated_at = $4
WHERE scope_id = $1 AND member_id = $2
`, scopeID, memberID, seq, time.Now().UTC())
}
