package privateagent

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

	"github.com/stretchr/testify/require"

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

func legacyTableNameForPrivateAgentTest(parts ...string) string {
	return strings.Join(parts, "_")
}

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

	for _, filename := range []string{"conversation_write.go", "dependencies.go"} {
		source, err := os.ReadFile(filename)
		require.NoError(t, err)
		require.NotContains(t, string(source), legacyTableNameForPrivateAgentTest("targeted", "delivery", "events"), filename)
	}
}

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

	_, err := os.Stat("private_agent_wake_resolver.go")
	require.True(t, os.IsNotExist(err), "private-agent-specific wake resolver should stay deleted")

	source, err := os.ReadFile("dependencies.go")
	require.NoError(t, err)

	require.NotContains(t, string(source), "scope_boundary_grants")
	require.NotContains(t, string(source), "MaxPrivateAgentWakeDirectMentionTokens")
	require.NotContains(t, string(source), "parsePrivateAgentWakeDirectMentionTokens")
	require.NotContains(t, string(source), "normalizePrivateAgentWakeMentionTokens")
}

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

	for _, filename := range []string{
		"scope_notifications.go",
		"wake_state.go",
	} {
		source, err := os.ReadFile(filename)
		require.NoError(t, err)
		contents := string(source)
		require.NotContains(t, contents, legacyTableNameForPrivateAgentTest("channel", "agent", "entries"), filename)
		require.NotContains(t, contents, legacyTableNameForPrivateAgentTest("space", "participants"), filename)
		require.NotContains(t, contents, legacyTableNameForPrivateAgentTest("space", "subscriptions"), filename)
		require.NotContains(t, contents, legacyTableNameForPrivateAgentTest("thread", "subscriptions"), filename)
		require.NotContains(t, contents, legacyTableNameForPrivateAgentTest("reply", "threads"), filename)
		require.NotContains(t, contents, legacyTableNameForPrivateAgentTest("dm", "threads"), filename)
	}
}

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

	source, err := os.ReadFile("private_agents.go")
	require.NoError(t, err)
	contents := string(source)

	ensureStart := strings.Index(contents, "func (store *Store) ensurePrivateChannelAgentEntry")
	require.NotEqual(t, -1, ensureStart)
	ensureMembershipStart := strings.Index(contents, "func ensurePrivateAgentScopeMembership")
	require.NotEqual(t, -1, ensureMembershipStart)
	require.Less(t, ensureStart, ensureMembershipStart)
	ensureBody := contents[ensureStart:ensureMembershipStart]
	require.Contains(t, ensureBody, "ensurePrivateAgentScopeMembership(ctx, tx, channelID, agent, createdAt)")
	require.NotContains(t, ensureBody, legacyTableNameForPrivateAgentTest("channel", "agent", "entries"))
	require.NotContains(t, contents, "LegacyChannelAgentEntry")

	removeStart := strings.Index(contents, "func (store *Store) RemoveChannelPrivateAgentForUser")
	require.NotEqual(t, -1, removeStart)
	removeMembershipStart := strings.Index(contents, "func markPrivateAgentScopeMembershipRemoved")
	require.NotEqual(t, -1, removeMembershipStart)
	require.Less(t, removeStart, removeMembershipStart)
	removeBody := contents[removeStart:removeMembershipStart]
	require.Contains(t, removeBody, "markPrivateAgentScopeMembershipRemoved(ctx, tx, channelID, agent.AgentMemberID, agent.OwnerMemberID, now)")
	require.NotContains(t, removeBody, legacyTableNameForPrivateAgentTest("channel", "agent", "entries"))
}

