package message

import (
	"context"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

var (
	benchmarkHistoryRecordsSink []model.ThreadMessageRecord
	benchmarkHistoryHasMoreSink bool
)

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

	bounds := model.ThreadHistoryBounds{LatestSeq: 1004, ArchivedUntilSeq: 1000}
	calls := make([]string, 0)
	records, hasMore, err := AssembleThreadHistoryRecords(
		context.Background(),
		bounds,
		6,
		nil,
		nil,
		HistoryRangeReader{
			Asc: func(context.Context, int64, int64, int) ([]model.ThreadMessageRecord, error) {
				panic("unexpected cold asc")
			},
			Desc: func(_ context.Context, lowSeq int64, highSeq int64, limit int) ([]model.ThreadMessageRecord, error) {
				calls = append(calls, fmt.Sprintf("cold-desc:%d-%d:%d", lowSeq, highSeq, limit))
				return syntheticThreadRecords(highSeq, lowSeq, -1, limit, "cold"), nil
			},
		},
		HistoryRangeReader{
			Asc: func(context.Context, int64, int64, int) ([]model.ThreadMessageRecord, error) {
				panic("unexpected hot asc")
			},
			Desc: func(_ context.Context, lowSeq int64, highSeq int64, limit int) ([]model.ThreadMessageRecord, error) {
				calls = append(calls, fmt.Sprintf("hot-desc:%d-%d:%d", lowSeq, highSeq, limit))
				return syntheticThreadRecords(highSeq, lowSeq, -1, limit, "hot"), nil
			},
		},
	)
	require.NoError(t, err)
	require.True(t, hasMore)
	require.Equal(t, []string{"hot-desc:1001-1004:7", "cold-desc:1-1000:3"}, calls)
	require.Equal(t, []int64{999, 1000, 1001, 1002, 1003, 1004}, recordSeqs(records))
}

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

	after := int64(998)
	bounds := model.ThreadHistoryBounds{LatestSeq: 1004, ArchivedUntilSeq: 1000}
	calls := make([]string, 0)
	records, hasMore, err := AssembleThreadHistoryRecords(
		context.Background(),
		bounds,
		4,
		nil,
		&after,
		HistoryRangeReader{
			Asc: func(_ context.Context, lowSeq int64, highSeq int64, limit int) ([]model.ThreadMessageRecord, error) {
				calls = append(calls, fmt.Sprintf("cold-asc:%d-%d:%d", lowSeq, highSeq, limit))
				return syntheticThreadRecords(lowSeq, highSeq, 1, limit, "cold"), nil
			},
			Desc: func(context.Context, int64, int64, int) ([]model.ThreadMessageRecord, error) {
				panic("unexpected cold desc")
			},
		},
		HistoryRangeReader{
			Asc: func(_ context.Context, lowSeq int64, highSeq int64, limit int) ([]model.ThreadMessageRecord, error) {
				calls = append(calls, fmt.Sprintf("hot-asc:%d-%d:%d", lowSeq, highSeq, limit))
				return syntheticThreadRecords(lowSeq, highSeq, 1, limit, "hot"), nil
			},
			Desc: func(context.Context, int64, int64, int) ([]model.ThreadMessageRecord, error) {
				panic("unexpected hot desc")
			},
		},
	)
	require.NoError(t, err)
	require.True(t, hasMore)
	require.Equal(t, []string{"cold-asc:999-1000:5", "hot-asc:1001-1004:3"}, calls)
	require.Equal(t, []int64{999, 1000, 1001, 1002}, recordSeqs(records))
}

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

	addedAt := time.Date(2026, 5, 6, 10, 0, 0, 0, time.UTC)
	expiresAt := addedAt.Add(2 * time.Hour)
	visibility := OwnerPrivateContextVisibility{
		AlwaysVisibleAuthorMemberIDs: map[string]struct{}{
			"mem_owner_agent": {},
		},
		PeopleGrants: []ownerPrivateHistoryPeopleGrant{
			{
				MemberID:  "mem_collab",
				AddedAt:   addedAt,
				ExpiresAt: &expiresAt,
			},
		},
	}

	require.True(t, OwnerPrivateContextHistoryRecordVisible(model.ThreadMessageRecord{
		AuthorMemberID: "mem_owner_agent",
		CreatedAt:      addedAt.Add(-time.Hour),
	}, visibility))
	require.True(t, OwnerPrivateContextHistoryRecordVisible(model.ThreadMessageRecord{
		AuthorMemberID: "mem_collab",
		CreatedAt:      addedAt.Add(time.Hour),
	}, visibility))
	require.False(t, OwnerPrivateContextHistoryRecordVisible(model.ThreadMessageRecord{
		AuthorMemberID: "mem_collab",
		CreatedAt:      addedAt.Add(-time.Second),
	}, visibility))
	require.False(t, OwnerPrivateContextHistoryRecordVisible(model.ThreadMessageRecord{
		AuthorMemberID: "mem_collab",
		CreatedAt:      expiresAt,
	}, visibility))
	require.False(t, OwnerPrivateContextHistoryRecordVisible(model.ThreadMessageRecord{
		ScopeID:        "chn_shared",
		ScopeKind:      "channel",
		AuthorMemberID: "mem_channel_member",
		AuthorType:     "human",
		CreatedAt:      addedAt.Add(time.Hour),
	}, visibility))
}

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

	source, err := os.ReadFile("history.go")
	require.NoError(t, err)
	visibleSource := sourceFromFunctionForHistoryTest(t, string(source), "func loadOwnerPrivateHistoryVisibleAuthorMemberIDs")
	require.Contains(t, visibleSource, "FROM scope_memberships sm")
	require.Contains(t, visibleSource, "JOIN scope_memberships sibling_private")
	require.NotContains(t, visibleSource, legacyTableNameForMessageTest("channel", "agent", "entries"))
	sourceScopesSource := sourceFromFunctionForHistoryTest(t, string(source), "func loadOwnerPrivateHistorySourceScopes")
	require.Contains(t, sourceScopesSource, "FROM scopes scope")
	require.Contains(t, sourceScopesSource, "LEFT JOIN scopes parent_scope")
	require.NotContains(t, sourceScopesSource, legacyTableNameForMessageTest("reply", "threads"))

	ctx := context.Background()
	pool := startMessagingTestPool(t, ctx)
	now := time.Date(2026, 5, 14, 9, 51, 0, 0, time.UTC)

	const (
		workspaceID       = "wsp_owner_private_history_scope"
		ownerUserID       = "usr_owner_private_history_scope"
		ownerMemberID     = "mem_owner_private_history_scope"
		targetAgentMember = "mem_owner_private_history_target"
		siblingAgent      = "agt_owner_private_history_sibling"
		siblingAgentMem   = "mem_owner_private_history_sibling"
		closedAgent       = "agt_owner_private_history_closed"
		closedAgentMem    = "mem_owner_private_history_closed"
		machineID         = "mach_owner_private_history_scope"
		siblingMachineID  = "mach_owner_private_history_sibling"
		closedMachineID   = "mach_owner_private_history_closed"
		parentChannelID   = "chn_owner_private_history_scope"
		parentThreadID    = "thr_owner_private_history_parent"
		rootMessageID     = "msg_owner_private_history_root"
		childChannelID    = "chn_owner_private_history_child"
		childThreadID     = "thr_owner_private_history_child"
	)

	insertMessagingWorkspace(t, ctx, pool, workspaceID, now)
	insertMessagingUser(t, ctx, pool, ownerUserID, "owner-private-history-scope@example.com", now)
	insertMessagingHumanMember(t, ctx, pool, ownerMemberID, workspaceID, ownerUserID, now)
	insertMessagingAgentMember(t, ctx, pool, targetAgentMember, workspaceID, now)
	insertMessagingAgentMember(t, ctx, pool, siblingAgentMem, workspaceID, now)
	insertMessagingAgentMember(t, ctx, pool, closedAgentMem, workspaceID, now)
	insertMessagingMachine(t, ctx, pool, machineID, workspaceID, ownerUserID, now)
	insertOwnerPrivateHistoryMachine(t, ctx, pool, siblingMachineID, workspaceID, ownerUserID, "sibling-machine-key", now)
	insertOwnerPrivateHistoryMachine(t, ctx, pool, closedMachineID, workspaceID, ownerUserID, "closed-machine-key", now)
	insertMessagingAgent(t, ctx, pool, "agt_owner_private_history_target", workspaceID, machineID, targetAgentMember, "owner-private-history-target", now)
	insertMessagingAgent(t, ctx, pool, siblingAgent, workspaceID, siblingMachineID, siblingAgentMem, "owner-private-history-sibling", now)
	insertMessagingAgent(t, ctx, pool, closedAgent, workspaceID, closedMachineID, closedAgentMem, "owner-private-history-closed", now)
	insertMessagingChannel(t, ctx, pool, parentChannelID, workspaceID, "owner-private-history", ownerMemberID, parentThreadID, now)
	insertMessagingMessage(t, ctx, pool, rootMessageID, workspaceID, parentChannelID, ownerMemberID, "root", 1, 1, now)
	insertMessagingReplyThread(t, ctx, pool, childChannelID, workspaceID, parentChannelID, rootMessageID, "root", childThreadID, now)

	require.NoError(t, EnsureScopeMembership(ctx, pool, parentChannelID, targetAgentMember, "private", ownerMemberID, "auto-wake", 0, 0, now))
	require.NoError(t, EnsureScopeMembership(ctx, pool, parentChannelID, siblingAgentMem, "private", ownerMemberID, "auto-wake", 0, 0, now))
	require.NoError(t, EnsureScopeMembership(ctx, pool, childChannelID, siblingAgentMem, "private", ownerMemberID, "auto-wake", 0, 0, now))
	require.NoError(t, EnsureScopeMembership(ctx, pool, parentChannelID, closedAgentMem, "private", ownerMemberID, "auto-wake", 0, 0, now))
	require.NoError(t, EnsureScopeMembership(ctx, pool, childChannelID, closedAgentMem, "private", ownerMemberID, "auto-wake", 0, 0, now))
	mustExecMessaging(t, ctx, pool, `
UPDATE scope_memberships
SET effective_to_seq = 1
WHERE scope_id = $1 AND member_id = $2
`, childChannelID, closedAgentMem)

	visibility, err := loadOwnerPrivateContextVisibility(ctx, pool, workspaceID, targetAgentMember, childChannelID)
	require.NoError(t, err)
	require.Contains(t, visibility.AlwaysVisibleAuthorMemberIDs, ownerMemberID)
	require.Contains(t, visibility.AlwaysVisibleAuthorMemberIDs, targetAgentMember)
	require.Contains(t, visibility.AlwaysVisibleAuthorMemberIDs, siblingAgentMem)
	require.NotContains(t, visibility.AlwaysVisibleAuthorMemberIDs, closedAgentMem)
}

