package channel

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

	"github.com/stretchr/testify/require"

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

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

	sourceBytes, err := os.ReadFile("collaboration.go")
	require.NoError(t, err)
	createSource := channelSourceFromFunctionForTest(t, string(sourceBytes), "func (store *Store) CreateChannelInTx")
	require.Contains(t, createSource, "UpsertScope")
	require.Contains(t, createSource, "INSERT INTO scope_channel_profiles")

	ctx := context.Background()
	pool := startChannelDiscoveryTestDB(t, ctx)
	now := time.Date(2026, 5, 15, 6, 20, 0, 0, time.UTC)

	const (
		workspaceID   = "wsp_create_channel_scope"
		ownerUserID   = "usr_create_channel_scope_owner"
		ownerMemberID = "mem_create_channel_scope_owner"
	)
	seedChannelDiscoveryWorkspace(t, ctx, pool, workspaceID, ownerUserID, ownerMemberID, now)

	store := New(pool, Options{
		RequireWorkspaceAccess: func(ctx context.Context, userID string, workspaceID string) (string, error) {
			return channelDiscoveryRequireWorkspaceAccess(ctx, pool, userID, workspaceID)
		},
		EnsureChannelScopeParticipantProjection: func(ctx context.Context, execer coredb.QueryExecer, channelID string, memberID string, deliveryPolicy string, createdAt time.Time) error {
			return message.EnsureScopeMembership(ctx, execer, channelID, memberID, "public", "", deliveryPolicy, 0, 0, createdAt)
		},
		CheckWorkspaceQuotaInTx: func(context.Context, coredb.Tx, string, plan.MetricKey, int64) error { return nil },
	})
	created, err := store.CreateChannelForUser(ctx, ownerUserID, workspaceID, "Project Alpha")
	require.NoError(t, err)
	require.NotEmpty(t, created.ChannelID)
	require.Equal(t, "project-alpha", created.Slug)
	require.True(t, created.Joined)
	require.True(t, created.CanManage)
	require.Equal(t, "", created.Description)
	require.Equal(t, "private", created.Visibility)

	var scopeCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scopes s
JOIN scope_channel_profiles p ON p.scope_id = s.id
WHERE s.id = $1
  AND s.workspace_id = $2
  AND s.type = 'channel'
  AND s.status = 'active'
  AND p.slug = 'project-alpha'
  AND p.display_name = 'Project Alpha'
  AND p.description = ''
  AND p.visibility = 'private'
  AND p.owner_member_id = $3