func TestMarkPublicChannelAgentEntryProjectionRemovedClearsHostThreadScopeState(t *testing.T) {
	execer := &captureExecer{}

	err := (&Store{}).markPublicChannelAgentEntryProjectionRemoved(context.Background(), execer, "chn-host", "mem-agent", time.Now().UTC())

	require.NoError(t, err)
	require.Len(t, execer.calls, 2)
	require.Contains(t, execer.calls[0].query, "DELETE sns")
	require.Contains(t, execer.calls[0].query, "JOIN scopes scope")
	require.Contains(t, execer.calls[0].query, "scope.id = $1 OR scope.parent_scope_id = $1")
	require.Equal(t, []any{"chn-host", "mem-agent"}, execer.calls[0].args)
	require.Contains(t, execer.calls[1].query, "UPDATE scope_memberships sm")
	require.Contains(t, execer.calls[1].query, "JOIN scopes scope")
	require.Contains(t, execer.calls[1].query, "sm.visibility = 'public'")
	require.Contains(t, execer.calls[1].query, "scope.id = $1 OR scope.parent_scope_id = $1")
	require.Len(t, execer.calls[1].args, 3)
	require.Equal(t, "chn-host", execer.calls[1].args[0])
	require.Equal(t, "mem-agent", execer.calls[1].args[1])
}

func TestRemovePrivateEntriesForOwnerHostChannelClearsWakeSubscriptions(t *testing.T) {
	execer := &captureExecer{}

	err := (&Store{}).removePrivateEntriesForOwnerHostChannel(context.Background(), execer, "mem-owner", "chn-host", time.Now().UTC())

	require.NoError(t, err)
	require.Len(t, execer.calls, 2)
	require.Contains(t, execer.calls[0].query, "DELETE sns")
	require.Contains(t, execer.calls[0].query, "FROM scope_notification_settings sns")
	require.Contains(t, execer.calls[0].query, "JOIN scope_memberships sm")
	require.Contains(t, execer.calls[0].query, "JOIN scopes scope")
	require.NotContains(t, execer.calls[0].query, legacyTableNameForPrivateAgentTest("space", "subscriptions"))
	require.NotContains(t, execer.calls[0].query, legacyTableNameForPrivateAgentTest("thread", "subscriptions"))
	require.NotContains(t, execer.calls[0].query, legacyTableNameForPrivateAgentTest("reply", "threads"))
	require.Equal(t, []any{"chn-host", "mem-owner"}, execer.calls[0].args)
	require.Contains(t, execer.calls[1].query, "UPDATE scope_memberships sm")
	require.Contains(t, execer.calls[1].query, "sm.owner_member_id = $2")
	require.NotContains(t, execer.calls[1].query, legacyTableNameForPrivateAgentTest("channel", "agent", "entries"))
}

func TestRemovePrivateEntriesForOwnerClearsWakeSubscriptions(t *testing.T) {
	execer := &captureExecer{}

	err := (&Store{}).removePrivateEntriesForOwner(context.Background(), execer, "mem-owner", time.Now().UTC())

	require.NoError(t, err)
	require.Len(t, execer.calls, 2)
	require.Contains(t, execer.calls[0].query, "DELETE sns")
	require.Contains(t, execer.calls[0].query, "FROM scope_notification_settings sns")
	require.Contains(t, execer.calls[0].query, "JOIN scope_memberships sm")
	require.NotContains(t, execer.calls[0].query, legacyTableNameForPrivateAgentTest("space", "subscriptions"))
	require.NotContains(t, execer.calls[0].query, legacyTableNameForPrivateAgentTest("thread", "subscriptions"))
	require.NotContains(t, execer.calls[0].query, legacyTableNameForPrivateAgentTest("reply", "threads"))
	require.Equal(t, []any{"mem-owner"}, execer.calls[0].args)
	require.Contains(t, execer.calls[1].query, "UPDATE scope_memberships")
	require.Contains(t, execer.calls[1].query, "owner_member_id = $1")
	require.NotContains(t, execer.calls[1].query, legacyTableNameForPrivateAgentTest("channel", "agent", "entries"))
}

