/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
	acceptInvite,
	addMemberRole,
	createChannel,
	createChannelInvite,
	createDmChannel,
	createFriendship,
	createGroupDmChannel,
	createGuild,
	createPermissionOverwrite,
	createRole,
	leaveGuild,
	removeRecipientFromGroupDm,
	sendChannelMessage,
	updateChannel,
	updateRole,
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {getRoles, updateGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
import {
	markChannelAsIndexed,
	markGuildChannelsAsIndexed,
	markUserDmChannelsAsIndexed,
} from '@fluxer/api/src/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {GuildNSFWLevel} from '@fluxer/constants/src/GuildConstants';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';

interface MessageSearchResult {
	messages: Array<{
		id: string;
		channel_id: string;
		content: string;
	}>;
	total: number;
	hits_per_page: number;
	page: number;
}

interface MessageSearchIndexingResponse {
	indexing: true;
}

type MessageSearchResponse = MessageSearchResult | MessageSearchIndexingResponse;

function isSearchResult(response: MessageSearchResponse): response is MessageSearchResult {
	return 'messages' in response;
}

describe('Message Search Permissions', () => {
	let harness: ApiTestHarness;

	beforeEach(async () => {
		harness = await createApiTestHarness({search: 'meilisearch'});
	});

	afterEach(async () => {
		await harness?.shutdown();
	});

	describe('Guild membership permissions', () => {
		test('cannot search in guilds user is not a member of', async () => {
			const owner = await createTestAccount(harness);
			const nonMember = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Private Guild');
			const channelId = guild.system_channel_id!;

			await sendChannelMessage(harness, owner.token, channelId, 'secret guild message');
			await markChannelAsIndexed(harness, channelId);

			await createBuilder(harness, nonMember.token)
				.post('/search/messages')
				.body({
					content: 'secret',
					context_guild_id: guild.id,
				})
				.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_GUILD')
				.execute();
		});

		test('can search in guilds user is a member of', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const channelId = guild.system_channel_id!;

			const invite = await createChannelInvite(harness, owner.token, channelId);
			await acceptInvite(harness, member.token, invite.code);

			await sendChannelMessage(harness, owner.token, channelId, 'searchable guild content');
			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'searchable',
					context_guild_id: guild.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}
			expect(result.messages.length).toBeGreaterThan(0);
			expect(result.messages[0]?.content).toContain('searchable');
		});

		test('after leaving guild, search results exclude that guilds messages', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const channelId = guild.system_channel_id!;

			const invite = await createChannelInvite(harness, owner.token, channelId);
			await acceptInvite(harness, member.token, invite.code);

			await sendChannelMessage(harness, owner.token, channelId, 'member can see this initially');
			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const beforeLeave = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'member can see',
					context_guild_id: guild.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(beforeLeave)) {
				expect.fail('Expected search result but got indexing response');
			}
			expect(beforeLeave.messages.length).toBeGreaterThan(0);

			await leaveGuild(harness, member.token, guild.id);

			await createBuilder(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'member can see',
					context_guild_id: guild.id,
				})
				.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_GUILD')
				.execute();
		});
	});

	describe('Channel permissions', () => {
		test('requires READ_MESSAGE_HISTORY permission to search channel', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const channelId = guild.system_channel_id!;

			await sendChannelMessage(harness, owner.token, channelId, 'restricted content');

			const roles = await getRoles(harness, owner.token, guild.id);
			const everyoneRole = roles.find((r) => r.id === guild.id);

			if (everyoneRole) {
				const permissions = BigInt(everyoneRole.permissions);
				const newPermissions = permissions & ~Permissions.READ_MESSAGE_HISTORY;
				await updateRole(harness, owner.token, guild.id, everyoneRole.id, {
					permissions: newPermissions.toString(),
				});
			}

			const invite = await createChannelInvite(harness, owner.token, channelId);
			await acceptInvite(harness, member.token, invite.code);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'restricted',
					context_channel_id: channelId,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages).toEqual([]);
		});

		test('without READ_MESSAGE_HISTORY, channel messages are excluded from guild search', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const systemChannelId = guild.system_channel_id!;

			const restrictedChannel = await createChannel(
				harness,
				owner.token,
				guild.id,
				'restricted',
				ChannelTypes.GUILD_TEXT,
			);

			await sendChannelMessage(harness, owner.token, restrictedChannel.id, 'hidden from member search');
			await sendChannelMessage(harness, owner.token, systemChannelId, 'visible to member search');

			const invite = await createChannelInvite(harness, owner.token, systemChannelId);
			await acceptInvite(harness, member.token, invite.code);

			await createPermissionOverwrite(harness, owner.token, restrictedChannel.id, member.userId, {
				type: 1,
				allow: '0',
				deny: Permissions.READ_MESSAGE_HISTORY.toString(),
			});

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'search',
					context_guild_id: guild.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const restrictedMessages = result.messages.filter((m) => m.channel_id === restrictedChannel.id);
			const visibleMessages = result.messages.filter((m) => m.channel_id === systemChannelId);

			expect(restrictedMessages.length).toBe(0);
			expect(visibleMessages.length).toBeGreaterThan(0);
		});

		test('private channels only searchable by members with access', async () => {
			const owner = await createTestAccount(harness);
			const allowedMember = await createTestAccount(harness);
			const deniedMember = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const systemChannelId = guild.system_channel_id!;

			const privateChannel = await createChannel(harness, owner.token, guild.id, 'private', ChannelTypes.GUILD_TEXT);

			const roles = await getRoles(harness, owner.token, guild.id);
			const everyoneRole = roles.find((r) => r.id === guild.id);

			if (everyoneRole) {
				await createPermissionOverwrite(harness, owner.token, privateChannel.id, everyoneRole.id, {
					type: 0,
					allow: '0',
					deny: Permissions.VIEW_CHANNEL.toString(),
				});
			}

			await sendChannelMessage(harness, owner.token, privateChannel.id, 'private channel secrets');

			const invite = await createChannelInvite(harness, owner.token, systemChannelId);
			await acceptInvite(harness, allowedMember.token, invite.code);
			await acceptInvite(harness, deniedMember.token, invite.code);

			await createPermissionOverwrite(harness, owner.token, privateChannel.id, allowedMember.userId, {
				type: 1,
				allow: (Permissions.VIEW_CHANNEL | Permissions.READ_MESSAGE_HISTORY).toString(),
				deny: '0',
			});

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const allowedResult = await createBuilder<MessageSearchResponse>(harness, allowedMember.token)
				.post('/search/messages')
				.body({
					content: 'secrets',
					context_guild_id: guild.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			const deniedResult = await createBuilder<MessageSearchResponse>(harness, deniedMember.token)
				.post('/search/messages')
				.body({
					content: 'secrets',
					context_guild_id: guild.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(allowedResult)) {
				expect.fail('Expected search result but got indexing response for allowed user');
			}
			if (!isSearchResult(deniedResult)) {
				expect.fail('Expected search result but got indexing response for denied user');
			}

			expect(allowedResult.messages.length).toBeGreaterThan(0);
			expect(deniedResult.messages.length).toBe(0);
		});
	});

	describe('NSFW filtering', () => {
		test('by default (include_nsfw: false), NSFW channel messages are excluded', async () => {
			const owner = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const systemChannelId = guild.system_channel_id!;

			const nsfwChannel = await createChannel(harness, owner.token, guild.id, 'nsfw-channel', ChannelTypes.GUILD_TEXT);
			await updateChannel(harness, owner.token, nsfwChannel.id, {nsfw: true});

			await sendChannelMessage(harness, owner.token, nsfwChannel.id, 'nsfw unique content here');
			await sendChannelMessage(harness, owner.token, systemChannelId, 'sfw unique content here');

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, owner.token)
				.post('/search/messages')
				.body({
					content: 'unique content',
					context_guild_id: guild.id,
					include_nsfw: false,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const nsfwMessages = result.messages.filter((m) => m.channel_id === nsfwChannel.id);
			const sfwMessages = result.messages.filter((m) => m.channel_id === systemChannelId);

			expect(nsfwMessages.length).toBe(0);
			expect(sfwMessages.length).toBeGreaterThan(0);
		});

		test('with include_nsfw: true, NSFW channel messages are included', async () => {
			const owner = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');

			const nsfwChannel = await createChannel(harness, owner.token, guild.id, 'nsfw-channel', ChannelTypes.GUILD_TEXT);
			await updateChannel(harness, owner.token, nsfwChannel.id, {nsfw: true});

			await sendChannelMessage(harness, owner.token, nsfwChannel.id, 'nsfw searchable message');

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, owner.token)
				.post('/search/messages')
				.body({
					content: 'nsfw searchable',
					context_guild_id: guild.id,
					include_nsfw: true,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages.length).toBeGreaterThan(0);
			expect(result.messages[0]?.channel_id).toBe(nsfwChannel.id);
		});

		test('user must still have access to the NSFW channel', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const systemChannelId = guild.system_channel_id!;

			const nsfwChannel = await createChannel(harness, owner.token, guild.id, 'nsfw-channel', ChannelTypes.GUILD_TEXT);
			await updateChannel(harness, owner.token, nsfwChannel.id, {nsfw: true});

			const roles = await getRoles(harness, owner.token, guild.id);
			const everyoneRole = roles.find((r) => r.id === guild.id);

			if (everyoneRole) {
				await createPermissionOverwrite(harness, owner.token, nsfwChannel.id, everyoneRole.id, {
					type: 0,
					allow: '0',
					deny: Permissions.VIEW_CHANNEL.toString(),
				});
			}

			await sendChannelMessage(harness, owner.token, nsfwChannel.id, 'nsfw restricted content');

			const invite = await createChannelInvite(harness, owner.token, systemChannelId);
			await acceptInvite(harness, member.token, invite.code);

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'nsfw restricted',
					context_guild_id: guild.id,
					include_nsfw: true,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages.length).toBe(0);
		});

		test('age-restricted guild requires include_nsfw: true', async () => {
			const owner = await createTestAccount(harness, {dateOfBirth: '2000-01-01'});

			const guild = await createGuild(harness, owner.token, 'Age Restricted Search Guild');
			const systemChannelId = guild.system_channel_id!;

			await sendChannelMessage(harness, owner.token, systemChannelId, 'age restricted guild searchable message');

			await updateGuild(harness, owner.token, guild.id, {nsfw_level: GuildNSFWLevel.AGE_RESTRICTED});
			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const excluded = await createBuilder<MessageSearchResponse>(harness, owner.token)
				.post('/search/messages')
				.body({
					content: 'age restricted guild searchable',
					context_guild_id: guild.id,
					include_nsfw: false,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(excluded)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(excluded.messages.length).toBe(0);

			const included = await createBuilder<MessageSearchResponse>(harness, owner.token)
				.post('/search/messages')
				.body({
					content: 'age restricted guild searchable',
					context_guild_id: guild.id,
					include_nsfw: true,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(included)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(included.messages.length).toBeGreaterThan(0);
			expect(included.messages.some((m) => m.channel_id === systemChannelId)).toBe(true);
		});

		test('underage user cannot search messages in an age-restricted guild', async () => {
			const owner = await createTestAccount(harness, {dateOfBirth: '2000-01-01'});
			const underageMember = await createTestAccount(harness, {dateOfBirth: '2012-01-01'});

			const guild = await createGuild(harness, owner.token, 'Age Restricted Guild');
			const systemChannelId = guild.system_channel_id!;

			const invite = await createChannelInvite(harness, owner.token, systemChannelId);
			await acceptInvite(harness, underageMember.token, invite.code);

			await sendChannelMessage(harness, owner.token, systemChannelId, 'underage search test message');
			await updateGuild(harness, owner.token, guild.id, {nsfw_level: GuildNSFWLevel.AGE_RESTRICTED});
			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			await createBuilder(harness, underageMember.token)
				.post('/search/messages')
				.body({
					content: 'underage search test',
					context_guild_id: guild.id,
					include_nsfw: true,
				})
				.expect(HTTP_STATUS.FORBIDDEN, APIErrorCodes.NSFW_CONTENT_AGE_RESTRICTED)
				.execute();
		});

		test('scope: all_guilds excludes NSFW channel messages by default', async () => {
			const owner = await createTestAccount(harness, {dateOfBirth: '2000-01-01'});

			const guild = await createGuild(harness, owner.token, 'All Guilds NSFW Filter Guild');
			const systemChannelId = guild.system_channel_id!;

			const nsfwChannel = await createChannel(harness, owner.token, guild.id, 'nsfw-channel', ChannelTypes.GUILD_TEXT);
			await updateChannel(harness, owner.token, nsfwChannel.id, {nsfw: true});

			await sendChannelMessage(harness, owner.token, systemChannelId, 'all guilds sfw unique content');
			await sendChannelMessage(harness, owner.token, nsfwChannel.id, 'all guilds nsfw unique content');

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, owner.token)
				.post('/search/messages')
				.body({
					content: 'all guilds unique content',
					scope: 'all_guilds',
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const nsfwMessages = result.messages.filter((m) => m.channel_id === nsfwChannel.id);
			const sfwMessages = result.messages.filter((m) => m.channel_id === systemChannelId);

			expect(nsfwMessages.length).toBe(0);
			expect(sfwMessages.length).toBeGreaterThan(0);
		});

		test('scope: all_guilds includes NSFW channel messages with include_nsfw: true', async () => {
			const owner = await createTestAccount(harness, {dateOfBirth: '2000-01-01'});

			const guild = await createGuild(harness, owner.token, 'All Guilds NSFW Include Guild');
			const systemChannelId = guild.system_channel_id!;

			const nsfwChannel = await createChannel(harness, owner.token, guild.id, 'nsfw-channel', ChannelTypes.GUILD_TEXT);
			await updateChannel(harness, owner.token, nsfwChannel.id, {nsfw: true});

			await sendChannelMessage(harness, owner.token, systemChannelId, 'all guilds sfw include content');
			await sendChannelMessage(harness, owner.token, nsfwChannel.id, 'all guilds nsfw include content');

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, owner.token)
				.post('/search/messages')
				.body({
					content: 'all guilds include content',
					scope: 'all_guilds',
					include_nsfw: true,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const nsfwMessages = result.messages.filter((m) => m.channel_id === nsfwChannel.id);
			const sfwMessages = result.messages.filter((m) => m.channel_id === systemChannelId);

			expect(nsfwMessages.length).toBeGreaterThan(0);
			expect(sfwMessages.length).toBeGreaterThan(0);
		});
	});

	describe('DM permissions', () => {
		test('can search own DM conversations', async () => {
			const user1 = await createTestAccount(harness);
			const user2 = await createTestAccount(harness);

			await createFriendship(harness, user1, user2);

			const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
			await sendChannelMessage(harness, user1.token, dmChannel.id, 'private dm message search test');

			await markChannelAsIndexed(harness, dmChannel.id);

			const result = await createBuilder<MessageSearchResponse>(harness, user1.token)
				.post('/search/messages')
				.body({
					content: 'dm message search',
					context_channel_id: dmChannel.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages.length).toBeGreaterThan(0);
		});

		test('cannot search other users DM conversations', async () => {
			const user1 = await createTestAccount(harness);
			const user2 = await createTestAccount(harness);
			const outsider = await createTestAccount(harness);

			await createFriendship(harness, user1, user2);

			const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
			await sendChannelMessage(harness, user1.token, dmChannel.id, 'secret dm conversation');

			await markChannelAsIndexed(harness, dmChannel.id);

			await createBuilder(harness, outsider.token)
				.post('/search/messages')
				.body({
					content: 'secret dm',
					context_channel_id: dmChannel.id,
				})
				.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
				.execute();
		});

		test('ignores channel_id when searching current DM context', async () => {
			const userA = await createTestAccount(harness);
			const userB = await createTestAccount(harness);
			const userC = await createTestAccount(harness);

			await createFriendship(harness, userA, userB);
			const dmAB = await createDmChannel(harness, userA.token, userB.userId);
			await sendChannelMessage(harness, userA.token, dmAB.id, 'public message from A to B');
			await markChannelAsIndexed(harness, dmAB.id);

			await createFriendship(harness, userB, userC);
			const dmBC = await createDmChannel(harness, userB.token, userC.userId);
			await sendChannelMessage(harness, userB.token, dmBC.id, 'top secret message from B to C');
			await markChannelAsIndexed(harness, dmBC.id);

			const result = await createBuilder<MessageSearchResponse>(harness, userA.token)
				.post('/search/messages')
				.body({
					content: 'secret',
					context_channel_id: dmAB.id,
					channel_id: [dmBC.id],
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages.length).toBe(0);
		});

		test('group DMs only searchable by participants', async () => {
			const owner = await createTestAccount(harness);
			const participant1 = await createTestAccount(harness);
			const participant2 = await createTestAccount(harness);
			const outsider = await createTestAccount(harness);

			await createFriendship(harness, owner, participant1);
			await createFriendship(harness, owner, participant2);

			const groupDm = await createGroupDmChannel(harness, owner.token, [participant1.userId, participant2.userId]);
			await sendChannelMessage(harness, owner.token, groupDm.id, 'group dm secret message');

			await markChannelAsIndexed(harness, groupDm.id);

			const participantResult = await createBuilder<MessageSearchResponse>(harness, participant1.token)
				.post('/search/messages')
				.body({
					content: 'group dm secret',
					context_channel_id: groupDm.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(participantResult)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(participantResult.messages.length).toBeGreaterThan(0);

			await createBuilder(harness, outsider.token)
				.post('/search/messages')
				.body({
					content: 'group dm secret',
					context_channel_id: groupDm.id,
				})
				.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
				.execute();
		});

		test('after leaving group DM, user cannot search that group', async () => {
			const owner = await createTestAccount(harness);
			const participant = await createTestAccount(harness);
			const otherParticipant = await createTestAccount(harness);

			await createFriendship(harness, owner, participant);
			await createFriendship(harness, owner, otherParticipant);

			const groupDm = await createGroupDmChannel(harness, owner.token, [participant.userId, otherParticipant.userId]);
			await sendChannelMessage(harness, owner.token, groupDm.id, 'group dm content before leave');

			await markChannelAsIndexed(harness, groupDm.id);

			const beforeLeave = await createBuilder<MessageSearchResponse>(harness, participant.token)
				.post('/search/messages')
				.body({
					content: 'before leave',
					context_channel_id: groupDm.id,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(beforeLeave)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(beforeLeave.messages.length).toBeGreaterThan(0);

			await removeRecipientFromGroupDm(harness, owner.token, groupDm.id, participant.userId);

			await createBuilder(harness, participant.token)
				.post('/search/messages')
				.body({
					content: 'before leave',
					context_channel_id: groupDm.id,
				})
				.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
				.execute();
		});
	});

	describe('Scope-based access', () => {
		test('scope: all_guilds only returns guild messages user has access to', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild1 = await createGuild(harness, owner.token, 'Guild One');
			const guild2 = await createGuild(harness, owner.token, 'Guild Two');

			await sendChannelMessage(harness, owner.token, guild1.system_channel_id!, 'guild one searchable message');
			await sendChannelMessage(harness, owner.token, guild2.system_channel_id!, 'guild two searchable message');

			const invite = await createChannelInvite(harness, owner.token, guild1.system_channel_id!);
			await acceptInvite(harness, member.token, invite.code);

			await markGuildChannelsAsIndexed(harness, owner.token, guild1.id);
			await markGuildChannelsAsIndexed(harness, owner.token, guild2.id);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'searchable message',
					scope: 'all_guilds',
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const guild1Messages = result.messages.filter((m) => m.channel_id === guild1.system_channel_id);
			const guild2Messages = result.messages.filter((m) => m.channel_id === guild2.system_channel_id);

			expect(guild1Messages.length).toBeGreaterThan(0);
			expect(guild2Messages.length).toBe(0);
		});

		test('scope: all_dms only returns DMs user is participant in', async () => {
			const user1 = await createTestAccount(harness);
			const user2 = await createTestAccount(harness);
			const user3 = await createTestAccount(harness);
			const user4 = await createTestAccount(harness);

			await createFriendship(harness, user1, user2);
			await createFriendship(harness, user3, user4);

			const dmChannel12 = await createDmChannel(harness, user1.token, user2.userId);
			const dmChannel34 = await createDmChannel(harness, user3.token, user4.userId);

			await sendChannelMessage(harness, user1.token, dmChannel12.id, 'dm searchable content user12');
			await sendChannelMessage(harness, user3.token, dmChannel34.id, 'dm searchable content user34');

			await markChannelAsIndexed(harness, dmChannel12.id);
			await markChannelAsIndexed(harness, dmChannel34.id);

			const result = await createBuilder<MessageSearchResponse>(harness, user1.token)
				.post('/search/messages')
				.body({
					content: 'dm searchable content',
					scope: 'all_dms',
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const user12Messages = result.messages.filter((m) => m.channel_id === dmChannel12.id);
			const user34Messages = result.messages.filter((m) => m.channel_id === dmChannel34.id);

			expect(user12Messages.length).toBeGreaterThan(0);
			expect(user34Messages.length).toBe(0);
		});

		test('scope: open_dms returns only currently open DMs', async () => {
			const user1 = await createTestAccount(harness);
			const user2 = await createTestAccount(harness);
			const user3 = await createTestAccount(harness);

			await createFriendship(harness, user1, user2);
			await createFriendship(harness, user1, user3);

			const openDm = await createDmChannel(harness, user1.token, user2.userId);

			await sendChannelMessage(harness, user1.token, openDm.id, 'open dm searchable message');

			await markUserDmChannelsAsIndexed(harness, user1.token);

			const result = await createBuilder<MessageSearchResponse>(harness, user1.token)
				.post('/search/messages')
				.body({
					content: 'open dm searchable',
					scope: 'open_dms',
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages.length).toBeGreaterThan(0);
			expect(result.messages[0]?.channel_id).toBe(openDm.id);
		});

		test('scope: all combines guilds and DMs correctly', async () => {
			const user = await createTestAccount(harness);
			const friend = await createTestAccount(harness);

			await createFriendship(harness, user, friend);

			const guild = await createGuild(harness, user.token, 'Test Guild');
			const dmChannel = await createDmChannel(harness, user.token, friend.userId);

			await sendChannelMessage(harness, user.token, guild.system_channel_id!, 'guild combined searchable');
			await sendChannelMessage(harness, user.token, dmChannel.id, 'dm combined searchable');

			await markGuildChannelsAsIndexed(harness, user.token, guild.id);
			await markChannelAsIndexed(harness, dmChannel.id);

			const result = await createBuilder<MessageSearchResponse>(harness, user.token)
				.post('/search/messages')
				.body({
					content: 'combined searchable',
					scope: 'all',
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const guildMessages = result.messages.filter((m) => m.channel_id === guild.system_channel_id);
			const dmMessages = result.messages.filter((m) => m.channel_id === dmChannel.id);

			expect(guildMessages.length).toBeGreaterThan(0);
			expect(dmMessages.length).toBeGreaterThan(0);
		});

		test('scope: open_dms_and_all_guilds works correctly', async () => {
			const user = await createTestAccount(harness);
			const friend = await createTestAccount(harness);

			await createFriendship(harness, user, friend);

			const guild = await createGuild(harness, user.token, 'Test Guild');
			const openDm = await createDmChannel(harness, user.token, friend.userId);

			await sendChannelMessage(harness, user.token, guild.system_channel_id!, 'guild open_dms_guilds searchable');
			await sendChannelMessage(harness, user.token, openDm.id, 'dm open_dms_guilds searchable');

			await markGuildChannelsAsIndexed(harness, user.token, guild.id);
			await markChannelAsIndexed(harness, openDm.id);

			const result = await createBuilder<MessageSearchResponse>(harness, user.token)
				.post('/search/messages')
				.body({
					content: 'open_dms_guilds searchable',
					scope: 'open_dms_and_all_guilds',
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const guildMessages = result.messages.filter((m) => m.channel_id === guild.system_channel_id);
			const dmMessages = result.messages.filter((m) => m.channel_id === openDm.id);

			expect(guildMessages.length).toBeGreaterThan(0);
			expect(dmMessages.length).toBeGreaterThan(0);
		});

		test('scope: all_guilds excludes inaccessible channels within accessible guilds', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const systemChannelId = guild.system_channel_id!;

			const privateChannel = await createChannel(harness, owner.token, guild.id, 'private', ChannelTypes.GUILD_TEXT);

			const roles = await getRoles(harness, owner.token, guild.id);
			const everyoneRole = roles.find((r) => r.id === guild.id);

			if (everyoneRole) {
				await createPermissionOverwrite(harness, owner.token, privateChannel.id, everyoneRole.id, {
					type: 0,
					allow: '0',
					deny: (Permissions.VIEW_CHANNEL | Permissions.READ_MESSAGE_HISTORY).toString(),
				});
			}

			await sendChannelMessage(harness, owner.token, systemChannelId, 'public all_guilds searchable');
			await sendChannelMessage(harness, owner.token, privateChannel.id, 'private all_guilds searchable');

			const invite = await createChannelInvite(harness, owner.token, systemChannelId);
			await acceptInvite(harness, member.token, invite.code);

			await markGuildChannelsAsIndexed(harness, owner.token, guild.id);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'all_guilds searchable',
					scope: 'all_guilds',
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			const publicMessages = result.messages.filter((m) => m.channel_id === systemChannelId);
			const privateMessages = result.messages.filter((m) => m.channel_id === privateChannel.id);

			expect(publicMessages.length).toBeGreaterThan(0);
			expect(privateMessages.length).toBe(0);
		});
	});

	describe('Role-based channel access', () => {
		test('role with VIEW_CHANNEL but without READ_MESSAGE_HISTORY cannot search', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const channelId = guild.system_channel_id!;

			const viewOnlyRole = await createRole(harness, owner.token, guild.id, {
				name: 'View Only',
				permissions: Permissions.VIEW_CHANNEL.toString(),
			});

			await sendChannelMessage(harness, owner.token, channelId, 'restricted history content');

			const roles = await getRoles(harness, owner.token, guild.id);
			const everyoneRole = roles.find((r) => r.id === guild.id);

			if (everyoneRole) {
				const permissions = BigInt(everyoneRole.permissions);
				const newPermissions = permissions & ~(Permissions.VIEW_CHANNEL | Permissions.READ_MESSAGE_HISTORY);
				await updateRole(harness, owner.token, guild.id, everyoneRole.id, {
					permissions: newPermissions.toString(),
				});
			}

			const invite = await createChannelInvite(harness, owner.token, channelId);
			await acceptInvite(harness, member.token, invite.code);

			await addMemberRole(harness, owner.token, guild.id, member.userId, viewOnlyRole.id);

			await markChannelAsIndexed(harness, channelId);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'restricted history',
					context_channel_id: channelId,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages).toEqual([]);
		});

		test('channel permission overwrite can grant search access', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const channelId = guild.system_channel_id!;

			await sendChannelMessage(harness, owner.token, channelId, 'overwrite granted content');

			const roles = await getRoles(harness, owner.token, guild.id);
			const everyoneRole = roles.find((r) => r.id === guild.id);

			if (everyoneRole) {
				const permissions = BigInt(everyoneRole.permissions);
				const newPermissions = permissions & ~Permissions.READ_MESSAGE_HISTORY;
				await updateRole(harness, owner.token, guild.id, everyoneRole.id, {
					permissions: newPermissions.toString(),
				});
			}

			const invite = await createChannelInvite(harness, owner.token, channelId);
			await acceptInvite(harness, member.token, invite.code);

			await createPermissionOverwrite(harness, owner.token, channelId, member.userId, {
				type: 1,
				allow: Permissions.READ_MESSAGE_HISTORY.toString(),
				deny: '0',
			});

			await markChannelAsIndexed(harness, channelId);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'overwrite granted',
					context_channel_id: channelId,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages.length).toBeGreaterThan(0);
		});

		test('channel permission overwrite can deny search access despite role permission', async () => {
			const owner = await createTestAccount(harness);
			const member = await createTestAccount(harness);

			const guild = await createGuild(harness, owner.token, 'Test Guild');
			const channelId = guild.system_channel_id!;

			await sendChannelMessage(harness, owner.token, channelId, 'overwrite denied content');

			const invite = await createChannelInvite(harness, owner.token, channelId);
			await acceptInvite(harness, member.token, invite.code);

			await createPermissionOverwrite(harness, owner.token, channelId, member.userId, {
				type: 1,
				allow: '0',
				deny: Permissions.READ_MESSAGE_HISTORY.toString(),
			});

			await markChannelAsIndexed(harness, channelId);

			const result = await createBuilder<MessageSearchResponse>(harness, member.token)
				.post('/search/messages')
				.body({
					content: 'overwrite denied',
					context_channel_id: channelId,
				})
				.expect(HTTP_STATUS.OK)
				.execute();

			if (!isSearchResult(result)) {
				expect.fail('Expected search result but got indexing response');
			}

			expect(result.messages).toEqual([]);
		});
	});
});
