import {
	createTeamProject,
	getPersonalProject,
	linkUserToProject,
	createWorkflow,
	createActiveWorkflow,
	setActiveVersion,
	createWorkflowWithHistory,
	shareWorkflowWithProjects,
	shareWorkflowWithUsers,
	randomCredentialPayload,
	testDb,
	mockInstance,
} from '@n8n/backend-test-utils';
import type {
	User,
	ListQueryDb,
	WorkflowFolderUnionFull,
	Role,
	WorkflowHistory,
	WorkflowEntity,
} from '@n8n/db';
import {
	ProjectRepository,
	WorkflowHistoryRepository,
	SharedWorkflowRepository,
	WorkflowRepository,
	WorkflowPublishHistoryRepository,
} from '@n8n/db';
import { Container } from '@n8n/di';
import type { Scope } from '@n8n/permissions';
import { WorkflowValidationService } from '@/workflows/workflow-validation.service';
import { createFolder } from '@test-integration/db/folders';
import { DateTime } from 'luxon';
import { PROJECT_ROOT, type INode, type IPinData, type IWorkflowBase } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';

import { saveCredential } from '../shared/db/credentials';
import { createCustomRoleWithScopeSlugs, cleanupRolesAndScopes } from '../shared/db/roles';
import { assignTagToWorkflow, createTag } from '../shared/db/tags';
import { createChatUser, createManyUsers, createMember, createOwner } from '../shared/db/users';
import { createWorkflowHistoryItem } from '../shared/db/workflow-history';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
import { makeWorkflow, MOCK_PINDATA } from '../shared/utils/';

import { ActiveWorkflowManager } from '@/active-workflow-manager';
import { CollaborationService } from '@/collaboration/collaboration.service';
import { EventService } from '@/events/event.service';
import { License } from '@/license';
import { ProjectService } from '@/services/project.service.ee';
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';

let owner: User;
let member: User;
let anotherMember: User;

let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
const testServer = utils.setupTestServer({
	endpointGroups: ['workflows'],
	enabledFeatures: ['feat:sharing'],
	quotas: {
		'quota:maxTeamProjects': -1,
	},
});

const { objectContaining, arrayContaining, any } = expect;

const activeWorkflowManagerLike = mockInstance(ActiveWorkflowManager);
const workflowValidationService = mockInstance(WorkflowValidationService);
const collaborationService = mockInstance(CollaborationService);

let projectRepository: ProjectRepository;
let workflowRepository: WorkflowRepository;
let workflowHistoryRepository: WorkflowHistoryRepository;
let eventService: EventService;
let folderListMissingRole: Role;
let workflowPublishHistoryRepository: WorkflowPublishHistoryRepository;

beforeEach(async () => {
	await testDb.truncate([
		'SharedWorkflow',
		'ProjectRelation',
		'Folder',
		'WorkflowEntity',
		'WorkflowHistory',
		'WorkflowPublishHistory',
		'TagEntity',
		'Project',
		'User',
	]);
	await cleanupRolesAndScopes();
	projectRepository = Container.get(ProjectRepository);
	workflowRepository = Container.get(WorkflowRepository);
	workflowHistoryRepository = Container.get(WorkflowHistoryRepository);
	eventService = Container.get(EventService);
	workflowPublishHistoryRepository = Container.get(WorkflowPublishHistoryRepository);
	owner = await createOwner();
	authOwnerAgent = testServer.authAgentFor(owner);
	member = await createMember();
	authMemberAgent = testServer.authAgentFor(member);
	anotherMember = await createMember();

	workflowValidationService.validateForActivation.mockReturnValue({ isValid: true });
	workflowValidationService.validateSubWorkflowReferences.mockResolvedValue({ isValid: true });

	folderListMissingRole = await createCustomRoleWithScopeSlugs(['workflow:read', 'workflow:list'], {
		roleType: 'project',
		displayName: 'Workflow Read-Only',
		description: 'Can only read and list workflows',
	});
});

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

describe('POST /workflows', () => {
	const testWithPinData = async (withPinData: boolean) => {
		const workflow = makeWorkflow({ withPinData });
		const response = await authOwnerAgent.post('/workflows').send(workflow);
		expect(response.statusCode).toBe(200);
		return (response.body.data as { pinData: IPinData }).pinData;
	};

	test('should store pin data for node in workflow', async () => {
		const pinData = await testWithPinData(true);
		expect(pinData).toMatchObject(MOCK_PINDATA);
	});

	test('should set pin data to null if no pin data', async () => {
		const pinData = await testWithPinData(false);
		expect(pinData).toBeNull();
	});

	test('should retain accept `workflow.id`', async () => {
		const payload = {
			id: 'HDssU5Ce250UWyLg_MNG4',
			name: 'name',
			nodes: [],
			connections: {},
			staticData: null,
			settings: {},
			active: false,
		};

		const response = await authMemberAgent.post('/workflows').send(payload).expect(200);

		expect(response.body.data.id).toBe(payload.id);
	});

	test('fails if a workflow with that id already exists', async () => {
		const payload1 = {
			id: 'HDssU5Ce250UWyLg_MNG4',
			name: 'testing with context',
			nodes: [],
			connections: {},
			staticData: null,
			settings: {},
			active: false,
		};
		const payload2 = { ...payload1, name: 'different name' };

		await authMemberAgent.post('/workflows').send(payload1).expect(200);
		const response = await authMemberAgent.post('/workflows').send(payload2);

		expect(response.status).toBe(400);
	});

	test('should return scopes on created workflow', async () => {
		const payload = {
			name: 'testing',
			nodes: [
				{
					id: 'uuid-1234',
					parameters: {},
					name: 'Start',
					type: 'n8n-nodes-base.manualTrigger',
					typeVersion: 1,
					position: [240, 300],
				},
			],
			connections: {},
			staticData: null,
			settings: {
				saveExecutionProgress: true,
				saveManualExecutions: true,
				saveDataErrorExecution: 'all',
				saveDataSuccessExecution: 'all',
				executionTimeout: 3600,
				timezone: 'America/New_York',
			},
			active: false,
			activeVersionId: null,
		};

		const response = await authMemberAgent.post('/workflows').send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id, scopes },
		} = response.body;

		expect(id).toBeDefined();
		expect(scopes).toEqual(
			[
				'workflow:delete',
				'workflow:execute',
				'workflow:execute-chat',
				'workflow:move',
				'workflow:publish',
				'workflow:read',
				'workflow:share',
				'workflow:update',
			].sort(),
		);
	});

	test('should create workflow with uiContext parameter', async () => {
		const payload = {
			name: 'testing with context',
			nodes: [
				{
					id: 'uuid-1234',
					parameters: {},
					name: 'Start',
					type: 'n8n-nodes-base.manualTrigger',
					typeVersion: 1,
					position: [240, 300],
				},
			],
			connections: {},
			staticData: null,
			settings: {},
			active: false,
			activeVersionId: null,
			uiContext: 'workflow_list',
		};

		const response = await authMemberAgent.post('/workflows').send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id, name },
		} = response.body;

		expect(id).toBeDefined();
		expect(name).toBe('testing with context');
	});

	test('should always create workflow history version', async () => {
		const payload = {
			name: 'testing',
			nodes: [
				{
					id: 'uuid-1234',
					parameters: {},
					name: 'Start',
					type: 'n8n-nodes-base.manualTrigger',
					typeVersion: 1,
					position: [240, 300],
				},
			],
			connections: {},
			staticData: null,
			settings: {
				saveExecutionProgress: true,
				saveManualExecutions: true,
				saveDataErrorExecution: 'all',
				saveDataSuccessExecution: 'all',
				executionTimeout: 3600,
				timezone: 'America/New_York',
			},
			active: false,
			activeVersionId: null,
		};

		const response = await authOwnerAgent.post('/workflows').send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id },
		} = response.body;

		expect(id).toBeDefined();
		expect(await workflowHistoryRepository.count({ where: { workflowId: id } })).toBe(1);
		const historyVersion = await workflowHistoryRepository.findOne({
			where: {
				workflowId: id,
			},
		});
		expect(historyVersion).not.toBeNull();
		expect(historyVersion!.connections).toEqual(payload.connections);
		expect(historyVersion!.nodes).toEqual(payload.nodes);
	});

	test('should set autosaved: true in workflow history when autosaved parameter is sent', async () => {
		const payload = {
			name: 'testing autosave',
			nodes: [
				{
					id: 'uuid-1234',
					parameters: {},
					name: 'Start',
					type: 'n8n-nodes-base.manualTrigger',
					typeVersion: 1,
					position: [240, 300],
				},
			],
			connections: {},
			staticData: null,
			settings: {},
			active: false,
			activeVersionId: null,
			autosaved: true,
		};

		const response = await authOwnerAgent.post('/workflows').send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id },
		} = response.body as { data: WorkflowEntity };

		expect(id).toBeDefined();
		const historyVersion = await workflowHistoryRepository.findOne({
			where: {
				workflowId: id,
			},
		});
		expect(historyVersion).not.toBeNull();
		expect(historyVersion!.autosaved).toBe(true);
	});

	test('should set autosaved: false in workflow history when autosaved is not sent', async () => {
		const payload = {
			name: 'testing manual save',
			nodes: [
				{
					id: 'uuid-1234',
					parameters: {},
					name: 'Start',
					type: 'n8n-nodes-base.manualTrigger',
					typeVersion: 1,
					position: [240, 300],
				},
			],
			connections: {},
			staticData: null,
			settings: {},
			active: false,
			activeVersionId: null,
		};

		const response = await authOwnerAgent.post('/workflows').send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id },
		} = response.body as { data: WorkflowEntity };

		expect(id).toBeDefined();
		const historyVersion = await workflowHistoryRepository.findOne({
			where: {
				workflowId: id,
			},
		});
		expect(historyVersion).not.toBeNull();
		expect(historyVersion!.autosaved).toBe(false);
	});

	test('should create workflow as inactive even when active fields are provided', async () => {
		const activeWorkflow = await createActiveWorkflow({}, owner);
		const activeVersion = await workflowHistoryRepository.findOne({
			where: {
				versionId: activeWorkflow.activeVersionId!,
			},
		});

		const { id: existingWorkflowId, ...workflowWithoutId } = activeWorkflow;
		const payload = {
			...workflowWithoutId,
			// Deliberately set active fields
			active: true,
			activeVersionId: activeWorkflow.activeVersionId,
			activeVersion,
		};

		const response = await authOwnerAgent.post('/workflows').send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id, versionId, activeVersionId, active },
		} = response.body;

		expect(id).toBeDefined();
		expect(versionId).toBeDefined();
		// New version should be created
		expect(versionId).not.toBe(activeWorkflow.versionId);
		expect(activeVersionId).toBeNull();
		expect(active).toBe(false);

		// Verify in database that workflow is completely inactive
		const workflow = await workflowRepository.findOneBy({ id });
		expect(workflow?.activeVersionId).toBeNull();
		expect(workflow?.active).toBe(false);

		// Verify workflow history was created
		const historyCount = await workflowHistoryRepository.count({ where: { workflowId: id } });
		expect(historyCount).toBe(1);
	});

	test('create workflow in personal project by default', async () => {
		//
		// ARRANGE
		//
		const tag = await createTag({ name: 'A' });
		const workflow = makeWorkflow();
		const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

		//
		// ACT
		//
		const response = await authOwnerAgent
			.post('/workflows')
			.send({ ...workflow, tags: [tag.id] })
			.expect(200);

		//
		// ASSERT
		//
		await Container.get(SharedWorkflowRepository).findOneOrFail({
			where: {
				projectId: personalProject.id,
				workflowId: response.body.data.id,
			},
		});
		expect(response.body.data).toMatchObject({
			active: false,
			activeVersionId: null,
			id: expect.any(String),
			name: workflow.name,
			sharedWithProjects: [],
			usedCredentials: [],
			homeProject: {
				id: personalProject.id,
				name: personalProject.name,
				type: personalProject.type,
			},
			tags: [{ id: tag.id, name: tag.name }],
		});
		expect(response.body.data.shared).toBeUndefined();
	});

	test('creates workflow in a specific project if the projectId is passed', async () => {
		//
		// ARRANGE
		//
		const tag = await createTag({ name: 'A' });
		const workflow = makeWorkflow();
		const project = await projectRepository.save(
			projectRepository.create({
				name: 'Team Project',
				type: 'team',
			}),
		);
		await Container.get(ProjectService).addUser(project.id, {
			userId: owner.id,
			role: 'project:admin',
		});

		//
		// ACT
		//
		const response = await authOwnerAgent
			.post('/workflows')
			.send({ ...workflow, projectId: project.id, tags: [tag.id] })
			.expect(200);

		//
		// ASSERT
		//
		await Container.get(SharedWorkflowRepository).findOneOrFail({
			where: {
				projectId: project.id,
				workflowId: response.body.data.id,
			},
		});
		expect(response.body.data).toMatchObject({
			active: false,
			activeVersionId: null,
			id: expect.any(String),
			name: workflow.name,
			sharedWithProjects: [],
			usedCredentials: [],
			homeProject: {
				id: project.id,
				name: project.name,
				type: project.type,
			},
			tags: [{ id: tag.id, name: tag.name }],
		});
		expect(response.body.data.shared).toBeUndefined();
	});

	test('does not create the workflow in a specific project if the user is not part of the project', async () => {
		//
		// ARRANGE
		//
		const workflow = makeWorkflow();
		const project = await projectRepository.save(
			projectRepository.create({
				name: 'Team Project',
				type: 'team',
			}),
		);

		//
		// ACT
		//
		await testServer
			.authAgentFor(member)
			.post('/workflows')
			.send({ ...workflow, projectId: project.id })
			//
			// ASSERT
			//
			.expect(400, {
				code: 400,
				message: "You don't have the permissions to save the workflow in this project.",
			});
	});

	test('does not create the workflow in a specific project if the user does not have the right role to do so', async () => {
		//
		// ARRANGE
		//
		const workflow = makeWorkflow();
		const project = await projectRepository.save(
			projectRepository.create({
				name: 'Team Project',
				type: 'team',
			}),
		);
		await Container.get(ProjectService).addUser(project.id, {
			userId: member.id,
			role: 'project:viewer',
		});

		//
		// ACT
		//
		await testServer
			.authAgentFor(member)
			.post('/workflows')
			.send({ ...workflow, projectId: project.id })
			//
			// ASSERT
			//
			.expect(400, {
				code: 400,
				message: "You don't have the permissions to save the workflow in this project.",
			});
	});

	test('does not create the workflow in a personal project if the user is chat user', async () => {
		//
		// ARRANGE
		//
		const chatUser = await createChatUser();
		const workflow = makeWorkflow();

		//
		// ACT
		//
		await testServer
			.authAgentFor(chatUser)
			.post('/workflows')
			.send({ ...workflow })
			//
			// ASSERT
			//
			.expect(400, {
				code: 400,
				message: "You don't have the permissions to save the workflow in this project.",
			});
	});

	test('create link workflow with folder if one is provided', async () => {
		//
		// ARRANGE
		//
		const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
		const folder = await createFolder(personalProject, { name: 'Folder 1' });

		const workflow = makeWorkflow();

		//
		// ACT
		//
		const response = await authOwnerAgent
			.post('/workflows')
			.send({ ...workflow, parentFolderId: folder.id });

		//
		// ASSERT
		//

		expect(response.body.data).toMatchObject({
			active: false,
			activeVersionId: null,
			id: expect.any(String),
			name: workflow.name,
			sharedWithProjects: [],
			usedCredentials: [],
			homeProject: {
				id: personalProject.id,
				name: personalProject.name,
				type: personalProject.type,
			},
			parentFolder: {
				id: folder.id,
				name: folder.name,
			},
		});
		expect(response.body.data.shared).toBeUndefined();
	});

	test('create workflow without parent folder if no folder is provided', async () => {
		//
		// ARRANGE
		//
		const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
		const workflow = makeWorkflow();

		//
		// ACT
		//
		const response = await authOwnerAgent
			.post('/workflows')
			.send({ ...workflow })
			.expect(200);

		//
		// ASSERT
		//

		expect(response.body.data).toMatchObject({
			active: false,
			activeVersionId: null,
			id: expect.any(String),
			name: workflow.name,
			sharedWithProjects: [],
			usedCredentials: [],
			homeProject: {
				id: personalProject.id,
				name: personalProject.name,
				type: personalProject.type,
			},
			parentFolder: null,
		});
		expect(response.body.data.shared).toBeUndefined();
	});

	test('create workflow without parent is provided folder does not exist in the project', async () => {
		//
		// ARRANGE
		//
		const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
		const workflow = makeWorkflow();

		//
		// ACT
		//
		const response = await authOwnerAgent
			.post('/workflows')
			.send({ ...workflow, parentFolderId: 'non-existing-folder-id' })
			.expect(200);

		//
		// ASSERT
		//

		expect(response.body.data).toMatchObject({
			active: false,
			activeVersionId: null,
			id: expect.any(String),
			name: workflow.name,
			sharedWithProjects: [],
			usedCredentials: [],
			homeProject: {
				id: personalProject.id,
				name: personalProject.name,
				type: personalProject.type,
			},
			parentFolder: null,
		});
		expect(response.body.data.shared).toBeUndefined();
	});

	describe('Security: Mass Assignment Protection', () => {
		test.each([
			{
				field: 'triggerCount' as const,
				maliciousValue: 999,
				expectedValue: 0,
				description: 'billing bypass protection',
			},
			{
				field: 'versionCounter' as const,
				maliciousValue: 999,
				expectedValue: 1,
				description: 'versioning manipulation',
			},
			{
				field: 'isArchived' as const,
				maliciousValue: true,
				expectedValue: false,
				description: 'archived workflow creation',
			},
		])(
			'should ignore $field field sent via API ($description)',
			async ({ field, maliciousValue, expectedValue }) => {
				const payload = {
					name: 'Test Workflow',
					nodes: [],
					connections: {},
					[field]: maliciousValue,
				};

				const response = await authMemberAgent.post('/workflows').send(payload).expect(200);

				const createdWorkflow = await workflowRepository.findOneBy({
					id: response.body.data.id,
				});

				expect(createdWorkflow?.[field]).toBe(expectedValue);
			},
		);

		test('should prevent setting activeVersionId via API', async () => {
			const maliciousVersionId = uuid();
			const payload = {
				name: 'Test Workflow',
				nodes: [],
				connections: {},
				activeVersionId: maliciousVersionId,
			};

			const response = await authMemberAgent.post('/workflows').send(payload).expect(200);

			const createdWorkflow = await workflowRepository.findOneBy({
				id: response.body.data.id,
			});

			expect(createdWorkflow?.activeVersionId).toBeNull();
		});

		test('should always create workflow as inactive regardless of active flag', async () => {
			const payload = {
				name: 'Test Workflow',
				nodes: [],
				connections: {},
				active: true, // Attempt to create active workflow
			};

			const response = await authMemberAgent.post('/workflows').send(payload).expect(200);

			const createdWorkflow = await workflowRepository.findOneBy({
				id: response.body.data.id,
			});

			// Workflow should always be created as inactive
			expect(createdWorkflow?.active).toBe(false);
			expect(response.body.data.active).toBe(false);
		});

		test('should allow setting legitimate fields like name, nodes, connections, settings', async () => {
			const payload = {
				name: 'Legitimate Workflow',
				description: 'A legitimate workflow',
				nodes: [
					{
						id: 'test-node',
						name: 'Test Node',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [250, 300],
						parameters: {},
					},
				],
				connections: {},
				settings: {
					saveExecutionProgress: true,
				},
				meta: { testMeta: 'value' },
			};

			const response = await authMemberAgent.post('/workflows').send(payload).expect(200);

			const createdWorkflow = await workflowRepository.findOneBy({
				id: response.body.data.id,
			});

			// Legitimate fields should be set correctly
			expect(createdWorkflow?.name).toBe('Legitimate Workflow');
			expect(createdWorkflow?.description).toBe('A legitimate workflow');
			expect(createdWorkflow?.nodes).toHaveLength(1);
			expect(createdWorkflow?.settings).toMatchObject({ saveExecutionProgress: true });
			expect(createdWorkflow?.meta).toMatchObject({ testMeta: 'value' });
		});
	});
});