func sourceFromFunctionForHistoryTest(t *testing.T, source string, signature string) string {
	t.Helper()

	start := strings.Index(source, signature)
	require.NotEqual(t, -1, start)
	rest := source[start+len(signature):]
	next := strings.Index(rest, "\nfunc ")
	if next == -1 {
		return source[start:]
	}
	return source[start : start+len(signature)+next]
}

func insertOwnerPrivateHistoryMachine(t *testing.T, ctx context.Context, pool coredb.QueryExecer, machineID string, workspaceID string, userID string, apiKey string, now time.Time) {
	t.Helper()

	mustExecMessaging(t, ctx, pool, `
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, workspaceID, userID, apiKey, now)
}

func syntheticThreadRecords(startSeq int64, endSeq int64, step int64, limit int, sender string) []model.ThreadMessageRecord {
	records := make([]model.ThreadMessageRecord, 0, limit)
	base := time.Date(2026, 3, 10, 12, 0, 0, 0, time.UTC)
	for seq := startSeq; ; seq += step {
		records = append(records, model.ThreadMessageRecord{
			Seq:            seq,
			AuthorMemberID: sender,
			AuthorType:     "human",
			Content:        sender,
			CreatedAt:      base.Add(time.Duration(seq) * time.Second),
		})
		if len(records) == limit || seq == endSeq {
			break
		}
	}
	return records
}

func recordSeqs(records []model.ThreadMessageRecord) []int64 {
	seqs := make([]int64, 0, len(records))
	for _, record := range records {
		seqs = append(seqs, record.Seq)
	}
	return seqs
}

func BenchmarkAssembleThreadHistoryRecordsLatestPage(b *testing.B) {
	bounds := model.ThreadHistoryBounds{LatestSeq: 1004, ArchivedUntilSeq: 1000}
	coldReader := HistoryRangeReader{
		Asc: func(context.Context, int64, int64, int) ([]model.ThreadMessageRecord, error) {
			panic("unexpected cold asc")
		},
		Desc: func(_ context.Context, lowSeq int64, highSeq int64, limit int) ([]model.ThreadMessageRecord, error) {
			return syntheticThreadRecords(highSeq, lowSeq, -1, limit, "cold"), nil
		},
	}
	hotReader := HistoryRangeReader{
		Asc: func(context.Context, int64, int64, int) ([]model.ThreadMessageRecord, error) {
			panic("unexpected hot asc")
		},
		Desc: func(_ context.Context, lowSeq int64, highSeq int64, limit int) ([]model.ThreadMessageRecord, error) {
			return syntheticThreadRecords(highSeq, lowSeq, -1, limit, "hot"), nil
		},
	}

	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		records, hasMore, err := AssembleThreadHistoryRecords(
			context.Background(),
			bounds,
			64,
			nil,
			nil,
			coldReader,
			hotReader,
		)
		if err != nil {
			b.Fatal(err)
		}
		benchmarkHistoryRecordsSink = records
		benchmarkHistoryHasMoreSink = hasMore
	}
}