`, created.ChannelID, workspaceID, ownerMemberID).Scan(&scopeCount))
	require.Equal(t, 1, scopeCount)

	var membershipCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'public'
  AND removed_at IS NULL
`, created.ChannelID, ownerMemberID).Scan(&membershipCount))
	require.Equal(t, 1, membershipCount)

}

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

	sourceBytes, err := os.ReadFile("collaboration.go")
	require.NoError(t, err)
	listSource := channelSourceFromFunctionForTest(t, string(sourceBytes), "func (store *Store) ListManagedIMChannelsForUserPage")
	require.Contains(t, listSource, "ORDER BY slug ASC, channel_id ASC")
	require.Contains(t, listSource, "WITH managed_im_channels AS")
	require.NotContains(t, listSource, "BeginTx", "count and page must come from one statement/snapshot")
	require.NotContains(t, listSource, "QueryRow", "count and page must come from one statement/snapshot")

	ctx := context.Background()
	pool := startChannelDiscoveryTestDB(t, ctx)
	now := time.Date(2026, 8, 9, 0, 45, 0, 0, time.UTC)

	const (
		workspaceID   = "wsp_managed_im_page"
		ownerUserID   = "usr_managed_im_page_owner"
		ownerMemberID = "mem_managed_im_page_owner"
	)
	seedChannelDiscoveryWorkspace(t, ctx, pool, workspaceID, ownerUserID, ownerMemberID, now)

	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_a", "a-managed", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_b", "b-space-team", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_c", "c-managed", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_kb", "d-kb", ownerMemberID, "kb", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_tutorial", "e-tutorial", ownerMemberID, "tutorial", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_team", "f-team", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_unjoined", "g-unjoined", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_private_membership", "h-private-membership", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_removed_membership", "i-removed-membership", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_closed_membership", "j-closed-membership", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_inactive", "k-inactive", ownerMemberID, "im", now)
	seedManagedIMListChannel(t, ctx, pool, workspaceID, "chn_managed_archived", "l-archived", ownerMemberID, "im", now)

	mustExecChannelDiscovery(t, ctx, pool, `DELETE FROM scope_memberships WHERE scope_id = $1`, "chn_managed_unjoined")
	mustExecChannelDiscovery(t, ctx, pool, `UPDATE scope_memberships SET visibility = 'private' WHERE scope_id = $1`, "chn_managed_private_membership")
	mustExecChannelDiscovery(t, ctx, pool, `UPDATE scope_memberships SET removed_at = $2 WHERE scope_id = $1`, "chn_managed_removed_membership", now)
	mustExecChannelDiscovery(t, ctx, pool, `UPDATE scope_memberships SET effective_to_seq = 1 WHERE scope_id = $1`, "chn_managed_closed_membership")
	mustExecChannelDiscovery(t, ctx, pool, `UPDATE scopes SET status = 'archived' WHERE id = $1`, "chn_managed_inactive")
	mustExecChannelDiscovery(t, ctx, pool, `UPDATE scopes SET archived_at = $2 WHERE id = $1`, "chn_managed_archived", now)

	// The legacy service filter trims TeamID before deciding whether a row is a
	// team carrier. Preserve that exact boundary behavior while moving the
	// predicate into SQL: a whitespace-only team id remains a plain IM row,
	// while a non-empty id is excluded.
	seedManagedIMListTeam(t, ctx, pool, workspaceID, "   ", "space-team", "chn_managed_b", ownerMemberID, now)
	seedManagedIMListTeam(t, ctx, pool, workspaceID, "team_managed", "managed-team", "chn_managed_team", ownerMemberID, now)

	store := New(pool, Options{RequireWorkspaceAccess: func(ctx context.Context, userID string, workspaceID string) (string, error) {
		return channelDiscoveryRequireWorkspaceAccess(ctx, pool, userID, workspaceID)
	}})

	first, err := store.ListManagedIMChannelsForUserPage(ctx, ownerUserID, workspaceID, ManagedIMChannelListOptions{Limit: 2})
	require.NoError(t, err)
	require.Equal(t, []string{"chn_managed_a", "chn_managed_b"}, managedIMChannelIDs(first.Channels))
	require.True(t, first.HasMore)
	require.Equal(t, "2", first.NextCursor)
	require.Equal(t, 3, first.TotalCount)

	second, err := store.ListManagedIMChannelsForUserPage(ctx, ownerUserID, workspaceID, ManagedIMChannelListOptions{
		Limit:  2,
		Cursor: first.NextCursor,
	})
	require.NoError(t, err)
	require.Equal(t, []string{"chn_managed_c"}, managedIMChannelIDs(second.Channels))
	require.False(t, second.HasMore)
	require.Empty(t, second.NextCursor)
	require.Equal(t, 3, second.TotalCount)

	beyondEnd, err := store.ListManagedIMChannelsForUserPage(ctx, ownerUserID, workspaceID, ManagedIMChannelListOptions{
		Limit:  2,
		Cursor: "99",
	})
	require.NoError(t, err)
	require.Empty(t, beyondEnd.Channels)
	require.False(t, beyondEnd.HasMore)
	require.Empty(t, beyondEnd.NextCursor)
	require.Equal(t, 3, beyondEnd.TotalCount)
}

func seedManagedIMListChannel(
	t *testing.T,
	ctx context.Context,
	pool coredb.QueryExecer,
	workspaceID string,
	channelID string,
	slug string,
	ownerMemberID string,
	subsystem string,
	now time.Time,
) {
	t.Helper()
	seedChannelDiscoveryChannelScope(t, ctx, pool, workspaceID, channelID, slug, ownerMemberID, now)
	mustExecChannelDiscovery(t, ctx, pool, `
UPDATE scope_channel_profiles
SET subsystem = $2
WHERE scope_id = $1
`, channelID, subsystem)
}

func seedManagedIMListTeam(
	t *testing.T,
	ctx context.Context,
	pool coredb.QueryExecer,
	workspaceID string,
	teamID string,
	handle string,
	channelID string,
	ownerMemberID string,
	now time.Time,
) {
	t.Helper()
	mustExecChannelDiscovery(t, ctx, pool, `
INSERT INTO workspace_teams (
    id, workspace_id, handle, display_name, description, status,
    channel_id, created_by_member_id, created_at, updated_at
) VALUES ($1, $2, $3, $3, '', 'active', $4, $5, $6, $6)
`, teamID, workspaceID, handle, channelID, ownerMemberID, now)
}

func managedIMChannelIDs(channels []message.ChannelSummary) []string {
	ids := make([]string, 0, len(channels))
	for _, channel := range channels {
		ids = append(ids, channel.ChannelID)
	}
	return ids
}

func channelSourceFromFunctionForTest(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]
}