describe('GET /workflows/:workflowId', () => {
	test('should return pin data', async () => {
		const workflow = makeWorkflow({ withPinData: true });
		const workflowCreationResponse = await authOwnerAgent.post('/workflows').send(workflow);

		const { id } = workflowCreationResponse.body.data as { id: string };
		const workflowRetrievalResponse = await authOwnerAgent.get(`/workflows/${id}`);

		expect(workflowRetrievalResponse.statusCode).toBe(200);
		const { pinData } = workflowRetrievalResponse.body.data as { pinData: IPinData };
		expect(pinData).toMatchObject(MOCK_PINDATA);
	});

	test('should return tags', async () => {
		const tag = await createTag({ name: 'A' });
		const workflow = await createWorkflow({ tags: [tag] }, owner);

		const response = await authOwnerAgent.get(`/workflows/${workflow.id}`).expect(200);

		expect(response.body.data).toMatchObject({
			tags: [expect.objectContaining({ id: tag.id, name: tag.name })],
		});
	});

	test('should return active version with workflowPublishHistory', async () => {
		const workflow = await createActiveWorkflow({}, owner);

		const response = await authOwnerAgent.get(`/workflows/${workflow.id}`).expect(200);

		const { data: responseData } = response.body as { data: { activeVersion: WorkflowHistory } };
		const { activeVersion } = responseData;

		expect(activeVersion).toMatchObject({
			versionId: workflow.activeVersionId,
			workflowId: workflow.id,
		});
		expect(activeVersion.workflowPublishHistory).toHaveLength(1);
		expect(activeVersion.workflowPublishHistory[0]).toMatchObject({
			event: 'activated',
			versionId: workflow.activeVersionId,
		});
	});

	test('should return parent folder', async () => {
		const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

		const folder1 = await createFolder(personalProject, { name: 'Folder 1' });

		const folder2 = await createFolder(personalProject, {
			name: 'Folder 2',
			parentFolder: folder1,
		});

		const workflow1 = await createWorkflow({ parentFolder: folder2 }, owner);

		const workflow2 = await createWorkflow({}, owner);

		const workflow3 = await createWorkflow({ parentFolder: folder1 }, owner);

		const workflowInNestedFolderWithGrantParent = await authOwnerAgent
			.get(`/workflows/${workflow1.id}`)
			.expect(200);

		expect(workflowInNestedFolderWithGrantParent.body.data).toMatchObject({
			parentFolder: expect.objectContaining({
				id: folder2.id,
				name: folder2.name,
				parentFolderId: folder1.id,
			}),
		});

		const workflowInProjectRoot = await authOwnerAgent
			.get(`/workflows/${workflow2.id}`)
			.expect(200);

		expect(workflowInProjectRoot.body.data).toMatchObject({
			parentFolder: null,
		});

		const workflowInNestedFolder = await authOwnerAgent
			.get(`/workflows/${workflow3.id}`)
			.expect(200);

		expect(workflowInNestedFolder.body.data).toMatchObject({
			parentFolder: expect.objectContaining({
				id: folder1.id,
				name: folder1.name,
				parentFolderId: null,
			}),
		});
	});
});

describe('GET /workflows/:workflowId/exists', () => {
	test('should return true when workflow exists and user has access', async () => {
		const workflow = await createWorkflow({}, owner);

		const response = await authOwnerAgent.get(`/workflows/${workflow.id}/exists`).expect(200);

		expect(response.body).toEqual({ data: { exists: true } });
	});

	test('should return false when workflow does not exist', async () => {
		const nonExistentId = uuid();

		const response = await authOwnerAgent.get(`/workflows/${nonExistentId}/exists`).expect(200);

		expect(response.body).toEqual({ data: { exists: false } });
	});

	test('should return true when workflow exists even if user does not have access', async () => {
		const workflow = await createWorkflow({}, owner);

		const response = await authMemberAgent.get(`/workflows/${workflow.id}/exists`).expect(200);

		expect(response.body).toEqual({ data: { exists: true } });
	});

	test('should return true when workflow is shared with user', async () => {
		const workflow = await createWorkflow({}, owner);

		const memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
			member.id,
		);

		await shareWorkflowWithProjects(workflow, [{ project: memberPersonalProject }]);

		const response = await authMemberAgent.get(`/workflows/${workflow.id}/exists`).expect(200);

		expect(response.body).toEqual({ data: { exists: true } });
	});
});

