import { mockInstance } from '@n8n/backend-test-utils';
import { Container } from '@n8n/di';
import { GLOBAL_MEMBER_SCOPES, type Scope } from '@n8n/permissions';
import type { Response } from 'express';
import type { Mock, Mocked } from 'vitest';

import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { DataTableAggregateService } from '@/modules/data-table/data-table-aggregate.service';
import { DataTableService } from '@/modules/data-table/data-table.service';
import { DataTableNotFoundError } from '@/modules/data-table/errors/data-table-not-found.error';
import type { DataTableRequest } from '@/public-api/types';
import * as middlewares from '@/public-api/v1/shared/middlewares/global.middleware';
import { ProjectNotFoundError } from '@/services/project.service.ee';

// Mock middleware before requiring handler
const mockMiddleware = vi.fn(async (_req, _res, next) => next()) as any;
vi.spyOn(middlewares, 'publicApiScope').mockReturnValue(mockMiddleware);
vi.spyOn(middlewares, 'projectScope').mockReturnValue(mockMiddleware);
vi.spyOn(middlewares, 'validCursor').mockReturnValue(mockMiddleware);

// Loaded after the middleware spies above are installed; typed loosely so the
// suite can invoke individual route entries by index.
let mainHandler: Record<string, Array<(...args: unknown[]) => unknown>>;
let handler: Record<string, Array<(...args: unknown[]) => unknown>>;

beforeAll(async () => {
	mainHandler = (await import('../data-tables.handler.js')) as unknown as typeof mainHandler;
	handler = (await import('../data-tables.rows.handler.js')) as unknown as typeof handler;
});