func TestLoadOwnedChannelNotificationAgentsIssuesCursorPaginatedUnion(t *testing.T) {
	queryer := &scopeNotificationAgentQueryCapture{}

	_, _, _, err := (&Store{}).loadOwnedChannelNotificationAgents(context.Background(), queryer, "wsp-1", "chn-1", "mem-owner", "", 0)

	require.NoError(t, err)
	require.Len(t, queryer.calls, 1)
	query := queryer.calls[0].query
	require.Contains(t, query, "UNION ALL")
	require.Contains(t, query, "sm.archived_at IS NULL")
	require.Contains(t, query, "sm.effective_to_seq IS NULL")
	require.Contains(t, query, "LOWER(TRIM(display_name)) AS sort_key")
	require.Contains(t, query, "ORDER BY sort_key ASC, agent_id ASC")
	// pageSize defaults to defaultScopeNotificationAgentPageSize and the SQL
	// LIMIT is pageSize+1 so the Go wrapper can detect hasMore in one round trip.
	require.Equal(t, defaultScopeNotificationAgentPageSize+1, queryer.calls[0].args[len(queryer.calls[0].args)-1])
}

func TestLoadOwnedThreadNotificationAgentsIssuesCursorPaginatedUnion(t *testing.T) {
	queryer := &scopeNotificationAgentQueryCapture{}

	_, _, _, err := (&Store{}).loadOwnedThreadNotificationAgents(context.Background(), queryer, "wsp-1", "thr-1", "chn-1", "mem-owner", "", 0)

	require.NoError(t, err)
	require.Len(t, queryer.calls, 1)
	query := queryer.calls[0].query
	require.Contains(t, query, "UNION ALL")
	require.Contains(t, query, "sm.archived_at IS NULL")
	require.Contains(t, query, "sm.effective_to_seq IS NULL")
	require.Contains(t, query, "LOWER(TRIM(display_name)) AS sort_key")
	require.Contains(t, query, "ORDER BY sort_key ASC, agent_id ASC")
	require.Equal(t, defaultScopeNotificationAgentPageSize+1, queryer.calls[0].args[len(queryer.calls[0].args)-1])
}

type captureExecer struct {
	calls []captureExecCall
}

type captureExecCall struct {
	query string
	args  []any
}

func (execer *captureExecer) Exec(_ context.Context, query string, args ...any) (coredb.Result, error) {
	execer.calls = append(execer.calls, captureExecCall{query: query, args: append([]any(nil), args...)})
	return captureResult(1), nil
}

func (execer *captureExecer) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return nil, errors.New("unexpected query")
}

func (execer *captureExecer) QueryRow(context.Context, string, ...any) coredb.Row {
	return nil
}

type scopeNotificationAgentQueryCapture struct {
	calls []captureExecCall
}

func (queryer *scopeNotificationAgentQueryCapture) Query(_ context.Context, query string, args ...any) (coredb.Rows, error) {
	if !strings.Contains(query, "FROM scope_memberships") {
		return nil, errors.New("unexpected query")
	}
	if strings.Contains(query, legacyTableNameForPrivateAgentTest("channel", "agent", "entries")) || strings.Contains(query, legacyTableNameForPrivateAgentTest("space", "participants")) || strings.Contains(query, legacyTableNameForPrivateAgentTest("thread", "subscriptions")) || strings.Contains(query, legacyTableNameForPrivateAgentTest("space", "subscriptions")) {
		return nil, errors.New("target notification agent query should use scope state")
	}
	queryer.calls = append(queryer.calls, captureExecCall{query: query, args: append([]any(nil), args...)})
	return privateAgentEmptyRows{}, nil
}

func (queryer *scopeNotificationAgentQueryCapture) QueryRow(context.Context, string, ...any) coredb.Row {
	return privateAgentEmptyRow{}
}

type privateAgentEmptyRows struct{}

func (rows privateAgentEmptyRows) Close()     {}
func (rows privateAgentEmptyRows) Err() error { return nil }
func (rows privateAgentEmptyRows) Next() bool { return false }
func (rows privateAgentEmptyRows) Scan(...any) error {
	return errors.New("unexpected scan")
}

type privateAgentEmptyRow struct{}

func (row privateAgentEmptyRow) Scan(...any) error {
	return errors.New("unexpected scan")
}

type captureResult int64

func (result captureResult) RowsAffected() int64 { return int64(result) }