describe('GET /workflows', () => {
	test('should return zero workflows if none exist', async () => {
		const response = await authOwnerAgent.get('/workflows').expect(200);

		expect(response.body).toEqual({ count: 0, data: [] });
	});

	test('should return workflows', async () => {
		const credential = await saveCredential(randomCredentialPayload(), {
			user: owner,
			role: 'credential:owner',
		});
		const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

		const nodes: INode[] = [
			{
				id: uuid(),
				name: 'Action Network',
				type: 'n8n-nodes-base.actionNetwork',
				parameters: {},
				typeVersion: 1,
				position: [0, 0],
				credentials: {
					actionNetworkApi: {
						id: credential.id,
						name: credential.name,
					},
				},
			},
		];

		const tag = await createTag({ name: 'A' });

		await createWorkflow({ name: 'First', nodes, tags: [tag] }, owner);
		await createWorkflow({ name: 'Second' }, owner);

		const response = await authOwnerAgent.get('/workflows').expect(200);

		expect(response.body).toEqual({
			count: 2,
			data: arrayContaining([
				objectContaining({
					id: any(String),
					name: 'First',
					active: false,
					activeVersionId: null,
					createdAt: any(String),
					updatedAt: any(String),
					tags: [{ id: any(String), name: 'A' }],
					versionId: any(String),
					homeProject: {
						id: ownerPersonalProject.id,
						name: owner.createPersonalProjectName(),
						icon: null,
						type: ownerPersonalProject.type,
					},
					sharedWithProjects: [],
				}),
				objectContaining({
					id: any(String),
					name: 'Second',
					active: false,
					activeVersionId: null,
					createdAt: any(String),
					updatedAt: any(String),
					tags: [],
					versionId: any(String),
					homeProject: {
						id: ownerPersonalProject.id,
						name: owner.createPersonalProjectName(),
						icon: null,
						type: ownerPersonalProject.type,
					},
					sharedWithProjects: [],
				}),
			]),
		});

		const found = response.body.data.find(
			(w: ListQueryDb.Workflow.WithOwnership) => w.name === 'First',
		);

		expect(found.nodes).toBeUndefined();
		expect(found.sharedWithProjects).toHaveLength(0);
		expect(found.usedCredentials).toBeUndefined();
	});

	test('should return workflows with scopes when ?includeScopes=true', async () => {
		const [member1, member2] = await createManyUsers(2, {
			role: { slug: 'global:member' },
		});

		const teamProject = await createTeamProject(undefined, member1);
		await linkUserToProject(member2, teamProject, 'project:editor');

		const credential = await saveCredential(randomCredentialPayload(), {
			user: owner,
			role: 'credential:owner',
		});

		const nodes: INode[] = [
			{
				id: uuid(),
				name: 'Action Network',
				type: 'n8n-nodes-base.actionNetwork',
				parameters: {},
				typeVersion: 1,
				position: [0, 0],
				credentials: {
					actionNetworkApi: {
						id: credential.id,
						name: credential.name,
					},
				},
			},
		];

		const tag = await createTag({ name: 'A' });

		const [savedWorkflow1, savedWorkflow2] = await Promise.all([
			createWorkflow({ name: 'First', nodes, tags: [tag] }, teamProject),
			createWorkflow({ name: 'Second' }, member2),
		]);

		await shareWorkflowWithProjects(savedWorkflow2, [{ project: teamProject }]);

		{
			const response = await testServer.authAgentFor(member1).get('/workflows?includeScopes=true');

			expect(response.statusCode).toBe(200);
			expect(response.body.data.length).toBe(2);

			const workflows = response.body.data as Array<IWorkflowBase & { scopes: Scope[] }>;
			const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf1.scopes).toEqual(
				[
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:update',
				].sort(),
			);

			// Shared workflow
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(wf2.scopes).toEqual(
				[
					'workflow:read',
					'workflow:update',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:publish',
				].sort(),
			);
		}

		{
			const response = await testServer.authAgentFor(member2).get('/workflows?includeScopes=true');

			expect(response.statusCode).toBe(200);
			expect(response.body.data.length).toBe(2);

			const workflows = response.body.data as Array<IWorkflowBase & { scopes: Scope[] }>;
			const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf1.scopes).toEqual([
				'workflow:delete',
				'workflow:execute',
				'workflow:execute-chat',
				'workflow:publish',
				'workflow:read',
				'workflow:update',
			]);

			// Shared workflow
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(wf2.scopes).toEqual(
				[
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:share',
					'workflow:update',
				].sort(),
			);
		}

		{
			const response = await testServer.authAgentFor(owner).get('/workflows?includeScopes=true');

			expect(response.statusCode).toBe(200);
			expect(response.body.data.length).toBe(2);

			const workflows = response.body.data as Array<IWorkflowBase & { scopes: Scope[] }>;
			const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf1.scopes).toEqual(
				[
					'workflow:create',
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:list',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:share',
					'workflow:update',
				].sort(),
			);

			// Shared workflow
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(wf2.scopes).toEqual(
				[
					'workflow:create',
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:list',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:share',
					'workflow:update',
				].sort(),
			);
		}
	});

	describe('filter', () => {
		test('should filter workflows by field: query', async () => {
			await createWorkflow({ name: 'First', description: 'A workflow' }, owner);
			await createWorkflow({ name: 'Second', description: 'Also a workflow' }, owner);
			await createWorkflow({ name: 'Third', description: 'My first workflow' }, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={"query":"first"}')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: [objectContaining({ name: 'First' }), objectContaining({ name: 'Third' })],
			});
		});

		test('should filter workflows by field: active', async () => {
			await createActiveWorkflow({}, owner);
			await createWorkflow({}, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "active": true }')
				.expect(200);

			expect(response.body).toEqual({
				count: 1,
				data: [objectContaining({ active: true, activeVersionId: expect.any(String) })],
			});
		});

		test('should filter workflows by field: availableInMCP', async () => {
			const workflow1 = await createWorkflow({ settings: { availableInMCP: true } }, owner);
			await createWorkflow({ settings: {} }, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "availableInMCP": true }')
				.expect(200);

			expect(response.body).toEqual({
				count: 1,
				data: [objectContaining({ id: workflow1.id })],
			});
		});

		test('should filter workflows by field: tags (AND operator)', async () => {
			const workflow1 = await createWorkflow({ name: 'First' }, owner);
			const workflow2 = await createWorkflow({ name: 'Second' }, owner);

			const baseDate = DateTime.now();

			await createTag(
				{
					name: 'A',
					createdAt: baseDate.toJSDate(),
				},
				workflow1,
			);
			await createTag(
				{
					name: 'B',
					createdAt: baseDate.plus({ seconds: 1 }).toJSDate(),
				},
				workflow1,
			);

			const tagC = await createTag({ name: 'C' }, workflow2);

			await assignTagToWorkflow(tagC, workflow2);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "tags": ["A", "B"] }')
				.expect(200);

			expect(response.body).toEqual({
				count: 1,
				data: [
					objectContaining({
						name: 'First',
						tags: expect.arrayContaining([
							{ id: any(String), name: 'A' },
							{ id: any(String), name: 'B' },
						]),
					}),
				],
			});
		});

		test('should filter workflows by projectId', async () => {
			const workflow = await createWorkflow({ name: 'First' }, owner);
			const pp = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

			const response1 = await authOwnerAgent
				.get('/workflows')
				.query(`filter={ "projectId": "${pp.id}" }`)
				.expect(200);

			expect(response1.body.data).toHaveLength(1);
			expect(response1.body.data[0].id).toBe(workflow.id);

			const response2 = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "projectId": "Non-Existing Project ID" }')
				.expect(200);

			expect(response2.body.data).toHaveLength(0);
		});

		test('should filter by personal project and return only workflows where the user is owner', async () => {
			const workflow = await createWorkflow({ name: 'First' }, owner);
			const workflow2 = await createWorkflow({ name: 'Second' }, member);
			await shareWorkflowWithUsers(workflow2, [owner]);
			const pp = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

			const response1 = await authOwnerAgent
				.get('/workflows')
				.query(`filter={ "projectId": "${pp.id}" }`)
				.expect(200);

			expect(response1.body.data).toHaveLength(1);
			expect(response1.body.data[0].id).toBe(workflow.id);

			const response2 = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "projectId": "Non-Existing Project ID" }')
				.expect(200);

			expect(response2.body.data).toHaveLength(0);

			const response3 = await authOwnerAgent.get('/workflows').query('filter={}').expect(200);
			expect(response3.body.data).toHaveLength(2);
		});

		test('should filter by personal project and return only workflows where the user is member', async () => {
			const workflow = await createWorkflow({ name: 'First' }, member);
			const workflow2 = await createWorkflow({ name: 'Second' }, owner);
			await shareWorkflowWithUsers(workflow2, [member]);
			const pp = await projectRepository.getPersonalProjectForUserOrFail(member.id);

			const response1 = await authMemberAgent
				.get('/workflows')
				.query(`filter={ "projectId": "${pp.id}" }`)
				.expect(200);

			expect(response1.body.data).toHaveLength(1);
			expect(response1.body.data[0].id).toBe(workflow.id);

			const response2 = await authMemberAgent
				.get('/workflows')
				.query('filter={ "projectId": "Non-Existing Project ID" }')
				.expect(200);

			expect(response2.body.data).toHaveLength(0);

			const response3 = await authMemberAgent.get('/workflows').query('filter={}').expect(200);
			expect(response3.body.data).toHaveLength(2);
		});

		test('should filter workflows by parentFolderId', async () => {
			const pp = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

			const folder1 = await createFolder(pp, { name: 'Folder 1' });

			const workflow1 = await createWorkflow({ name: 'First', parentFolder: folder1 }, owner);

			const workflow2 = await createWorkflow({ name: 'Second' }, owner);

			const response1 = await authOwnerAgent
				.get('/workflows')
				.query(`filter={ "parentFolderId": "${folder1.id}" }`)
				.expect(200);

			expect(response1.body.data).toHaveLength(1);
			expect(response1.body.data[0].id).toBe(workflow1.id);

			// if not provided, looks for workflows without a parentFolder
			const response2 = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "parentFolderId": "0" }');
			expect(200);

			expect(response2.body.data).toHaveLength(1);
			expect(response2.body.data[0].id).toBe(workflow2.id);
		});

		test('should filter workflows by nodeTypes', async () => {
			const httpWorkflow = await createWorkflow(
				{
					name: 'HTTP Workflow',
					nodes: [
						{
							id: uuid(),
							name: 'HTTP Request',
							type: 'n8n-nodes-base.httpRequest',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			const slackWorkflow = await createWorkflow(
				{
					name: 'Slack Workflow',
					nodes: [
						{
							id: uuid(),
							name: 'Slack',
							type: 'n8n-nodes-base.slack',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			const mixedWorkflow = await createWorkflow(
				{
					name: 'Mixed Workflow',
					nodes: [
						{
							id: uuid(),
							name: 'HTTP Request',
							type: 'n8n-nodes-base.httpRequest',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
						{
							id: uuid(),
							name: 'Slack',
							type: 'n8n-nodes-base.slack',
							parameters: {},
							typeVersion: 1,
							position: [100, 0],
						},
					],
				},
				owner,
			);

			// Filter by single node type
			const httpResponse = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "nodeTypes": ["n8n-nodes-base.httpRequest"] }&select=["nodes"]')
				.expect(200);

			expect(httpResponse.body.data).toHaveLength(2);
			const httpWorkflowIds = httpResponse.body.data.map((w: any) => w.id);
			expect(httpWorkflowIds).toContain(httpWorkflow.id);
			expect(httpWorkflowIds).toContain(mixedWorkflow.id);
			expect(httpResponse.body.data[0].nodes).toHaveLength(1);
			expect(httpResponse.body.data[1].nodes).toHaveLength(2);

			// Filter by multiple node types (OR operation - returns workflows containing ANY of the specified node types)
			const multipleResponse = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "nodeTypes": ["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"] }')
				.expect(200);

			expect(multipleResponse.body.data).toHaveLength(3);
			const multipleWorkflowIds = multipleResponse.body.data.map((w: any) => w.id);
			expect(multipleWorkflowIds).toContain(httpWorkflow.id);
			expect(multipleWorkflowIds).toContain(slackWorkflow.id);
			expect(multipleWorkflowIds).toContain(mixedWorkflow.id);

			// Filter by non-existent node type
			const emptyResponse = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "nodeTypes": ["n8n-nodes-base.nonExistent"] }')
				.expect(200);

			expect(emptyResponse.body.data).toHaveLength(0);
		});

		test('should filter workflows by triggerNodeTypes', async () => {
			const executeWorkflowTriggerWorkflow = await createWorkflow(
				{
					name: 'Subworkflow',
					nodes: [
						{
							id: uuid(),
							name: 'When Executed by Another Workflow',
							type: 'n8n-nodes-base.executeWorkflowTrigger',
							parameters: {
								inputSource: 'passthrough',
							},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			const errorTriggerWorkflow = await createWorkflow(
				{
					name: 'Error Handler',
					nodes: [
						{
							id: uuid(),
							name: 'When Workflow Errors',
							type: 'n8n-nodes-base.errorTrigger',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			const scheduleTriggerWorkflow = await createWorkflow(
				{
					name: 'Normal Workflow',
					nodes: [
						{
							id: uuid(),
							name: 'Schedule Trigger',
							type: 'n8n-nodes-base.scheduleTrigger',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			// Filter by Execute Workflow Trigger (single type in array)
			const executeWorkflowResponse = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "triggerNodeTypes": ["n8n-nodes-base.executeWorkflowTrigger"] }')
				.expect(200);

			expect(executeWorkflowResponse.body.data).toHaveLength(1);
			expect(executeWorkflowResponse.body.data[0].id).toBe(executeWorkflowTriggerWorkflow.id);
			expect(executeWorkflowResponse.body.data[0].name).toBe('Subworkflow');

			// Filter by Error Trigger (single type in array)
			const errorTriggerResponse = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "triggerNodeTypes": ["n8n-nodes-base.errorTrigger"] }')
				.expect(200);

			expect(errorTriggerResponse.body.data).toHaveLength(1);
			expect(errorTriggerResponse.body.data[0].id).toBe(errorTriggerWorkflow.id);
			expect(errorTriggerResponse.body.data[0].name).toBe('Error Handler');

			// Filter by multiple trigger types
			const multiTriggerResponse = await authOwnerAgent
				.get('/workflows')
				.query(
					'filter={ "triggerNodeTypes": ["n8n-nodes-base.executeWorkflowTrigger", "n8n-nodes-base.scheduleTrigger"] }',
				)
				.expect(200);

			expect(multiTriggerResponse.body.data).toHaveLength(2);
			const returnedIds = multiTriggerResponse.body.data.map(
				(w: { id: string }) => w.id,
			) as string[];
			expect(returnedIds).toContain(executeWorkflowTriggerWorkflow.id);
			expect(returnedIds).toContain(scheduleTriggerWorkflow.id);
			expect(returnedIds).not.toContain(errorTriggerWorkflow.id);

			// Filter by non-existent trigger type
			const emptyResponse = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "triggerNodeTypes": ["n8n-nodes-base.nonExistentTrigger"] }')
				.expect(200);

			expect(emptyResponse.body.data).toHaveLength(0);
		});

		test('should all workflows when filtering by empty nodeTypes array', async () => {
			await createWorkflow(
				{
					name: 'Test Workflow',
					nodes: [
						{
							id: uuid(),
							name: 'Start',
							type: 'n8n-nodes-base.manualTrigger',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "nodeTypes": [] }')
				.expect(200);

			expect(response.body.data).toHaveLength(1); // Should return all workflows when nodeTypes is empty
		});
	});

	describe('select', () => {
		test('should select workflow field: name', async () => {
			await createWorkflow({ name: 'First' }, owner);
			await createWorkflow({ name: 'Second' }, owner);

			const response = await authOwnerAgent.get('/workflows').query('select=["name"]').expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					{ id: any(String), name: 'First' },
					{ id: any(String), name: 'Second' },
				]),
			});
		});

		test('should select workflow field: active', async () => {
			await createActiveWorkflow({}, owner);
			await createWorkflow({}, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('select=["active"]')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					{ id: any(String), active: true },
					{ id: any(String), active: false },
				]),
			});
		});

		test('should select workflow field: activeVersionId', async () => {
			const activeWorkflow = await createActiveWorkflow({}, owner);
			await createWorkflow({}, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('select=["activeVersionId"]')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					{ id: any(String), activeVersionId: activeWorkflow.versionId },
					{ id: any(String), activeVersionId: null },
				]),
			});
		});

		test('should select workflow field: tags', async () => {
			const firstWorkflow = await createWorkflow({ name: 'First' }, owner);
			const secondWorkflow = await createWorkflow({ name: 'Second' }, owner);

			await createTag({ name: 'A' }, firstWorkflow);
			await createTag({ name: 'B' }, secondWorkflow);

			const response = await authOwnerAgent.get('/workflows').query('select=["tags"]').expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					objectContaining({ id: any(String), tags: [{ id: any(String), name: 'A' }] }),
					objectContaining({ id: any(String), tags: [{ id: any(String), name: 'B' }] }),
				]),
			});
		});

		test('should select workflow fields: createdAt and updatedAt', async () => {
			const firstWorkflowCreatedAt = '2023-08-08T09:31:25.000Z';
			const firstWorkflowUpdatedAt = '2023-08-08T09:31:40.000Z';
			const secondWorkflowCreatedAt = '2023-07-07T09:31:25.000Z';
			const secondWorkflowUpdatedAt = '2023-07-07T09:31:40.000Z';

			await createWorkflow(
				{
					createdAt: new Date(firstWorkflowCreatedAt),
					updatedAt: new Date(firstWorkflowUpdatedAt),
				},
				owner,
			);
			await createWorkflow(
				{
					createdAt: new Date(secondWorkflowCreatedAt),
					updatedAt: new Date(secondWorkflowUpdatedAt),
				},
				owner,
			);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('select=["createdAt", "updatedAt"]')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					objectContaining({
						id: any(String),
						createdAt: firstWorkflowCreatedAt,
						updatedAt: firstWorkflowUpdatedAt,
					}),
					objectContaining({
						id: any(String),
						createdAt: secondWorkflowCreatedAt,
						updatedAt: secondWorkflowUpdatedAt,
					}),
				]),
			});
		});

		test('should select workflow field: versionId', async () => {
			const firstWorkflowVersionId = 'e95ccdde-2b4e-4fd0-8834-220a2b5b4353';
			const secondWorkflowVersionId = 'd099b8dc-b1d8-4b2d-9b02-26f32c0ee785';

			await createWorkflow({ versionId: firstWorkflowVersionId }, owner);
			await createWorkflow({ versionId: secondWorkflowVersionId }, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('select=["versionId"]')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					{ id: any(String), versionId: firstWorkflowVersionId },
					{ id: any(String), versionId: secondWorkflowVersionId },
				]),
			});
		});

		test('should select workflow field: ownedBy', async () => {
			await createWorkflow({}, owner);
			await createWorkflow({}, owner);
			const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
				owner.id,
			);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('select=["ownedBy"]')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					{
						id: any(String),
						homeProject: {
							id: ownerPersonalProject.id,
							name: owner.createPersonalProjectName(),
							icon: null,
							type: ownerPersonalProject.type,
						},
						sharedWithProjects: [],
					},
					{
						id: any(String),
						homeProject: {
							id: ownerPersonalProject.id,
							name: owner.createPersonalProjectName(),
							icon: null,
							type: ownerPersonalProject.type,
						},
						sharedWithProjects: [],
					},
				]),
			});
		});

		test('should select workflow field: parentFolder', async () => {
			const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
				owner.id,
			);
			const folder = await createFolder(ownerPersonalProject, { name: 'Folder 1' });

			await createWorkflow({ parentFolder: folder }, owner);
			await createWorkflow({}, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('select=["parentFolder"]')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					{
						id: any(String),
						parentFolder: {
							id: folder.id,
							name: folder.name,
							parentFolderId: null,
						},
					},
					{
						id: any(String),
						parentFolder: null,
					},
				]),
			});
		});
	});

	describe('sortBy', () => {
		test('should fail when trying to sort by non sortable column', async () => {
			await authOwnerAgent.get('/workflows').query('sortBy=nonSortableColumn:asc').expect(500);
		});

		test('should sort by createdAt column', async () => {
			await createWorkflow({ name: 'First' }, owner);
			await createWorkflow({ name: 'Second' }, owner);

			let response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=createdAt:asc')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					expect.objectContaining({ name: 'First' }),
					expect.objectContaining({ name: 'Second' }),
				]),
			});

			response = await authOwnerAgent.get('/workflows').query('sortBy=createdAt:desc').expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					expect.objectContaining({ name: 'Second' }),
					expect.objectContaining({ name: 'First' }),
				]),
			});
		});

		test('should sort by name column', async () => {
			await createWorkflow({ name: 'a' }, owner);
			await createWorkflow({ name: 'b' }, owner);
			await createWorkflow({ name: 'My workflow' }, owner);

			let response;

			response = await authOwnerAgent.get('/workflows').query('sortBy=name:asc').expect(200);

			expect(response.body).toEqual({
				count: 3,
				data: [
					expect.objectContaining({ name: 'a' }),
					expect.objectContaining({ name: 'b' }),
					expect.objectContaining({ name: 'My workflow' }),
				],
			});

			response = await authOwnerAgent.get('/workflows').query('sortBy=name:desc').expect(200);

			expect(response.body).toEqual({
				count: 3,
				data: [
					expect.objectContaining({ name: 'My workflow' }),
					expect.objectContaining({ name: 'b' }),
					expect.objectContaining({ name: 'a' }),
				],
			});
		});

		test('should sort by updatedAt column', async () => {
			const futureDate = new Date();
			futureDate.setDate(futureDate.getDate() + 10);

			await createWorkflow({ name: 'First', updatedAt: futureDate }, owner);
			await createWorkflow({ name: 'Second' }, owner);

			let response;

			response = await authOwnerAgent.get('/workflows').query('sortBy=updatedAt:asc').expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					expect.objectContaining({ name: 'Second' }),
					expect.objectContaining({ name: 'First' }),
				]),
			});

			response = await authOwnerAgent.get('/workflows').query('sortBy=name:desc').expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: arrayContaining([
					expect.objectContaining({ name: 'First' }),
					expect.objectContaining({ name: 'Second' }),
				]),
			});
		});
	});

	describe('pagination', () => {
		beforeEach(async () => {
			await createWorkflow({ name: 'Workflow 1' }, owner);
			await createWorkflow({ name: 'Workflow 2' }, owner);
			await createWorkflow({ name: 'Workflow 3' }, owner);
			await createWorkflow({ name: 'Workflow 4' }, owner);
			await createWorkflow({ name: 'Workflow 5' }, owner);
		});

		test('should fail when skip is provided without take', async () => {
			await authOwnerAgent.get('/workflows').query('skip=2').expect(500);
		});

		test('should handle skip with take parameter', async () => {
			const response = await authOwnerAgent
				.get('/workflows')
				.query('skip=2&take=2&sortBy=name:asc')
				.expect(200);

			const body = response.body as { data: WorkflowEntity[]; count: number };
			expect(body.count).toBe(5);
			expect(body.data).toHaveLength(2);
			const names = body.data.map((item) => item.name);
			expect(names).toEqual(expect.arrayContaining(['Workflow 3', 'Workflow 4']));
		});

		test('should handle pagination with sorting', async () => {
			const response = await authOwnerAgent
				.get('/workflows')
				.query('take=2&skip=1&sortBy=name:desc');

			const body = response.body as { data: WorkflowEntity[]; count: number };
			expect(body.count).toBe(5);
			expect(body.data).toHaveLength(2);
			const names = body.data.map((item) => item.name);
			expect(names).toEqual(expect.arrayContaining(['Workflow 3', 'Workflow 4']));
		});

		test('should handle pagination with filtering', async () => {
			// Create additional workflows with specific names for filtering
			await createWorkflow({ name: 'Special Workflow 1' }, owner);
			await createWorkflow({ name: 'Special Workflow 2' }, owner);
			await createWorkflow({ name: 'Special Workflow 3' }, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=name:asc&take=2&skip=1')
				.query('filter={"query":"Special"}')
				.expect(200);

			const body = response.body as { data: WorkflowEntity[]; count: number };
			expect(body.count).toBe(3); // Only 3 'Special' workflows exist
			expect(body.data).toHaveLength(2); // // We skip 1
			const names = body.data.map((item) => item.name);
			expect(names).toEqual(expect.arrayContaining(['Special Workflow 2', 'Special Workflow 3']));
		});

		test('should return empty array when pagination exceeds total count', async () => {
			const response = await authOwnerAgent.get('/workflows').query('take=2&skip=10').expect(200);

			expect(response.body.data).toHaveLength(0);
			expect(response.body.count).toBe(5);
		});

		test('should return all results when no pagination parameters are provided', async () => {
			const response = await authOwnerAgent.get('/workflows').expect(200);

			expect(response.body.data).toHaveLength(5);
			expect(response.body.count).toBe(5);
		});
	});
});