describe('DataTable Handler', () => {
	let mockDataTableService: Mocked<DataTableService>;
	let mockDataTableAggregateService: Mocked<DataTableAggregateService>;
	let mockResponse: Partial<Response>;

	const projectId = 'test-project-id';
	const dataTableId = 'test-data-table-id';
	const userId = 'test-user-id';

	const makeUser = (scopeSlugs: Scope[] = GLOBAL_MEMBER_SCOPES) => ({
		id: userId,
		role: { slug: 'global:member', scopes: scopeSlugs.map((slug) => ({ slug })) },
	});

	beforeEach(() => {
		mockDataTableService = mockInstance(DataTableService);
		mockDataTableAggregateService = mockInstance(DataTableAggregateService);

		vi.spyOn(Container, 'get').mockImplementation((serviceClass) => {
			if (serviceClass === DataTableService) {
				return mockDataTableService;
			}
			if (serviceClass === DataTableAggregateService) {
				return mockDataTableAggregateService;
			}
			return {};
		});

		mockDataTableService.getProjectIdForDataTable.mockResolvedValue(projectId);

		mockResponse = {
			json: vi.fn().mockReturnThis(),
			status: vi.fn().mockReturnThis(),
			send: vi.fn().mockReturnThis(),
		};
	});

	afterEach(() => {
		vi.clearAllMocks();
	});

	describe('createDataTable', () => {
		it('should create in personal project when no projectId provided', async () => {
			const req = {
				body: { name: 'test-table', columns: [{ name: 'col1', type: 'string' }] },
				user: makeUser(),
			} as unknown as DataTableRequest.Create;
			mockDataTableService.resolveOwningProjectId.mockResolvedValue(projectId);
			mockDataTableService.createDataTable.mockResolvedValue({
				id: dataTableId,
				name: 'test-table',
				columns: [],
				project: { id: projectId },
			} as never);

			await mainHandler.createDataTable[1](req, mockResponse as Response);

			expect(mockDataTableService.resolveOwningProjectId).toHaveBeenCalledWith(req.user, undefined);
			expect(mockDataTableService.createDataTable).toHaveBeenCalledWith(projectId, {
				name: 'test-table',
				columns: [{ name: 'col1', type: 'string' }],
				fileId: undefined,
				hasHeaders: undefined,
			});
			expect(mockResponse.status).toHaveBeenCalledWith(201);
		});

		it('should map a missing project to BadRequestError', async () => {
			const req = {
				body: { name: 'test-table', columns: [], projectId },
				user: makeUser(),
			} as unknown as DataTableRequest.Create;
			mockDataTableService.resolveOwningProjectId.mockRejectedValue(
				new ProjectNotFoundError(projectId),
			);

			const handlerFn = mainHandler.createDataTable[1];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			expect(caught).toBeInstanceOf(BadRequestError);
			expect(caught).toMatchObject({
				message: `Project with ID "${projectId}" not found`,
				httpStatusCode: 400,
			});
		});
	});

	describe('listDataTables', () => {
		it('should list via the aggregate service for a regular user', async () => {
			const req = { query: {}, user: makeUser() } as unknown as DataTableRequest.List;
			mockDataTableAggregateService.getManyAndCount.mockResolvedValue({
				data: [],
				count: 0,
			} as never);

			await mainHandler.listDataTables[2](req, mockResponse as Response);

			expect(mockDataTableAggregateService.getManyAndCount).toHaveBeenCalledWith(
				req.user,
				expect.objectContaining({ skip: 0, take: 100 }),
			);
		});

		it('should list via the aggregate service for a user with dataTable:listProject', async () => {
			const req = {
				query: {},
				user: makeUser(['dataTable:listProject']),
			} as unknown as DataTableRequest.List;
			mockDataTableAggregateService.getManyAndCount.mockResolvedValue({
				data: [],
				count: 0,
			} as never);

			await mainHandler.listDataTables[2](req, mockResponse as Response);

			expect(mockDataTableAggregateService.getManyAndCount).toHaveBeenCalledWith(
				req.user,
				expect.objectContaining({ skip: 0, take: 100, filter: undefined }),
			);
		});
	});

	describe('getDataTableRows', () => {
		it('should retrieve rows successfully', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				query: { offset: '0', limit: '100' },
				user: { id: userId },
			} as unknown as DataTableRequest.GetRows;

			const mockRows = [
				{ id: 1, name: 'Test Row 1', createdAt: new Date(), updatedAt: new Date() },
				{ id: 2, name: 'Test Row 2', createdAt: new Date(), updatedAt: new Date() },
			];

			mockDataTableService.getManyRowsAndCount.mockResolvedValue({
				data: mockRows,
				count: 2,
			});

			// Act
			await handler.getDataTableRows[3](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.getProjectIdForDataTable).toHaveBeenCalledWith(dataTableId);
			expect(mockDataTableService.getManyRowsAndCount).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				{ skip: 0, take: 100, filter: undefined, sortBy: undefined, search: undefined },
			);
			const callArg = (mockResponse.json as Mock).mock.calls[0][0];
			expect(callArg).toHaveProperty('data', mockRows);
			expect(callArg).toHaveProperty('nextCursor');
		});

		it('should handle filter, sortBy, and search parameters', async () => {
			// Arrange
			const filterStr =
				'{"type":"and","filters":[{"columnName":"status","condition":"eq","value":"active"}]}';
			const req = {
				params: { dataTableId },
				query: {
					offset: '10',
					limit: '50',
					filter: filterStr,
					sortBy: 'createdAt:desc',
					search: 'test',
				},
				user: { id: userId },
			} as unknown as DataTableRequest.GetRows;

			mockDataTableService.getManyRowsAndCount.mockResolvedValue({
				data: [],
				count: 0,
			});

			// Act
			await handler.getDataTableRows[3](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.getManyRowsAndCount).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				{
					skip: 10,
					take: 50,
					filter: JSON.parse(filterStr),
					sortBy: ['createdAt', 'DESC'],
					search: 'test',
				},
			);
		});

		it('should throw NotFoundError when data table not found', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				query: { offset: '0', limit: '100' },
				user: { id: userId },
			} as unknown as DataTableRequest.GetRows;

			mockDataTableService.getProjectIdForDataTable.mockRejectedValue(
				new DataTableNotFoundError(dataTableId),
			);

			// Act
			const handlerFn = handler.getDataTableRows[3];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({
				message: expect.stringContaining(dataTableId),
				httpStatusCode: 404,
			});
		});

		it('should throw BadRequestError for validation errors', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				query: { offset: '0', limit: '100', filter: 'invalid-json' },
				user: { id: userId },
			} as unknown as DataTableRequest.GetRows;

			// Act
			const handlerFn = handler.getDataTableRows[3];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(BadRequestError);
			expect(caught).toMatchObject({
				message: expect.stringContaining('Invalid'),
				httpStatusCode: 400,
			});
		});
	});

	describe('insertDataTableRows', () => {
		it('should insert rows and return count', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					data: [{ name: 'New Row' }],
					returnType: 'count',
				},
				user: { id: userId },
			} as unknown as DataTableRequest.InsertRows;

			mockDataTableService.insertRows.mockResolvedValue({ count: 1 } as any);

			// Act
			await handler.insertDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.insertRows).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				[{ name: 'New Row' }],
				'count',
			);
			expect(mockResponse.json).toHaveBeenCalledWith({ count: 1 });
		});

		it('should insert rows and return IDs', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					data: [{ name: 'Row 1' }, { name: 'Row 2' }],
					returnType: 'id',
				},
				user: { id: userId },
			} as unknown as DataTableRequest.InsertRows;

			mockDataTableService.insertRows.mockResolvedValue([1, 2] as any);

			// Act
			await handler.insertDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.insertRows).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				[{ name: 'Row 1' }, { name: 'Row 2' }],
				'id',
			);
			expect(mockResponse.json).toHaveBeenCalledWith([1, 2]);
		});

		it('should insert rows and return full rows', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					data: [{ name: 'Test' }],
					returnType: 'all',
				},
				user: { id: userId },
			} as unknown as DataTableRequest.InsertRows;

			const mockRow = { id: 1, name: 'Test', createdAt: new Date(), updatedAt: new Date() };
			mockDataTableService.insertRows.mockResolvedValue([mockRow]);

			// Act
			await handler.insertDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockResponse.json).toHaveBeenCalledWith([mockRow]);
		});

		it('should throw NotFoundError when data table not found', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: { data: [{ name: 'Test' }], returnType: 'count' },
				user: { id: userId },
			} as unknown as DataTableRequest.InsertRows;

			mockDataTableService.getProjectIdForDataTable.mockRejectedValue(
				new DataTableNotFoundError(dataTableId),
			);

			// Act
			const handlerFn = handler.insertDataTableRows[2];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({ httpStatusCode: 404 });
		});
	});

	describe('updateDataTableRows', () => {
		it('should update rows and return true when returnData is false', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
					data: { status: 'updated' },
					returnData: false,
					dryRun: false,
				},
				user: { id: userId },
			} as unknown as DataTableRequest.UpdateRows;

			mockDataTableService.updateRows.mockResolvedValue(true);

			// Act
			await handler.updateDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.updateRows).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				{
					filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
					data: { status: 'updated' },
				},
				false,
				false,
			);
			expect(mockResponse.json).toHaveBeenCalledWith(true);
		});

		it('should update rows and return updated rows when returnData is true', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
					data: { status: 'updated' },
					returnData: true,
					dryRun: false,
				},
				user: makeUser(['dataTable:readRow']),
			} as unknown as DataTableRequest.UpdateRows;

			const mockRow = { id: 1, status: 'updated', createdAt: new Date(), updatedAt: new Date() };
			mockDataTableService.updateRows.mockResolvedValue([mockRow] as any);

			// Act
			await handler.updateDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockResponse.json).toHaveBeenCalledWith([mockRow]);
		});

		it('should support dry run mode', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					filter: {
						type: 'and',
						filters: [{ columnName: 'id', condition: 'eq', value: 1 }],
					},
					data: { status: 'test' },
					returnData: true,
					dryRun: true,
				},
				user: makeUser(['dataTable:readRow']),
			} as unknown as DataTableRequest.UpdateRows;

			const mockDryRunResult = [
				{ id: 1, status: 'old', dryRunState: 'before' },
				{ id: 1, status: 'test', dryRunState: 'after' },
			];
			mockDataTableService.updateRows.mockResolvedValue(mockDryRunResult as any);

			// Act
			await handler.updateDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.updateRows).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				expect.any(Object),
				true,
				true,
			);
		});
	});

	describe('upsertDataTableRow', () => {
		it('should upsert row and return true when returnData is false', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					filter: {
						type: 'and',
						filters: [{ columnName: 'email', condition: 'eq', value: 'test@example.com' }],
					},
					data: { email: 'test@example.com', name: 'Test User' },
					returnData: false,
					dryRun: false,
				},
				user: { id: userId },
			} as unknown as DataTableRequest.UpsertRow;

			mockDataTableService.upsertRow.mockResolvedValue(true);

			// Act
			await handler.upsertDataTableRow[2](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.upsertRow).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				expect.objectContaining({
					filter: expect.any(Object),
					data: { email: 'test@example.com', name: 'Test User' },
				}),
				false,
				false,
			);
			expect(mockResponse.json).toHaveBeenCalledWith(true);
		});

		it('should upsert row and return upserted row when returnData is true', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				body: {
					filter: {
						type: 'and',
						filters: [{ columnName: 'email', condition: 'eq', value: 'test@example.com' }],
					},
					data: { email: 'test@example.com', name: 'Test User' },
					returnData: true,
					dryRun: false,
				},
				user: makeUser(['dataTable:readRow']),
			} as unknown as DataTableRequest.UpsertRow;

			const mockRow = {
				id: 1,
				email: 'test@example.com',
				name: 'Test User',
				createdAt: new Date(),
				updatedAt: new Date(),
			};
			mockDataTableService.upsertRow.mockResolvedValue(mockRow as any);

			// Act
			await handler.upsertDataTableRow[2](req, mockResponse as Response);

			// Assert
			expect(mockResponse.json).toHaveBeenCalledWith(mockRow);
		});
	});

	describe('deleteDataTableRows', () => {
		it('should delete rows and return true when returnData is false', async () => {
			// Arrange
			const filterStr = JSON.stringify({
				type: 'and',
				filters: [{ columnName: 'status', condition: 'eq', value: 'archived' }],
			});
			const req = {
				params: { dataTableId },
				query: {
					filter: filterStr,
					returnData: 'false',
					dryRun: 'false',
				},
				user: { id: userId },
			} as unknown as DataTableRequest.DeleteRows;

			mockDataTableService.deleteRows.mockResolvedValue(true);

			// Act
			await handler.deleteDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.deleteRows).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				{
					filter: JSON.parse(filterStr),
				},
				false,
				false,
			);
			expect(mockResponse.json).toHaveBeenCalledWith(true);
		});

		it('should delete rows and return deleted rows when returnData is true', async () => {
			// Arrange
			const filterStr = JSON.stringify({
				type: 'and',
				filters: [{ columnName: 'id', condition: 'eq', value: 1 }],
			});
			const req = {
				params: { dataTableId },
				query: {
					filter: filterStr,
					returnData: 'true',
					dryRun: 'false',
				},
				user: makeUser(['dataTable:readRow']),
			} as unknown as DataTableRequest.DeleteRows;

			const mockRow = { id: 1, name: 'Deleted', createdAt: new Date(), updatedAt: new Date() };
			mockDataTableService.deleteRows.mockResolvedValue([mockRow] as any);

			// Act
			await handler.deleteDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockResponse.json).toHaveBeenCalledWith([mockRow]);
		});

		it('should throw BadRequestError when filter is missing', async () => {
			// Arrange
			const req = {
				params: { dataTableId },
				query: {},
				user: { id: userId },
			} as unknown as DataTableRequest.DeleteRows;

			// Act
			const handlerFn = handler.deleteDataTableRows[2];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(BadRequestError);
			expect(caught).toMatchObject({
				message: 'Required',
				httpStatusCode: 400,
			});
		});

		it('should support dry run mode', async () => {
			// Arrange
			const filterStr = JSON.stringify({
				type: 'and',
				filters: [{ columnName: 'status', condition: 'eq', value: 'test' }],
			});
			const req = {
				params: { dataTableId },
				query: {
					filter: filterStr,
					returnData: 'true',
					dryRun: 'true',
				},
				user: makeUser(['dataTable:readRow']),
			} as unknown as DataTableRequest.DeleteRows;

			const mockRows = [{ id: 1, status: 'test', createdAt: new Date(), updatedAt: new Date() }];
			mockDataTableService.deleteRows.mockResolvedValue(mockRows as any);

			// Act
			await handler.deleteDataTableRows[2](req, mockResponse as Response);

			// Assert
			expect(mockDataTableService.deleteRows).toHaveBeenCalledWith(
				dataTableId,
				projectId,
				expect.objectContaining({ filter: expect.any(Object) }),
				true,
				true,
			);
		});
	});

	describe('Security - Cross-Project Access', () => {
		const otherUserDataTableId = 'other-user-data-table-id';

		it('should throw NotFoundError when trying to get rows from another users data table', async () => {
			// Arrange
			const req = {
				params: { dataTableId: otherUserDataTableId },
				query: { offset: '0', limit: '100' },
				user: { id: userId },
			} as unknown as DataTableRequest.GetRows;

			// The data table belongs to another project, so resolving project id throws
			mockDataTableService.getProjectIdForDataTable.mockRejectedValue(
				new DataTableNotFoundError(otherUserDataTableId),
			);

			// Act
			const handlerFn = handler.getDataTableRows[3];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(mockDataTableService.getProjectIdForDataTable).toHaveBeenCalledWith(
				otherUserDataTableId,
			);
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({
				message: expect.stringContaining(otherUserDataTableId),
				httpStatusCode: 404,
			});
		});

		it('should throw NotFoundError when trying to insert rows into another users data table', async () => {
			// Arrange
			const req = {
				params: { dataTableId: otherUserDataTableId },
				body: {
					data: [{ name: 'Malicious Row' }],
					returnType: 'count',
				},
				user: { id: userId },
			} as unknown as DataTableRequest.InsertRows;

			mockDataTableService.insertRows.mockRejectedValue(
				new DataTableNotFoundError(otherUserDataTableId),
			);

			// Act
			const handlerFn = handler.insertDataTableRows[2];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({
				message: expect.stringContaining(otherUserDataTableId),
				httpStatusCode: 404,
			});
		});

		it('should throw NotFoundError when trying to update rows in another users data table', async () => {
			// Arrange
			const req = {
				params: { dataTableId: otherUserDataTableId },
				body: {
					filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
					data: { status: 'hacked' },
					returnData: false,
					dryRun: false,
				},
				user: { id: userId },
			} as unknown as DataTableRequest.UpdateRows;

			mockDataTableService.updateRows.mockRejectedValue(
				new DataTableNotFoundError(otherUserDataTableId),
			);

			// Act
			const handlerFn = handler.updateDataTableRows[2];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({
				message: expect.stringContaining(otherUserDataTableId),
				httpStatusCode: 404,
			});
		});

		it('should throw NotFoundError when trying to upsert row in another users data table', async () => {
			// Arrange
			const req = {
				params: { dataTableId: otherUserDataTableId },
				body: {
					filter: {
						type: 'and',
						filters: [{ columnName: 'email', condition: 'eq', value: 'malicious@example.com' }],
					},
					data: { email: 'malicious@example.com', name: 'Hacker' },
					returnData: false,
					dryRun: false,
				},
				user: { id: userId },
			} as unknown as DataTableRequest.UpsertRow;

			mockDataTableService.upsertRow.mockRejectedValue(
				new DataTableNotFoundError(otherUserDataTableId),
			);

			// Act
			const handlerFn = handler.upsertDataTableRow[2];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({
				message: expect.stringContaining(otherUserDataTableId),
				httpStatusCode: 404,
			});
		});

		it('should throw NotFoundError when trying to delete rows from another users data table', async () => {
			// Arrange
			const filterStr = JSON.stringify({
				type: 'and',
				filters: [{ columnName: 'id', condition: 'eq', value: 1 }],
			});
			const req = {
				params: { dataTableId: otherUserDataTableId },
				query: {
					filter: filterStr,
					returnData: 'false',
					dryRun: 'false',
				},
				user: { id: userId },
			} as unknown as DataTableRequest.DeleteRows;

			mockDataTableService.deleteRows.mockRejectedValue(
				new DataTableNotFoundError(otherUserDataTableId),
			);

			// Act
			const handlerFn = handler.deleteDataTableRows[2];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({
				message: expect.stringContaining(otherUserDataTableId),
				httpStatusCode: 404,
			});
		});

		it('should not leak information about data table existence in error messages', async () => {
			// Arrange
			const nonExistentDataTableId = 'non-existent-table-id';
			const req = {
				params: { dataTableId: nonExistentDataTableId },
				query: { offset: '0', limit: '100' },
				user: { id: userId },
			} as unknown as DataTableRequest.GetRows;

			mockDataTableService.getProjectIdForDataTable.mockRejectedValue(
				new DataTableNotFoundError(nonExistentDataTableId),
			);

			// Act
			const handlerFn = handler.getDataTableRows[3];
			let caught: unknown;
			try {
				await handlerFn(req, mockResponse as Response);
			} catch (error) {
				caught = error;
			}

			// Assert
			// The error message should be the same whether:
			// 1. The table doesn't exist at all
			// 2. The table exists but belongs to another user's project
			// This prevents information leakage
			expect(caught).toBeInstanceOf(NotFoundError);
			expect(caught).toMatchObject({
				message: expect.stringContaining(nonExistentDataTableId),
				httpStatusCode: 404,
			});
		});
	});
});
