package access

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/testfixture"
)

type noopQueryer struct{}

func (noopQueryer) QueryRow(context.Context, string, ...any) coredb.Row {
	panic("unexpected QueryRow call")
}

func (noopQueryer) Query(context.Context, string, ...any) (coredb.Rows, error) {
	panic("unexpected Query call")
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)
	db := pool

	now := time.Date(2026, 3, 19, 12, 0, 0, 0, time.UTC)
	workspaceID := "wsp_access"
	userID := "usr_access"
	memberID := "mem_access"
	channelID := "chn_access"

	mustExec(t, ctx, pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'access@example.com', 'access', 'Access User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'access', 'access', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'Access User', $3, 'owner', $4, $4)
`, memberID, workspaceID, userID, now)
	mustSeedChannelScope(t, ctx, pool, workspaceID, channelID, "general", "general", "General discussion", "public", "", false, now)
	mustSeedScopeMembership(t, ctx, pool, channelID, memberID, now)

	membership, err := LoadHumanWorkspaceMember(ctx, db, userID, workspaceID)
	require.NoError(t, err)
	require.Equal(t, memberID, membership.Member.ID)
	require.Equal(t, workspaceID, membership.Member.WorkspaceID)
	require.Equal(t, domain.MemberTypeHuman, membership.Member.Type)
	require.NotNil(t, membership.Member.UserID)
	require.Equal(t, userID, *membership.Member.UserID)
	require.NotNil(t, membership.Member.WorkspaceRole)
	require.Equal(t, domain.WorkspaceRoleOwner, *membership.Member.WorkspaceRole)
	require.Equal(t, "owner", membership.WorkspaceRole)

	joined, err := IsPublicMemberInChannel(ctx, db, memberID, channelID)
	require.NoError(t, err)
	require.True(t, joined)

	joined, err = IsPublicMemberInChannel(ctx, db, memberID, "chn_missing")
	require.NoError(t, err)
	require.False(t, joined)
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)
	db := pool

	now := time.Date(2026, 3, 19, 12, 30, 0, 0, time.UTC)
	workspaceID := "wsp_access_lookup"
	memberID := "mem_access_lookup"
	channelID := "chn_access_lookup"

	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'access-lookup', 'access-lookup', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'agent', 'Access Agent', NULL, NULL, $3, $3)
`, memberID, workspaceID, now)
	mustSeedChannelScope(t, ctx, pool, workspaceID, channelID, "ops", "ops", "Operations", "public", "", false, now)

	channel, err := LookupChannel(ctx, db, workspaceID, channelID)
	require.NoError(t, err)
	require.Equal(t, channelID, channel.ChannelID)
	require.Equal(t, channelID, channel.ThreadID)
	require.Equal(t, "channel", channel.Type)
	require.Equal(t, "ops", channel.Slug)
	require.Equal(t, "Operations", channel.Description)

	require.NoError(t, EnsureMemberInWorkspace(ctx, db, workspaceID, memberID))

	err = EnsureMemberInWorkspace(ctx, db, workspaceID, "mem_missing")
	require.ErrorIs(t, err, ErrNotFound)

	_, err = LookupChannel(ctx, db, workspaceID, "chn_missing")
	require.ErrorIs(t, err, ErrNotFound)
}

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

	ctx := context.Background()

	_, err := ListVisibleThreads(ctx, noopQueryer{}, "", "mem_visible")
	require.EqualError(t, err, "workspace id is required")

	_, err = ListVisibleThreads(ctx, noopQueryer{}, "wsp_visible", "")
	require.EqualError(t, err, "member id is required")
}

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

	ctx := context.Background()
	pool := startAccessTestPool(t, ctx)

	now := time.Date(2026, 5, 13, 21, 30, 0, 0, time.UTC)
	workspaceID := "wsp_visible_scope"
	memberID := "mem_visible_scope"
	peerMemberID := "mem_visible_scope_peer"
	channelScopeID := "chn_visible_scope"
	dmScopeID := "dm_visible_scope"
	channelThreadScopeID := "thr_visible_scope_channel"
	dmThreadScopeID := "thr_visible_scope_dm"
	privateOnlyScopeID := "chn_visible_scope_private_only"

	mustExec(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'visible-scope', 'visible-scope', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	mustExec(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES
($1, $3, 'human', 'Visible Member', NULL, 'member', $4, $4),
($2, $3, 'human', 'Visible Peer', NULL, 'member', $4, $4)
`, memberID, peerMemberID, workspaceID, now)
	mustExec(t, ctx, pool, `
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_at, updated_at
) VALUES
($1, $7, 'channel', NULL, NULL, NULL, 'channel:visible', 0, 0, NULL, 0, 'active', $8, $8),
($2, $7, 'dm', NULL, NULL, NULL, 'dm:visible', 0, 0, NULL, 0, 'active', $8, $8),
($3, $7, 'thread', $1, 'msg-visible-channel-root', 'chroot', 'thread:visible-channel', 0, 0, NULL, 0, 'active', $8, $8),
($4, $7, 'thread', $2, 'msg-visible-dm-root', 'dmroot', 'thread:visible-dm', 0, 0, NULL, 0, 'active', $8, $8),
($5, $7, 'channel', NULL, NULL, NULL, 'channel:private-only', 0, 0, NULL, 0, 'active', $8, $8),
($6, $7, 'thread', $5, 'msg-hidden-root', 'hidden', 'thread:hidden', 0, 0, NULL, 0, 'active', $8, $8)
`, channelScopeID, dmScopeID, channelThreadScopeID, dmThreadScopeID, privateOnlyScopeID, "thr_visible_scope_hidden", workspaceID, now)
	mustExec(t, ctx, pool, `
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
) VALUES
($1, $6, 'public', NULL, 0, 0, 0, 0, 1, NULL, $8, $8, NULL, NULL),
($2, $6, 'public', NULL, 0, 0, 0, 0, 1, NULL, $8, $8, NULL, NULL),
($2, $7, 'public', NULL, 0, 0, 0, 0, 1, NULL, $8, $8, NULL, NULL),
($3, $7, 'public', NULL, 0, 0, 0, 0, 1, NULL, $8, $8, NULL, NULL),
($5, $6, 'private', $6, 0, 0, 0, 0, 1, NULL, $8, $8, NULL, NULL)
`, channelScopeID, dmScopeID, channelThreadScopeID, dmThreadScopeID, privateOnlyScopeID, memberID, peerMemberID, now)

	threads, err := ListVisibleThreads(ctx, pool, workspaceID, memberID)
	require.NoError(t, err)
	require.ElementsMatch(t, []VisibleThread{
		{ThreadID: channelScopeID, ChannelID: channelScopeID, ParentChannelID: channelScopeID},
		{ThreadID: dmScopeID, DMThreadID: dmScopeID, ParentDMThreadID: dmScopeID},
		{ThreadID: channelThreadScopeID, ChannelID: channelThreadScopeID, ParentChannelID: channelScopeID},
		{ThreadID: dmThreadScopeID, ChannelID: dmThreadScopeID, ParentDMThreadID: dmScopeID},
	}, threads)

	peerThreads, err := ListVisibleThreads(ctx, pool, workspaceID, peerMemberID)
	require.NoError(t, err)
	require.ElementsMatch(t, []VisibleThread{
		{ThreadID: dmScopeID, DMThreadID: dmScopeID, ParentDMThreadID: dmScopeID},
		{ThreadID: dmThreadScopeID, ChannelID: dmThreadScopeID, ParentDMThreadID: dmScopeID},
	}, peerThreads)
}

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

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

	requireCurrentMembershipJoin(t, source, "sm")
	requireCurrentMembershipJoin(t, source, "self_sm")
	requireCurrentMembershipJoin(t, source, "peer_sm")
	requireCurrentMembershipJoin(t, source, "child_sm")
	requireCurrentMembershipJoin(t, source, "parent_sm")
	require.Contains(t, source, "AND removed_at IS NULL\n  AND effective_to_seq IS NULL")
	require.Contains(t, source, "AND removed_at IS NULL\n  AND effective_to_seq IS NULL\n  AND visibility = 'public'")
	require.Contains(t, source, "s.status = 'active' AND s.archived_at IS NULL")
}

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

	ctx := context.Background()

	_, err := ResolveAccessibleChannelForMember(ctx, noopQueryer{}, "", "mem_visible", "general")
	require.EqualError(t, err, "workspace id is required")

	_, err = ResolveAccessibleChannelForMember(ctx, noopQueryer{}, "wsp_visible", "", "general")
	require.EqualError(t, err, "member id is required")

	_, err = ResolveAccessibleChannelForMember(ctx, noopQueryer{}, "wsp_visible", "mem_visible", "")
	require.EqualError(t, err, "channel slug is required")
}

func requireCurrentMembershipJoin(t *testing.T, source string, alias string) {
	t.Helper()
	require.Contains(t, source, "AND "+alias+".removed_at IS NULL\n")
	require.Contains(t, source, "AND "+alias+".effective_to_seq IS NULL")
}

func mustExec(t *testing.T, ctx context.Context, pool coredb.QueryExecer, sql string, args ...any) {
	t.Helper()

	_, err := pool.Exec(ctx, sql, args...)
	require.NoError(t, err)
}

func mustSeedChannelScope(t *testing.T, ctx context.Context, pool coredb.QueryExecer, workspaceID string, scopeID string, slug string, displayName string, description string, visibility string, ownerMemberID string, isAllChannel bool, now time.Time) {
	t.Helper()

	mustExec(t, ctx, pool, `
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_at, updated_at
) VALUES ($1, $2, 'channel', NULL, NULL, NULL, $3, 0, 0, NULL, 0, 'active', $4, $4)
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)
`, scopeID, workspaceID, "channel:"+scopeID, now)

	var ownerArg any
	if ownerMemberID != "" {
		ownerArg = ownerMemberID
	}
	mustExec(t, ctx, pool, `