describe('GET /workflows?onlySharedWithMe=true', () => {
	test('should return only workflows shared with me', async () => {
		const memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
			member.id,
		);

		const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

		await createWorkflow({ name: 'First' }, owner);
		await createWorkflow({ name: 'Second' }, member);
		const workflow3 = await createWorkflow({ name: 'Third' }, member);

		await shareWorkflowWithUsers(workflow3, [owner]);

		const response = await authOwnerAgent.get('/workflows').query({ onlySharedWithMe: true });
		expect(200);

		expect(response.body).toEqual({
			count: 1,
			data: arrayContaining([
				objectContaining({
					id: any(String),
					name: 'Third',
					active: false,
					activeVersionId: null,
					createdAt: any(String),
					updatedAt: any(String),
					versionId: any(String),
					parentFolder: null,
					homeProject: {
						id: memberPersonalProject.id,
						name: member.createPersonalProjectName(),
						icon: null,
						type: memberPersonalProject.type,
					},
					sharedWithProjects: [
						objectContaining({
							id: any(String),
							name: ownerPersonalProject.name,
							icon: null,
							type: ownerPersonalProject.type,
						}),
					],
				}),
			]),
		});
	});
});

describe('GET /workflows?includeFolders=true', () => {
	test('should return zero workflows and folders if none exist', async () => {
		const response = await authOwnerAgent.get('/workflows').query({ includeFolders: true });

		expect(response.body).toEqual({ count: 0, data: [] });
	});

	test('should return workflows and folders', async () => {
		const credential = await saveCredential(randomCredentialPayload(), {
			user: owner,
			role: 'credential:owner',
		});
		const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

		const nodes: INode[] = [
			{
				id: uuid(),
				name: 'Action Network',
				type: 'n8n-nodes-base.actionNetwork',
				parameters: {},
				typeVersion: 1,
				position: [0, 0],
				credentials: {
					actionNetworkApi: {
						id: credential.id,
						name: credential.name,
					},
				},
			},
		];

		const tag = await createTag({ name: 'A' });

		await createWorkflow({ name: 'First', nodes, tags: [tag] }, owner);
		await createWorkflow({ name: 'Second' }, owner);
		await createFolder(ownerPersonalProject, { name: 'Folder' });

		const response = await authOwnerAgent.get('/workflows').query({ includeFolders: true });
		expect(200);

		expect(response.body).toEqual({
			count: 3,
			data: arrayContaining([
				objectContaining({
					resource: 'workflow',
					id: any(String),
					name: 'First',
					active: false,
					activeVersionId: null,
					createdAt: any(String),
					updatedAt: any(String),
					tags: [{ id: any(String), name: 'A' }],
					versionId: any(String),
					homeProject: {
						id: ownerPersonalProject.id,
						name: owner.createPersonalProjectName(),
						icon: null,
						type: ownerPersonalProject.type,
					},
					sharedWithProjects: [],
				}),
				objectContaining({
					id: any(String),
					name: 'Second',
					activeVersionId: null,
					createdAt: any(String),
					updatedAt: any(String),
					tags: [],
					versionId: any(String),
					homeProject: {
						id: ownerPersonalProject.id,
						name: owner.createPersonalProjectName(),
						icon: null,
						type: ownerPersonalProject.type,
					},
					sharedWithProjects: [],
				}),
				objectContaining({
					resource: 'folder',
					id: any(String),
					name: 'Folder',
					createdAt: any(String),
					updatedAt: any(String),
					tags: [],
					homeProject: {
						id: ownerPersonalProject.id,
						name: owner.createPersonalProjectName(),
						icon: null,
						type: ownerPersonalProject.type,
					},
					parentFolder: null,
					workflowCount: 0,
					subFolderCount: 0,
				}),
			]),
		});

		const found = response.body.data.find(
			(w: ListQueryDb.Workflow.WithOwnership) => w.name === 'First',
		);

		expect(found.nodes).toBeUndefined();
		expect(found.sharedWithProjects).toHaveLength(0);
		expect(found.usedCredentials).toBeUndefined();
	});

	test('should NOT returns folders without folder:list scope', async () => {
		const [member1, member2] = await createManyUsers(2, {
			role: { slug: 'global:member' },
		});

		const teamProject = await createTeamProject(undefined, member1);
		await linkUserToProject(member2, teamProject, folderListMissingRole.slug);

		const [savedWorkflow1, savedWorkflow2, savedFolder1] = await Promise.all([
			createWorkflow({ name: 'First' }, teamProject),
			createWorkflow({ name: 'Second' }, teamProject),
			createFolder(teamProject, { name: 'Folder' }),
		]);

		{
			const response = await testServer
				.authAgentFor(member1)
				.get(
					`/workflows?filter={ "projectId": "${teamProject.id}" }&includeScopes=true&includeFolders=true`,
				);

			expect(response.statusCode).toBe(200);
			// project owner
			expect(response.body.data.length).toBe(3);

			const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
			const wf1 = workflows.find((wf) => wf.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((wf) => wf.id === savedWorkflow2.id)!;
			const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(f1.id).toBe(savedFolder1.id);
		}

		{
			const response = await testServer
				.authAgentFor(member2)
				.get(
					`/workflows?filter={ "projectId": "${teamProject.id}" }&includeScopes=true&includeFolders=true`,
				);

			expect(response.statusCode).toBe(200);
			// project member
			expect(response.body.data.length).toBe(2);

			const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
			const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;
			const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(f1).toBeUndefined();
		}
	});

	test('should return workflows with scopes and folders when ?includeScopes=true', async () => {
		const [member1, member2] = await createManyUsers(2, {
			role: { slug: 'global:member' },
		});

		const teamProject = await createTeamProject(undefined, member1);
		await linkUserToProject(member2, teamProject, 'project:editor');

		const credential = await saveCredential(randomCredentialPayload(), {
			user: owner,
			role: 'credential:owner',
		});

		const nodes: INode[] = [
			{
				id: uuid(),
				name: 'Action Network',
				type: 'n8n-nodes-base.actionNetwork',
				parameters: {},
				typeVersion: 1,
				position: [0, 0],
				credentials: {
					actionNetworkApi: {
						id: credential.id,
						name: credential.name,
					},
				},
			},
		];

		const tag = await createTag({ name: 'A' });

		const [savedWorkflow1, savedWorkflow2, savedFolder1] = await Promise.all([
			createWorkflow({ name: 'First', nodes, tags: [tag] }, teamProject),
			createWorkflow({ name: 'Second' }, member2),
			createFolder(teamProject, { name: 'Folder' }),
		]);

		await shareWorkflowWithProjects(savedWorkflow2, [{ project: teamProject }]);

		{
			const response = await testServer
				.authAgentFor(member1)
				.get('/workflows?includeScopes=true&includeFolders=true');

			expect(response.statusCode).toBe(200);
			expect(response.body.data.length).toBe(3);

			const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
			const wf1 = workflows.find((wf) => wf.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((wf) => wf.id === savedWorkflow2.id)!;
			const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf1.scopes).toEqual(
				[
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:update',
				].sort(),
			);

			// Shared workflow
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(wf2.scopes).toEqual(
				[
					'workflow:read',
					'workflow:update',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:publish',
				].sort(),
			);

			expect(f1.id).toBe(savedFolder1.id);
		}

		{
			const response = await testServer
				.authAgentFor(member2)
				.get('/workflows?includeScopes=true&includeFolders=true');

			expect(response.statusCode).toBe(200);
			expect(response.body.data.length).toBe(3);

			const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
			const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;
			const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf1.scopes).toEqual([
				'workflow:delete',
				'workflow:execute',
				'workflow:execute-chat',
				'workflow:publish',
				'workflow:read',
				'workflow:update',
			]);

			// Shared workflow
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(wf2.scopes).toEqual(
				[
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:share',
					'workflow:update',
				].sort(),
			);

			expect(f1.id).toBe(savedFolder1.id);
		}

		{
			const response = await testServer
				.authAgentFor(owner)
				.get('/workflows?includeScopes=true&includeFolders=true');

			expect(response.statusCode).toBe(200);
			expect(response.body.data.length).toBe(3);

			const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
			const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
			const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;
			const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;

			// Team workflow
			expect(wf1.id).toBe(savedWorkflow1.id);
			expect(wf1.scopes).toEqual(
				[
					'workflow:create',
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:list',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:share',
					'workflow:update',
				].sort(),
			);

			// Shared workflow
			expect(wf2.id).toBe(savedWorkflow2.id);
			expect(wf2.scopes).toEqual(
				[
					'workflow:create',
					'workflow:delete',
					'workflow:execute',
					'workflow:execute-chat',
					'workflow:list',
					'workflow:move',
					'workflow:publish',
					'workflow:read',
					'workflow:share',
					'workflow:update',
				].sort(),
			);

			expect(f1.id).toBe(savedFolder1.id);
		}
	});

	describe('filter', () => {
		test('should filter workflows and folders by field: query', async () => {
			const workflow1 = await createWorkflow({ name: 'First' }, owner);
			await createWorkflow({ name: 'Second' }, owner);

			const ownerProject = await getPersonalProject(owner);

			const folder1 = await createFolder(ownerProject, { name: 'First' });
			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={"query":"First"}&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: [
					objectContaining({ id: folder1.id, name: 'First' }),
					objectContaining({ id: workflow1.id, name: 'First' }),
				],
			});
		});

		test('should filter workflows and folders by field: active', async () => {
			const workflow1 = await createActiveWorkflow({}, owner);
			await createWorkflow({}, owner);

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "active": true }&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 1,
				data: [
					objectContaining({ id: workflow1.id, active: true, versionId: workflow1.versionId }),
				],
			});
		});

		test('should filter workflows and folders by field: tags (AND operator)', async () => {
			const baseDate = DateTime.now();

			const workflow1 = await createWorkflow(
				{ name: 'First', updatedAt: baseDate.toJSDate() },
				owner,
			);
			const workflow2 = await createWorkflow(
				{ name: 'Second', updatedAt: baseDate.toJSDate() },
				owner,
			);

			const ownerProject = await getPersonalProject(owner);

			const tagA = await createTag(
				{
					name: 'A',
				},
				workflow1,
			);
			const tagB = await createTag(
				{
					name: 'B',
				},
				workflow1,
			);

			await createTag({ name: 'C' }, workflow2);

			await createFolder(ownerProject, {
				name: 'First Folder',
				tags: [tagA, tagB],
				updatedAt: baseDate.plus({ minutes: 2 }).toJSDate(),
			});

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "tags": ["A", "B"] }&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 2,
				data: [
					objectContaining({
						name: 'First Folder',
						tags: expect.arrayContaining([
							{ id: any(String), name: 'A' },
							{ id: any(String), name: 'B' },
						]),
					}),
					objectContaining({
						name: 'First',
						tags: expect.arrayContaining([
							{ id: any(String), name: 'A' },
							{ id: any(String), name: 'B' },
						]),
					}),
				],
			});
		});

		test('should filter workflows by projectId', async () => {
			const workflow = await createWorkflow({ name: 'First' }, owner);
			const pp = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

			const folder = await createFolder(pp, {
				name: 'First Folder',
			});

			const response1 = await authOwnerAgent
				.get('/workflows')
				.query(`filter={ "projectId": "${pp.id}" }&includeFolders=true`)
				.expect(200);

			expect(response1.body.data).toHaveLength(2);
			expect(response1.body.data[0].id).toBe(folder.id);
			expect(response1.body.data[1].id).toBe(workflow.id);

			const response2 = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "projectId": "Non-Existing Project ID" }&includeFolders=true')
				.expect(200);

			expect(response2.body.data).toHaveLength(0);
		});

		test('should filter workflows by parentFolderId and its descendants when filtering by query', async () => {
			const pp = await projectRepository.getPersonalProjectForUserOrFail(owner.id);

			await createFolder(pp, {
				name: 'Root Folder 1',
			});

			const rootFolder2 = await createFolder(pp, {
				name: 'Root Folder 2',
			});

			await createFolder(pp, {
				name: 'Root Folder 3',
			});

			const subfolder1 = await createFolder(pp, {
				name: 'Root folder 2 subfolder 1 key',
				parentFolder: rootFolder2,
			});

			await createWorkflow(
				{
					name: 'Workflow 1 key',
					parentFolder: rootFolder2,
				},
				pp,
			);

			await createWorkflow(
				{
					name: 'workflow 2 key',
					parentFolder: rootFolder2,
				},
				pp,
			);

			await createWorkflow(
				{
					name: 'workflow 3 key',
					parentFolder: subfolder1,
				},
				pp,
			);

			const filter2Response = await authOwnerAgent
				.get('/workflows')
				.query(
					`filter={ "projectId": "${pp.id}", "parentFolderId": "${rootFolder2.id}", "query": "key" }&includeFolders=true`,
				);

			expect(filter2Response.body.count).toBe(4);
			expect(filter2Response.body.data).toHaveLength(4);
			expect(
				filter2Response.body.data.filter((w: WorkflowFolderUnionFull) => w.resource === 'workflow'),
			).toHaveLength(3);
			expect(
				filter2Response.body.data.filter((w: WorkflowFolderUnionFull) => w.resource === 'folder'),
			).toHaveLength(1);
		});

		test('should return homeProject when filtering workflows and folders by projectId', async () => {
			const workflow = await createWorkflow({ name: 'First' }, member);
			const pp = await getPersonalProject(member);
			const folder = await createFolder(pp, {
				name: 'First Folder',
			});

			const response = await authMemberAgent
				.get('/workflows')
				.query(`filter={ "projectId": "${pp.id}" }&includeFolders=true`)
				.expect(200);

			expect(response.body.data).toHaveLength(2);
			expect(response.body.data[0].id).toBe(folder.id);
			expect(response.body.data[0].homeProject).not.toBeNull();
			expect(response.body.data[1].id).toBe(workflow.id);
			expect(response.body.data[1].homeProject).not.toBeNull();
		});

		test('should filter workflows and folders by nodeTypes', async () => {
			const pp = await getPersonalProject(owner);

			const httpWorkflow = await createWorkflow(
				{
					name: 'HTTP Workflow',
					nodes: [
						{
							id: uuid(),
							name: 'HTTP Request',
							type: 'n8n-nodes-base.httpRequest',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			await createWorkflow(
				{
					name: 'Slack Workflow',
					nodes: [
						{
							id: uuid(),
							name: 'Slack',
							type: 'n8n-nodes-base.slack',
							parameters: {},
							typeVersion: 1,
							position: [0, 0],
						},
					],
				},
				owner,
			);

			const folder = await createFolder(pp, { name: 'Test Folder' });

			const response = await authOwnerAgent
				.get('/workflows')
				.query('filter={ "nodeTypes": ["n8n-nodes-base.httpRequest"] }&includeFolders=true')
				.expect(200);

			expect(response.body.data).toHaveLength(2); // 1 folder + 1 matching workflow
			const workflowItems = response.body.data.filter((item: any) => item.resource === 'workflow');
			const folderItems = response.body.data.filter((item: any) => item.resource === 'folder');

			expect(workflowItems).toHaveLength(1);
			expect(workflowItems[0].id).toBe(httpWorkflow.id);
			expect(folderItems).toHaveLength(1);
			expect(folderItems[0].id).toBe(folder.id);
		});
	});

	describe('sortBy', () => {
		test('should fail when trying to sort by non sortable column', async () => {
			await authOwnerAgent
				.get('/workflows')
				.query('sortBy=nonSortableColumn:asc&?includeFolders=true')
				.expect(500);
		});

		test('should sort by createdAt column', async () => {
			await createWorkflow({ name: 'First' }, owner);
			await createWorkflow({ name: 'Second' }, owner);
			const pp = await getPersonalProject(owner);
			await createFolder(pp, {
				name: 'First Folder',
			});

			await createFolder(pp, {
				name: 'Z Folder',
			});

			let response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=createdAt:asc&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 4,
				data: arrayContaining([
					expect.objectContaining({ name: 'First Folder' }),
					expect.objectContaining({ name: 'Z Folder' }),
					expect.objectContaining({ name: 'First' }),
					expect.objectContaining({ name: 'Second' }),
				]),
			});

			response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=createdAt:asc&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 4,
				data: arrayContaining([
					expect.objectContaining({ name: 'Z Folder' }),
					expect.objectContaining({ name: 'First Folder' }),
					expect.objectContaining({ name: 'Second' }),
					expect.objectContaining({ name: 'First' }),
				]),
			});
		});

		test('should sort by name column', async () => {
			await createWorkflow({ name: 'a' }, owner);
			await createWorkflow({ name: 'b' }, owner);
			await createWorkflow({ name: 'My workflow' }, owner);
			const pp = await getPersonalProject(owner);
			await createFolder(pp, {
				name: 'a Folder',
			});

			await createFolder(pp, {
				name: 'Z Folder',
			});

			let response;

			response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=name:asc&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 5,
				data: [
					expect.objectContaining({ name: 'a Folder' }),
					expect.objectContaining({ name: 'Z Folder' }),
					expect.objectContaining({ name: 'a' }),
					expect.objectContaining({ name: 'b' }),
					expect.objectContaining({ name: 'My workflow' }),
				],
			});

			response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=name:desc&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 5,
				data: [
					expect.objectContaining({ name: 'Z Folder' }),
					expect.objectContaining({ name: 'a Folder' }),
					expect.objectContaining({ name: 'My workflow' }),
					expect.objectContaining({ name: 'b' }),
					expect.objectContaining({ name: 'a' }),
				],
			});
		});

		test('should sort by updatedAt column', async () => {
			const baseDate = DateTime.now();

			const pp = await getPersonalProject(owner);
			await createFolder(pp, {
				name: 'Folder',
			});
			await createFolder(pp, {
				name: 'Z Folder',
			});
			await createWorkflow(
				{ name: 'Second', updatedAt: baseDate.plus({ minutes: 1 }).toJSDate() },
				owner,
			);
			await createWorkflow(
				{ name: 'First', updatedAt: baseDate.plus({ minutes: 2 }).toJSDate() },
				owner,
			);

			let response;

			response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=updatedAt:asc&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 4,
				data: arrayContaining([
					expect.objectContaining({ name: 'Folder' }),
					expect.objectContaining({ name: 'Z Folder' }),
					expect.objectContaining({ name: 'Second' }),
					expect.objectContaining({ name: 'First' }),
				]),
			});

			response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=updatedAt:desc&includeFolders=true')
				.expect(200);

			expect(response.body).toEqual({
				count: 4,
				data: arrayContaining([
					expect.objectContaining({ name: 'Z Folder' }),
					expect.objectContaining({ name: 'Folder' }),
					expect.objectContaining({ name: 'First' }),
					expect.objectContaining({ name: 'Second' }),
				]),
			});
		});
	});

	describe('pagination', () => {
		beforeEach(async () => {
			const pp = await getPersonalProject(owner);
			await createWorkflow({ name: 'Workflow 1' }, owner);
			await createWorkflow({ name: 'Workflow 2' }, owner);
			await createWorkflow({ name: 'Workflow 3' }, owner);
			await createWorkflow({ name: 'Workflow 4' }, owner);
			await createWorkflow({ name: 'Workflow 5' }, owner);
			await createFolder(pp, {
				name: 'Folder 1',
			});
		});

		test('should fail when skip is provided without take', async () => {
			await authOwnerAgent.get('/workflows?includeFolders=true').query('skip=2').expect(500);
		});

		test('should handle skip with take parameter', async () => {
			const response = await authOwnerAgent
				.get('/workflows')
				.query('skip=2&take=4&includeFolders=true&sortBy=name:asc');

			const body = response.body as { data: WorkflowEntity[]; count: number };
			expect(body.count).toBe(6);
			expect(body.data).toHaveLength(4);
			const names = body.data.map((item) => item.name);
			expect(names).toEqual(
				expect.arrayContaining(['Workflow 2', 'Workflow 3', 'Workflow 4', 'Workflow 5']),
			);
		});

		test('should handle pagination with sorting', async () => {
			const response = await authOwnerAgent
				.get('/workflows')
				.query('skip=1&take=2&sortBy=name:desc&includeFolders=true');

			expect(response.body.data).toHaveLength(2);
			expect(response.body.count).toBe(6);
			expect(response.body.data[0].name).toBe('Workflow 5');
			expect(response.body.data[1].name).toBe('Workflow 4');
		});

		test('should handle pagination with filtering', async () => {
			const pp = await getPersonalProject(owner);
			await createWorkflow({ name: 'Special Workflow 1' }, owner);
			await createWorkflow({ name: 'Special Workflow 2' }, owner);
			await createWorkflow({ name: 'Special Workflow 3' }, owner);
			await createFolder(pp, {
				name: 'Special Folder 1',
			});

			const response = await authOwnerAgent
				.get('/workflows')
				.query('sortBy=name:asc&take=2&skip=1')
				.query('filter={"query":"Special"}&includeFolders=true')
				.expect(200);

			const body = response.body as { data: WorkflowEntity[]; count: number };
			expect(body.count).toBe(4);
			expect(body.data).toHaveLength(2);
			const names = body.data.map((item) => item.name);
			expect(names).toEqual(expect.arrayContaining(['Special Workflow 1', 'Special Workflow 2']));
		});

		test('should return empty array when pagination exceeds total count', async () => {
			const response = await authOwnerAgent
				.get('/workflows')
				.query('take=2&skip=10&includeFolders=true')
				.expect(200);

			expect(response.body.data).toHaveLength(0);
			expect(response.body.count).toBe(6);
		});

		test('should return all results when no pagination parameters are provided', async () => {
			const response = await authOwnerAgent
				.get('/workflows')
				.query('includeFolders=true')
				.expect(200);

			expect(response.body.data).toHaveLength(6);
			expect(response.body.count).toBe(6);
		});
	});
});

describe('PATCH /workflows/:workflowId', () => {
	test('should always create workflow history version on nodes and connection changes', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const payload = {
			name: 'name updated',
			nodes: [
				{
					id: 'uuid-1234',
					parameters: {},
					name: 'Start',
					type: 'n8n-nodes-base.manualTrigger',
					typeVersion: 1,
					position: [240, 300],
				},
				{
					id: 'uuid-1234',
					parameters: {},
					name: 'Cron',
					type: 'n8n-nodes-base.cron',
					typeVersion: 1,
					position: [400, 300],
				},
			],
			connections: {},
			staticData: '{"id":1}',
			settings: {
				saveExecutionProgress: false,
				saveManualExecutions: false,
				saveDataErrorExecution: 'all',
				saveDataSuccessExecution: 'all',
				executionTimeout: 3600,
				timezone: 'America/New_York',
				timeSavedMode: 'fixed',
				timeSavedPerExecution: 10,
			},
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		const {
			data: { id, versionId: updatedVersionId },
		} = response.body as { data: WorkflowEntity };

		expect(response.statusCode).toBe(200);

		expect(id).toBe(workflow.id);
		const versions = await workflowHistoryRepository.find({ where: { workflowId: id } });
		expect(versions).toHaveLength(2);
		const newVersion = versions.find((v) => v.versionId === updatedVersionId);
		expect(newVersion).not.toBeNull();
		expect(newVersion!.connections).toEqual(payload.connections);
		expect(newVersion!.nodes).toEqual(payload.nodes);
	});

	test('should broadcast workflow update to collaborators', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const payload = {
			name: 'name updated',
		};

		await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(collaborationService.broadcastWorkflowUpdate).toHaveBeenCalledWith(
			workflow.id,
			owner.id,
		);
	});

	test('should not create workflow history version on other changes', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const payload = {
			name: 'name updated',
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		const {
			data: { id },
		} = response.body as { data: WorkflowEntity };

		expect(response.statusCode).toBe(200);

		expect(id).toBe(workflow.id);
		const versions = await workflowHistoryRepository.find({ where: { workflowId: id } });
		expect(versions).toHaveLength(1);
		expect(versions[0].versionId).toBe(workflow.versionId);
	});

	test('should not create workflow history version if nodes and connections did not change', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const payload = {
			name: 'name updated',
			nodes: workflow.nodes,
			connections: workflow.connections,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		const {
			data: { id },
		} = response.body as { data: WorkflowEntity };

		expect(response.statusCode).toBe(200);

		expect(id).toBe(workflow.id);
		const versions = await workflowHistoryRepository.find({ where: { workflowId: id } });
		expect(versions).toHaveLength(1);
		expect(versions[0].versionId).toBe(workflow.versionId);
	});

	test('should set autosaved: true in workflow history when autosaved parameter is sent on update', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const payload = {
			nodes: [
				{
					id: 'uuid-5678',
					parameters: {},
					name: 'Cron',
					type: 'n8n-nodes-base.cron',
					typeVersion: 1,
					position: [400, 300],
				},
			],
			connections: {},
			autosaved: true,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id, versionId: updatedVersionId },
		} = response.body as { data: WorkflowEntity };

		const versions = await workflowHistoryRepository.find({ where: { workflowId: id } });
		expect(versions).toHaveLength(2);
		const newVersion = versions.find((v) => v.versionId === updatedVersionId);
		expect(newVersion).not.toBeNull();
		expect(newVersion!.autosaved).toBe(true);
	});

	test('should set autosaved: false in workflow history when autosaved is not sent on update', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const payload = {
			nodes: [
				{
					id: 'uuid-5678',
					parameters: {},
					name: 'Cron',
					type: 'n8n-nodes-base.cron',
					typeVersion: 1,
					position: [400, 300],
				},
			],
			connections: {},
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);

		const {
			data: { id, versionId: updatedVersionId },
		} = response.body as { data: WorkflowEntity };

		const versions = await workflowHistoryRepository.find({ where: { workflowId: id } });
		expect(versions).toHaveLength(2);
		const newVersion = versions.find((v) => v.versionId === updatedVersionId);
		expect(newVersion).not.toBeNull();
		expect(newVersion!.autosaved).toBe(false);
	});

	test('should ignore provided version id', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const versionId = uuid();
		const payload = {
			description: 'description updated',
			versionId,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		const {
			data: { id: workflowId },
		} = response.body as { data: WorkflowEntity };

		expect(response.statusCode).toBe(200);

		expect(workflowId).toBe(workflow.id);
		const versions = await workflowHistoryRepository.find({ where: { workflowId } });
		expect(versions).toHaveLength(1);
		expect(versions[0].versionId).toBe(workflow.versionId);
	});

	test('should update the version counter', async () => {
		const workflow = await createWorkflow({}, owner);
		const payload = {
			name: 'name updated',
			versionId: workflow.versionId,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		const {
			data: { id, versionCounter },
		} = response.body as { data: WorkflowEntity };

		expect(response.statusCode).toBe(200);

		expect(id).toBe(workflow.id);
		expect(versionCounter).toBe(workflow.versionCounter + 1);
	});

	test('should update workflow without updating its active version', async () => {
		const addRecordSpy = jest.spyOn(workflowPublishHistoryRepository, 'addRecord');
		const workflow = await createActiveWorkflow({}, owner);
		await setActiveVersion(workflow.id, workflow.versionId);

		const payload = {
			nodes: [],
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.remove).not.toBeCalled();
		expect(activeWorkflowManagerLike.add).not.toBeCalled();
		expect(addRecordSpy).not.toBeCalled();

		const { data } = response.body as { data: WorkflowEntity };
		expect(data.nodes).toEqual([]);
		expect(data.versionId).not.toBe(workflow.versionId); // New version created
		expect(data.activeVersionId).toBe(workflow.versionId); // Should remain active
	});

	test('should update workflow meta', async () => {
		const workflow = await createWorkflow({}, owner);
		const payload = {
			...workflow,
			meta: {
				templateCredsSetupCompleted: true,
			},
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		const { data: updatedWorkflow } = response.body as { data: WorkflowEntity };

		expect(response.statusCode).toBe(200);

		expect(updatedWorkflow.id).toBe(workflow.id);
		expect(updatedWorkflow.meta).toEqual(payload.meta);
	});

	test('should move workflow to folder', async () => {
		const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
		const folder1 = await createFolder(ownerPersonalProject, { name: 'folder1' });

		const workflow = await createWorkflow({}, owner);
		const payload = {
			versionId: workflow.versionId,
			parentFolderId: folder1.id,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);

		const updatedWorkflow = await workflowRepository.findOneOrFail({
			where: { id: workflow.id },
			relations: ['parentFolder'],
		});

		expect(updatedWorkflow.parentFolder?.id).toBe(folder1.id);
	});

	test('should move workflow to project root', async () => {
		const workflow = await createWorkflow({}, owner);
		const payload = {
			versionId: workflow.versionId,
			parentFolderId: PROJECT_ROOT,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);

		const updatedWorkflow = await workflowRepository.findOneOrFail({
			where: { id: workflow.id },
			relations: ['parentFolder'],
		});

		expect(updatedWorkflow.parentFolder).toBe(null);
	});

	test('should fail if an invalid timeSavedMode is provided', async () => {
		const workflow = await createWorkflow({}, owner);
		const payload = {
			name: 'name updated',
			versionId: workflow.versionId,
			settings: {
				timeSavedMode: 'invalid' as 'fixed' | 'dynamic',
			},
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(400);
		expect(response.body.message).toBe('Invalid timeSavedMode');
	});

	test('should fail if trying update workflow parent folder with a folder that does not belong to project', async () => {
		const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
		const memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
			member.id,
		);

		await createFolder(ownerPersonalProject, { name: 'folder1' });
		const folder2 = await createFolder(memberPersonalProject, { name: 'folder2' });

		const workflow = await createWorkflow({}, owner);
		const payload = {
			versionId: workflow.versionId,
			parentFolderId: folder2.id,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(500);
	});

	test('should not activate when updating with active: true', async () => {
		const workflow = await createWorkflow({}, owner);
		const payload = {
			versionId: workflow.versionId,
			active: true,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.add).not.toBeCalled();

		const { data } = response.body as { data: WorkflowEntity };
		expect(data.active).toBe(false);
		expect(data.activeVersionId).toBeNull();
	});

	test('should not deactivate workflow when updating with active: false', async () => {
		const addRecordSpy = jest.spyOn(workflowPublishHistoryRepository, 'addRecord');
		const workflow = await createActiveWorkflow({}, owner);
		await setActiveVersion(workflow.id, workflow.versionId);

		const payload = {
			versionId: workflow.versionId,
			active: false,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.remove).not.toBeCalled();
		expect(addRecordSpy).not.toBeCalled();

		const { data } = response.body as { data: WorkflowEntity };
		expect(data.active).toBe(true);
		expect(data.activeVersionId).toBe(workflow.versionId);
	});

	test('should not modify activeVersionId when explicitly provided', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const payload = {
			versionId: workflow.versionId,
			activeVersionId: workflow.versionId,
		};

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.add).not.toBeCalled();

		const { data } = response.body as { data: WorkflowEntity };
		expect(data.activeVersionId).toBeNull(); // Should not be activated
	});

	test('should reactivate workflow when settings change', async () => {
		const workflow = await createActiveWorkflow({}, owner);
		await setActiveVersion(workflow.id, workflow.versionId);

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send({
			versionId: workflow.versionId,
			settings: {
				timezone: 'America/New_York',
			},
		});

		expect(response.statusCode).toBe(200);

		expect(activeWorkflowManagerLike.remove).toHaveBeenCalledWith(workflow.id);
		expect(activeWorkflowManagerLike.add).toHaveBeenCalledWith(workflow.id, 'update');
	});

	test('should not reactivate when settings unchanged', async () => {
		const workflow = await createActiveWorkflow({}, owner);
		await setActiveVersion(workflow.id, workflow.versionId);

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send({
			versionId: workflow.versionId,
			name: 'New Name',
		});

		expect(response.statusCode).toBe(200);

		expect(activeWorkflowManagerLike.remove).not.toHaveBeenCalled();
		expect(activeWorkflowManagerLike.add).not.toHaveBeenCalled();
	});

	test('should not reactivate inactive workflow even when settings change', async () => {
		const workflow = await createWorkflow({}, owner);

		const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send({
			versionId: workflow.versionId,
			settings: {
				timezone: 'America/New_York',
			},
		});

		expect(response.statusCode).toBe(200);

		expect(activeWorkflowManagerLike.remove).not.toHaveBeenCalled();
		expect(activeWorkflowManagerLike.add).not.toHaveBeenCalled();
	});

	describe('Security: Mass Assignment Protection on Update', () => {
		test.each([
			{
				field: 'triggerCount' as const,
				initialValue: 0,
				maliciousValue: 999,
				description: 'billing bypass',
				assertionType: 'exact' as const,
			},
			{
				field: 'versionCounter' as const,
				initialValue: 1,
				maliciousValue: 999,
				description: 'versioning manipulation',
				assertionType: 'notEqual' as const,
			},
			{
				field: 'isArchived' as const,
				initialValue: false,
				maliciousValue: true,
				description: 'archiving via update',
				assertionType: 'exact' as const,
			},
		])(
			'should prevent modifying $field via API ($description)',
			async ({ field, initialValue, maliciousValue, assertionType }) => {
				const workflow = await createWorkflow({}, owner);

				expect(workflow[field]).toBe(initialValue);

				const payload = {
					versionId: workflow.versionId,
					name: 'Updated Name',
					[field]: maliciousValue,
				};

				const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);
				expect(response.statusCode).toBe(200);

				const updatedWorkflow = await workflowRepository.findOneBy({ id: workflow.id });

				if (assertionType === 'exact') {
					expect(updatedWorkflow?.[field]).toBe(initialValue);
				} else {
					expect(updatedWorkflow?.[field]).not.toBe(maliciousValue);
				}
				expect(updatedWorkflow?.name).toBe('Updated Name');
			},
		);

		test('should allow updating legitimate fields while blocking internal fields', async () => {
			const workflow = await createWorkflow({}, owner);

			const payload = {
				versionId: workflow.versionId,
				name: 'New Name',
				description: 'New Description',
				nodes: [
					{
						id: 'test-node',
						name: 'Test Node',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [250, 300],
						parameters: {},
					},
				],
				connections: {},
				settings: {
					saveExecutionProgress: true,
				},
				meta: { updated: true },
				// Attempt to set internal fields
				triggerCount: 999,
				versionCounter: 999,
				isArchived: true,
			};

			const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);
			expect(response.statusCode).toBe(200);

			const updatedWorkflow = await workflowRepository.findOneBy({ id: workflow.id });

			// Legitimate fields should be updated
			expect(updatedWorkflow?.name).toBe('New Name');
			expect(updatedWorkflow?.description).toBe('New Description');
			expect(updatedWorkflow?.nodes).toHaveLength(1);
			expect(updatedWorkflow?.settings).toMatchObject({ saveExecutionProgress: true });
			expect(updatedWorkflow?.meta).toMatchObject({ updated: true });

			// Internal fields should NOT be modified
			expect(updatedWorkflow?.triggerCount).not.toBe(999);
			expect(updatedWorkflow?.versionCounter).not.toBe(999);
			expect(updatedWorkflow?.isArchived).toBe(false);
		});
	});
});