INSERT INTO scope_channel_profiles (
  scope_id, workspace_id, slug, display_name, description, visibility,
  owner_member_id, is_all_channel, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9)
ON DUPLICATE KEY UPDATE
  slug = VALUES(slug),
  display_name = VALUES(display_name),
  description = VALUES(description),
  visibility = VALUES(visibility),
  owner_member_id = VALUES(owner_member_id),
  is_all_channel = VALUES(is_all_channel),
  updated_at = VALUES(updated_at)
`, scopeID, workspaceID, slug, displayName, description, visibility, ownerArg, isAllChannel, now)
}

func mustSeedDMScope(t *testing.T, ctx context.Context, pool coredb.QueryExecer, workspaceID string, scopeID string, now time.Time) {
	t.Helper()

	mustExec(t, ctx, pool, `
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_at, updated_at
) VALUES ($1, $2, 'dm', NULL, NULL, NULL, $3, 0, 0, NULL, 0, 'active', $4, $4)
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)
`, scopeID, workspaceID, "dm:"+scopeID, now)
}

func mustSeedReplyThreadScope(t *testing.T, ctx context.Context, pool coredb.QueryExecer, workspaceID string, scopeID string, parentScopeID string, rootMessageID string, rootMessageShortID string, slug string, now time.Time) {
	t.Helper()

	mustExec(t, ctx, pool, `
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_at, updated_at
) VALUES ($1, $2, 'thread', $3, $4, $5, $6, 0, 0, NULL, 0, 'active', $7, $7)
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)
`, scopeID, workspaceID, parentScopeID, rootMessageID, rootMessageShortID, "thread:"+scopeID, now)

	mustExec(t, ctx, pool, `