describe('POST /workflows/:workflowId/activate', () => {
	test('should activate workflow with provided versionId', async () => {
		const addRecordSpy = jest.spyOn(workflowPublishHistoryRepository, 'addRecord');
		const workflow = await createWorkflowWithHistory({}, owner);
		const newVersionId = uuid();
		await createWorkflowHistoryItem(workflow.id, { versionId: newVersionId });

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: newVersionId });

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.add).toBeCalledWith(workflow.id, 'activate');

		const { data } = response.body;
		expect(data.id).toBe(workflow.id);
		expect(data.activeVersionId).toBe(newVersionId);
		expect(data.activeVersion.versionId).toBe(newVersionId);
		expect(data.activeVersion.workflowPublishHistory).toHaveLength(1);
		expect(data.activeVersion.workflowPublishHistory[0]).toMatchObject({
			event: 'activated',
			versionId: newVersionId,
		});

		expect(addRecordSpy).toBeCalledWith({
			event: 'activated',
			userId: owner.id,
			versionId: newVersionId,
			workflowId: workflow.id,
		});
	});

	test('should send activated event', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);

		const emitSpy = jest.spyOn(eventService, 'emit');
		await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId });

		expect(emitSpy).toHaveBeenCalledWith('workflow-activated', expect.anything());
	});

	test('should broadcast workflow update to collaborators', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);

		await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId });

		expect(collaborationService.broadcastWorkflowUpdate).toHaveBeenCalledWith(
			workflow.id,
			owner.id,
		);
	});

	test('should return 400 when versionId is missing', async () => {
		const workflow = await createWorkflow({}, owner);

		const response = await authOwnerAgent.post(`/workflows/${workflow.id}/activate`).send({});

		expect(response.statusCode).toBe(400);
		expect(response.body.path).toContain('versionId');
		expect(response.body.message).toContain('Required');
	});

	test('should return 404 when workflow does not exist', async () => {
		const response = await authOwnerAgent
			.post('/workflows/non-existent-id/activate')
			.send({ versionId: uuid() });

		expect(response.statusCode).toBe(404);
	});

	test('should return 404 if version does not exist', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const newVersionId = uuid();

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: newVersionId });

		expect(response.statusCode).toBe(404);
		expect(response.body.message).toBe('Version not found');
		expect(activeWorkflowManagerLike.add).not.toBeCalled();
	});

	test('should return 403 when user does not have update permission', async () => {
		const workflow = await createWorkflow({}, owner);

		const response = await authMemberAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId });

		expect(response.statusCode).toBe(403);
	});

	test('should return 403 when user lacks workflow:publish permission', async () => {
		// Create a custom role with workflow:update but not workflow:publish
		const customRole = await createCustomRoleWithScopeSlugs(['workflow:read', 'workflow:update'], {
			roleType: 'project',
			displayName: 'Custom Workflow Updater',
			description: 'Can update workflows but not publish them',
		});

		const teamProject = await createTeamProject('Test Project', owner);
		await linkUserToProject(member, teamProject, customRole.slug);

		const workflow = await createWorkflowWithHistory({}, teamProject);

		const response = await authMemberAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId });

		expect(response.statusCode).toBe(403);

		const workflowAfter = await workflowRepository.findOne({ where: { id: workflow.id } });
		expect(workflowAfter?.active).toBe(false);
		expect(workflowAfter?.activeVersionId).toBeNull();
	});

	test('should set activeVersion relation when activating', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);

		await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId });

		const updatedWorkflow = await workflowRepository.findOne({
			where: { id: workflow.id },
			relations: ['activeVersion'],
		});

		expect(updatedWorkflow?.activeVersionId).toBe(workflow.versionId);
		expect(updatedWorkflow?.activeVersion).not.toBeNull();
		expect(updatedWorkflow?.activeVersion?.versionId).toBe(workflow.versionId);
	});

	test('should update version name when provided during activation', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const newVersionName = 'Production Version';

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId, name: newVersionName });

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.add).toBeCalledWith(workflow.id, 'activate');

		const { data } = response.body;
		expect(data.activeVersionId).toBe(workflow.versionId);

		const historyVersion = await workflowHistoryRepository.findOne({
			where: { workflowId: workflow.id, versionId: workflow.versionId },
		});

		expect(historyVersion?.name).toBe(newVersionName);
	});

	test('should update version description when provided during activation', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const newDescription = 'This is the stable production release';

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId, description: newDescription });

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.add).toBeCalledWith(workflow.id, 'activate');

		const { data } = response.body;
		expect(data.activeVersionId).toBe(workflow.versionId);

		const historyVersion = await workflowHistoryRepository.findOne({
			where: { workflowId: workflow.id, versionId: workflow.versionId },
		});

		expect(historyVersion?.description).toBe(newDescription);
	});

	test('should update both version name and description when provided during activation', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const newVersionName = 'Production Version';
		const newDescription = 'Major update with new features';

		const response = await authOwnerAgent.post(`/workflows/${workflow.id}/activate`).send({
			versionId: workflow.versionId,
			name: newVersionName,
			description: newDescription,
		});

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.add).toBeCalledWith(workflow.id, 'activate');

		const { data } = response.body;
		expect(data.activeVersionId).toBe(workflow.versionId);

		const historyVersion = await workflowHistoryRepository.findOne({
			where: { workflowId: workflow.id, versionId: workflow.versionId },
		});

		expect(historyVersion?.name).toBe(newVersionName);
		expect(historyVersion?.description).toBe(newDescription);
	});

	test('should not update version name and description when activation fails', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const newVersionName = 'Production Version';
		const newDescription = 'Major update with new features';

		activeWorkflowManagerLike.add.mockRejectedValueOnce(new Error('Validation failed'));

		await authOwnerAgent.post(`/workflows/${workflow.id}/activate`).send({
			versionId: workflow.versionId,
			name: newVersionName,
			description: newDescription,
		});

		const updatedVersion = await workflowHistoryRepository.findOne({
			where: { versionId: workflow.versionId },
		});

		expect(updatedVersion?.name).toBeNull();
		expect(updatedVersion?.description).toBeNull();
	});

	test('should return 400 when version name exceeds max length', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const longName = 'a'.repeat(129); // Max is 128

		const response = await authOwnerAgent.post(`/workflows/${workflow.id}/activate`).send({
			versionId: workflow.versionId,
			name: longName,
		});

		expect(response.statusCode).toBe(400);
	});

	test('should return 400 when version description exceeds max length', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const longDescription = 'a'.repeat(2049); // Max is 2048

		const response = await authOwnerAgent.post(`/workflows/${workflow.id}/activate`).send({
			versionId: workflow.versionId,
			description: longDescription,
		});

		expect(response.statusCode).toBe(400);
	});

	test('should accept version name and description at max length', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const maxName = 'a'.repeat(128);
		const maxDescription = 'b'.repeat(1000);

		const response = await authOwnerAgent.post(`/workflows/${workflow.id}/activate`).send({
			versionId: workflow.versionId,
			name: maxName,
			description: maxDescription,
		});

		expect(response.statusCode).toBe(200);

		const historyVersion = await workflowHistoryRepository.findOne({
			where: { workflowId: workflow.id, versionId: workflow.versionId },
		});

		expect(historyVersion?.name).toBe(maxName);
		expect(historyVersion?.description).toBe(maxDescription);
	});

	test('should deactivate workflow when activation fails', async () => {
		const workflow = await createActiveWorkflow({}, owner);

		const newVersionId = uuid();
		await createWorkflowHistoryItem(workflow.id, { versionId: newVersionId });

		const emitSpy = jest.spyOn(eventService, 'emit');

		activeWorkflowManagerLike.add.mockRejectedValueOnce(new Error('Activation failed'));

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: newVersionId });

		expect(response.statusCode).toBe(400);
		expect(response.body.message).toBe('Activation failed');

		const updatedWorkflow = await workflowRepository.findOne({
			where: { id: workflow.id },
			relations: ['activeVersion'],
		});

		// Workflow should be deactivated after failed activation
		expect(updatedWorkflow?.active).toBe(false);
		expect(updatedWorkflow?.activeVersionId).toBeNull();

		// Should emit deactivation event
		expect(emitSpy).toHaveBeenCalledWith('workflow-deactivated', expect.anything());

		// Verify workflow was removed once (no re-add)
		expect(activeWorkflowManagerLike.remove).toHaveBeenCalledWith(workflow.id);
		expect(activeWorkflowManagerLike.add).toHaveBeenCalledTimes(1);
	});

	test('should call active workflow manager with update mode if workflow is active', async () => {
		const workflow = await createActiveWorkflow({}, owner);

		const newVersionId = uuid();
		await createWorkflowHistoryItem(workflow.id, { versionId: newVersionId });

		await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: newVersionId });

		// First remove active version
		expect(activeWorkflowManagerLike.remove).toBeCalledWith(workflow.id);
		expect(activeWorkflowManagerLike.add).toBeCalledWith(workflow.id, 'update');
	});

	test('should call active workflow manager with activate mode if workflow is not active', async () => {
		const addRecordSpy = jest.spyOn(workflowPublishHistoryRepository, 'addRecord');
		const workflow = await createWorkflowWithHistory({}, owner);

		await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: workflow.versionId });

		expect(activeWorkflowManagerLike.remove).not.toBeCalledWith(workflow.id);
		expect(activeWorkflowManagerLike.add).toBeCalledWith(workflow.id, 'activate');
		expect(addRecordSpy).toBeCalledWith({
			event: 'activated',
			userId: owner.id,
			versionId: workflow.versionId,
			workflowId: workflow.id,
		});
	});
});