INSERT INTO scope_channel_profiles (
  scope_id, workspace_id, slug, display_name, description, visibility,
  owner_member_id, is_all_channel, created_at, updated_at
) VALUES ($1, $2, $3, $3, '', 'private', NULL, FALSE, $4, $4)
ON DUPLICATE KEY UPDATE
  slug = VALUES(slug),
  display_name = VALUES(display_name),
  updated_at = VALUES(updated_at)
`, scopeID, workspaceID, slug, now)
}

func mustSeedScopeMembership(t *testing.T, ctx context.Context, pool coredb.QueryExecer, scopeID string, memberID string, now time.Time) {
	t.Helper()
	mustExec(t, ctx, pool, `
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
) VALUES ($1, $2, 'public', NULL, 0, 0, 0, 0, 1, NULL, $3, $3, NULL, NULL)
ON DUPLICATE KEY UPDATE
  visibility = VALUES(visibility),
  effective_to_seq = NULL,
  removed_at = NULL,
  archived_at = NULL,
  updated_at = VALUES(updated_at)
`, scopeID, memberID, now)
}

func mustSeedTaskScope(t *testing.T, ctx context.Context, pool coredb.QueryExecer, workspaceID string, taskScopeID string, now time.Time) {
	t.Helper()
	mustExec(t, ctx, pool, `
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_at, updated_at
) VALUES ($1, $2, 'task', NULL, NULL, NULL, $3, 0, 0, NULL, 0, 'active', $4, $4)
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)
`, taskScopeID, workspaceID, "scope/task/"+taskScopeID, now)
}

func mustSeedTaskBoard(t *testing.T, ctx context.Context, pool coredb.QueryExecer, workspaceID string, channelID string, boardID string, now time.Time) {
	t.Helper()
	mustExec(t, ctx, pool, `
INSERT INTO task_boards (id, channel_id, workspace_id, next_task_number, created_at, updated_at)
VALUES ($1, $2, $3, 1, $4, $4)
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)
`, boardID, channelID, workspaceID, now)
}

func mustSeedTaskRow(t *testing.T, ctx context.Context, pool coredb.QueryExecer, workspaceID string, channelID string, boardID string, taskScopeID string, taskID string, creatorMemberID string, now time.Time) {
	t.Helper()
	mustExec(t, ctx, pool, `
INSERT INTO tasks (
  id, task_scope_id, board_id, channel_id, task_number, title, status,
  created_by_member_id, created_by_ref, execution_owner_member_id, suggested_assignee_ref,
  source_thread_parent_channel_id, source_thread_parent_message_id, source_thread_ref,
  description, reviewer_ref, blocked_reason, parent_task_ref, kind,
  created_at, updated_at
) VALUES (
  $1, $2, $3, $4, 1, 'task-board-auth task', 'todo',
  $6, $6, $6, '',
  '', '', '',
  '', '', '', '', 'task',
  $5, $5
)
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)
`, taskID, taskScopeID, boardID, channelID, now, creatorMemberID)
}