describe('POST /workflows/:workflowId/deactivate', () => {
	test('should deactivate active workflow', async () => {
		const addRecordSpy = jest.spyOn(workflowPublishHistoryRepository, 'addRecord');
		const workflow = await createActiveWorkflow({}, owner);

		const response = await authOwnerAgent.post(`/workflows/${workflow.id}/deactivate`);

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.remove).toBeCalledWith(workflow.id);

		const { data } = response.body;
		expect(data.id).toBe(workflow.id);
		expect(data.active).toBe(false);
		expect(data.activeVersionId).toBeNull();
		expect(addRecordSpy).toBeCalledWith({
			event: 'deactivated',
			userId: owner.id,
			versionId: workflow.versionId,
			workflowId: workflow.id,
		});
	});

	test('should send deactivated event', async () => {
		const workflow = await createActiveWorkflow({}, owner);

		const emitSpy = jest.spyOn(eventService, 'emit');
		await authOwnerAgent.post(`/workflows/${workflow.id}/deactivate`);

		expect(emitSpy).toHaveBeenCalledWith('workflow-deactivated', expect.anything());
	});

	test('should broadcast workflow update to collaborators', async () => {
		const workflow = await createActiveWorkflow({}, owner);

		await authOwnerAgent.post(`/workflows/${workflow.id}/deactivate`);

		expect(collaborationService.broadcastWorkflowUpdate).toHaveBeenCalledWith(
			workflow.id,
			owner.id,
		);
	});

	test('should handle deactivating already inactive workflow', async () => {
		const addRecordSpy = jest.spyOn(workflowPublishHistoryRepository, 'addRecord');
		const workflow = await createWorkflow({}, owner);

		const response = await authOwnerAgent.post(`/workflows/${workflow.id}/deactivate`);

		expect(response.statusCode).toBe(200);
		expect(activeWorkflowManagerLike.remove).not.toBeCalled();
		expect(addRecordSpy).not.toBeCalled();

		const { data } = response.body;
		expect(data.activeVersionId).toBeNull();
	});

	test('should return 404 when workflow does not exist', async () => {
		const response = await authOwnerAgent.post('/workflows/non-existent-id/deactivate');

		expect(response.statusCode).toBe(404);
	});

	test('should return 403 when user does not have update permission', async () => {
		const workflow = await createActiveWorkflow({}, owner);

		const response = await authMemberAgent.post(`/workflows/${workflow.id}/deactivate`);

		expect(response.statusCode).toBe(403);
	});

	test('should return 403 when user lacks workflow:publish permission', async () => {
		// Create a custom role with workflow:update but not workflow:publish
		const customRole = await createCustomRoleWithScopeSlugs(['workflow:read', 'workflow:update'], {
			roleType: 'project',
			displayName: 'Custom Workflow Updater',
			description: 'Can update workflows but not publish them',
		});

		const teamProject = await createTeamProject('Test Project', owner);
		await linkUserToProject(member, teamProject, customRole.slug);

		const workflow = await createActiveWorkflow({}, teamProject);

		const response = await authMemberAgent.post(`/workflows/${workflow.id}/deactivate`);

		expect(response.statusCode).toBe(403);

		const workflowAfter = await workflowRepository.findOne({ where: { id: workflow.id } });
		expect(workflowAfter?.active).toBe(true);
		expect(workflowAfter?.activeVersionId).toBe(workflow.activeVersionId);
	});

	test('should clear activeVersion relation when deactivating', async () => {
		const workflow = await createActiveWorkflow({}, owner);
		await setActiveVersion(workflow.id, workflow.versionId);

		await authOwnerAgent.post(`/workflows/${workflow.id}/deactivate`);

		const updatedWorkflow = await workflowRepository.findOne({
			where: { id: workflow.id },
			relations: ['activeVersion'],
		});

		expect(updatedWorkflow?.activeVersionId).toBeNull();
		expect(updatedWorkflow?.activeVersion).toBeNull();
	});
});

describe('POST /workflows/:workflowId/run', () => {
	let sharingSpy: jest.SpyInstance;
	let tamperingSpy: jest.SpyInstance;
	let workflow: IWorkflowBase;

	beforeAll(() => {
		const enterpriseWorkflowService = Container.get(EnterpriseWorkflowService);

		sharingSpy = jest.spyOn(License.prototype, 'isSharingEnabled');
		tamperingSpy = jest.spyOn(enterpriseWorkflowService, 'preventTampering');
		workflow = workflowRepository.create({ id: uuid() });
	});

	test('should prevent tampering if sharing is enabled', async () => {
		sharingSpy.mockReturnValue(true);

		await authOwnerAgent.post(`/workflows/${workflow.id}/run`).send({ workflowData: workflow });

		expect(tamperingSpy).toHaveBeenCalledTimes(1);
	});

	test('should skip tampering prevention if sharing is disabled', async () => {
		sharingSpy.mockReturnValue(false);

		await authOwnerAgent.post(`/workflows/${workflow.id}/run`).send({ workflowData: workflow });

		expect(tamperingSpy).not.toHaveBeenCalled();
	});
});

describe('POST /workflows/:workflowId/archive', () => {
	test('should archive workflow', async () => {
		const workflow = await createWorkflow({}, owner);
		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/archive`)
			.send()
			.expect(200);

		const {
			data: { isArchived, versionId, active },
		} = response.body;

		expect(isArchived).toBe(true);
		expect(versionId).not.toBe(workflow.versionId);
		expect(active).toBe(false);

		const updatedWorkflow = await workflowRepository.findById(workflow.id);
		expect(updatedWorkflow).not.toBeNull();
		expect(updatedWorkflow!.isArchived).toBe(true);
	});

	test('should deactivate active workflow on archive', async () => {
		const workflow = await createActiveWorkflow({}, owner);
		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/archive`)
			.send()
			.expect(200);

		const {
			data: { isArchived, versionId, activeVersionId, active },
		} = response.body;

		expect(isArchived).toBe(true);
		expect(activeVersionId).toBeNull();
		expect(active).toBe(false);
		expect(versionId).not.toBe(workflow.versionId);
		expect(activeWorkflowManagerLike.remove).toBeCalledWith(workflow.id);

		const updatedWorkflow = await workflowRepository.findById(workflow.id);
		expect(updatedWorkflow).not.toBeNull();
		expect(updatedWorkflow!.isArchived).toBe(true);
	});

	test('should broadcast workflow update to collaborators', async () => {
		const workflow = await createWorkflow({}, owner);

		await authOwnerAgent.post(`/workflows/${workflow.id}/archive`).send();

		expect(collaborationService.broadcastWorkflowUpdate).toHaveBeenCalledWith(
			workflow.id,
			owner.id,
		);
	});

	test('should not archive workflow that is already archived', async () => {
		const workflow = await createWorkflow({ isArchived: true }, owner);
		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/archive`)
			.send()
			.expect(400);

		expect(response.body.message).toBe('Workflow is already archived.');

		const updatedWorkflow = await workflowRepository.findById(workflow.id);
		expect(updatedWorkflow).not.toBeNull();
		expect(updatedWorkflow!.isArchived).toBe(true);
	});

	test('should not archive missing workflow', async () => {
		const response = await authOwnerAgent.post('/workflows/404/archive').send().expect(403);
		expect(response.body.message).toBe(
			'Could not archive the workflow - workflow was not found in your projects',
		);
	});

	test('should not archive a workflow that is not owned by the user', async () => {
		const workflow = await createWorkflow({ isArchived: false }, member);

		await testServer
			.authAgentFor(anotherMember)
			.post(`/workflows/${workflow.id}/archive`)
			.send()
			.expect(403);

		const workflowsInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowsInDb).not.toBeNull();
		expect(workflowsInDb!.isArchived).toBe(false);
		expect(sharedWorkflowsInDb).toHaveLength(1);
	});

	test("should allow the owner to archive workflows they don't own", async () => {
		const workflow = await createWorkflow({ isArchived: false }, member);

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/archive`)
			.send()
			.expect(200);

		const {
			data: { isArchived, versionId },
		} = response.body;

		expect(isArchived).toBe(true);
		expect(versionId).not.toBe(workflow.versionId);

		const workflowsInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowsInDb).not.toBeNull();
		expect(workflowsInDb!.isArchived).toBe(true);
		expect(sharedWorkflowsInDb).toHaveLength(1);
	});

	test('should save workflow history', async () => {
		const workflow = await createWorkflowWithHistory({}, owner);
		const initialVersionId = workflow.versionId;

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/archive`)
			.send()
			.expect(200);

		const {
			data: { versionId: newVersionId },
		} = response.body;

		expect(newVersionId).not.toBe(initialVersionId);

		const historyRecord = await workflowHistoryRepository.findOne({
			where: {
				workflowId: workflow.id,
				versionId: newVersionId,
			},
		});

		expect(historyRecord).not.toBeNull();
		expect(historyRecord!.nodes).toEqual(workflow.nodes);
		expect(historyRecord!.connections).toEqual(workflow.connections);
	});
});

describe('POST /workflows/:workflowId/unarchive', () => {
	test('should unarchive workflow', async () => {
		const workflow = await createWorkflow({ isArchived: true }, owner);
		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/unarchive`)
			.send()
			.expect(200);

		const {
			data: { isArchived, versionId },
		} = response.body;

		expect(isArchived).toBe(false);
		expect(versionId).not.toBe(workflow.versionId);

		const updatedWorkflow = await workflowRepository.findById(workflow.id);
		expect(updatedWorkflow).not.toBeNull();
		expect(updatedWorkflow!.isArchived).toBe(false);
	});

	test('should broadcast workflow update to collaborators', async () => {
		const workflow = await createWorkflow({ isArchived: true }, owner);

		await authOwnerAgent.post(`/workflows/${workflow.id}/unarchive`).send();

		expect(collaborationService.broadcastWorkflowUpdate).toHaveBeenCalledWith(
			workflow.id,
			owner.id,
		);
	});

	test('should not unarchive workflow that is already not archived', async () => {
		const workflow = await createWorkflow({ isArchived: false }, owner);
		await authOwnerAgent.post(`/workflows/${workflow.id}/unarchive`).send().expect(400);

		const updatedWorkflow = await workflowRepository.findById(workflow.id);
		expect(updatedWorkflow).not.toBeNull();
		expect(updatedWorkflow!.isArchived).toBe(false);
	});

	test('should not unarchive missing workflow', async () => {
		const response = await authOwnerAgent.post('/workflows/404/unarchive').send().expect(403);
		expect(response.body.message).toBe(
			'Could not unarchive the workflow - workflow was not found in your projects',
		);
	});

	test('should not unarchive a workflow that is not owned by the user', async () => {
		const workflow = await createWorkflow({ isArchived: true }, member);

		await testServer
			.authAgentFor(anotherMember)
			.post(`/workflows/${workflow.id}/unarchive`)
			.send()
			.expect(403);

		const workflowsInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowsInDb).not.toBeNull();
		expect(workflowsInDb!.isArchived).toBe(true);
		expect(sharedWorkflowsInDb).toHaveLength(1);
	});

	test("should allow the owner to unarchive workflows they don't own", async () => {
		const workflow = await createWorkflow({ isArchived: true }, member);

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/unarchive`)
			.send()
			.expect(200);

		const {
			data: { isArchived, versionId },
		} = response.body;

		expect(isArchived).toBe(false);
		expect(versionId).not.toBe(workflow.versionId);

		const workflowsInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowsInDb).not.toBeNull();
		expect(workflowsInDb!.isArchived).toBe(false);
		expect(sharedWorkflowsInDb).toHaveLength(1);
	});

	test('should save workflow history', async () => {
		const workflow = await createWorkflowWithHistory({ isArchived: true }, owner);
		const initialVersionId = workflow.versionId;

		const response = await authOwnerAgent
			.post(`/workflows/${workflow.id}/unarchive`)
			.send()
			.expect(200);

		const {
			data: { versionId: newVersionId },
		} = response.body;

		expect(newVersionId).not.toBe(initialVersionId);

		const historyRecord = await workflowHistoryRepository.findOne({
			where: {
				workflowId: workflow.id,
				versionId: newVersionId,
			},
		});

		expect(historyRecord).not.toBeNull();
		expect(historyRecord!.nodes).toEqual(workflow.nodes);
		expect(historyRecord!.connections).toEqual(workflow.connections);
	});

	test('should be able to activate workflow after unarchiving', async () => {
		const workflow = await createWorkflowWithHistory(
			{
				nodes: [
					{
						id: 'trigger-1',
						parameters: {},
						name: 'Schedule Trigger',
						type: 'n8n-nodes-base.scheduleTrigger',
						typeVersion: 1,
						position: [240, 300],
					},
				],
				connections: {},
			},
			owner,
		);

		await authOwnerAgent.post(`/workflows/${workflow.id}/archive`).send().expect(200);

		const unarchiveResponse = await authOwnerAgent
			.post(`/workflows/${workflow.id}/unarchive`)
			.send()
			.expect(200);

		const { data: unarchivedWorkflow } = unarchiveResponse.body;

		const activateResponse = await authOwnerAgent
			.post(`/workflows/${workflow.id}/activate`)
			.send({ versionId: unarchivedWorkflow.versionId })
			.expect(200);

		expect(activateResponse.body.data.active).toBe(true);
		expect(activateResponse.body.data.activeVersionId).toBeDefined();
	});
});

describe('DELETE /workflows/:workflowId', () => {
	test('deletes an archived workflow owned by the user', async () => {
		const workflow = await createWorkflow({ isArchived: true }, owner);

		await authOwnerAgent.delete(`/workflows/${workflow.id}`).send().expect(200);

		const workflowInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowInDb).toBeNull();
		expect(sharedWorkflowsInDb).toHaveLength(0);
	});

	test('should not delete missing workflow', async () => {
		const response = await authOwnerAgent.delete('/workflows/404').send().expect(403);
		expect(response.body.message).toBe(
			'Could not delete the workflow - workflow was not found in your projects',
		);
	});

	test('deletes an archived workflow owned by the user, even if the user is just a member', async () => {
		const workflow = await createWorkflow({ isArchived: true }, member);

		await testServer.authAgentFor(member).delete(`/workflows/${workflow.id}`).send().expect(200);

		const workflowInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowInDb).toBeNull();
		expect(sharedWorkflowsInDb).toHaveLength(0);
	});

	test('does not delete a workflow that is not archived', async () => {
		const workflow = await createWorkflow({}, owner);

		const response = await authOwnerAgent.delete(`/workflows/${workflow.id}`).send().expect(400);
		expect(response.body.message).toBe('Workflow must be archived before it can be deleted.');

		const workflowInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowInDb).not.toBeNull();
		expect(sharedWorkflowsInDb).toHaveLength(1);
	});

	test('does not delete an archived workflow that is not owned by the user', async () => {
		const workflow = await createWorkflow({ isArchived: true }, member);

		await testServer
			.authAgentFor(anotherMember)
			.delete(`/workflows/${workflow.id}`)
			.send()
			.expect(403);

		const workflowsInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowsInDb).not.toBeNull();
		expect(sharedWorkflowsInDb).toHaveLength(1);
	});

	test("allows the owner to delete archived workflows they don't own", async () => {
		const workflow = await createWorkflow({ isArchived: true }, member);

		await authOwnerAgent.delete(`/workflows/${workflow.id}`).send().expect(200);

		const workflowsInDb = await workflowRepository.findById(workflow.id);
		const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
			workflowId: workflow.id,
		});

		expect(workflowsInDb).toBeNull();
		expect(sharedWorkflowsInDb).toHaveLength(0);
	});
});

describe('GET /workflows/:workflowId/executions/last-successful', () => {
	test('should return the last successful execution', async () => {
		const workflow = await createWorkflow({}, owner);

		const { createSuccessfulExecution } = await import('../shared/db/executions');

		// Create multiple executions with different statuses
		await createSuccessfulExecution(workflow);
		const lastExecution = await createSuccessfulExecution(workflow);

		const response = await authOwnerAgent
			.get(`/workflows/${workflow.id}/executions/last-successful`)
			.expect(200);

		expect(response.body.data).toMatchObject({
			id: lastExecution.id,
			workflowId: workflow.id,
		});
	});

	test('should return 200 with null when no successful execution exists', async () => {
		const workflow = await createWorkflow({}, owner);

		const response = await authOwnerAgent
			.get(`/workflows/${workflow.id}/executions/last-successful`)
			.expect(200);

		expect(response.body.data).toBeNull();
	});
});
