import {
	createTeamProject,
	linkUserToProject,
	createWorkflow,
	testDb,
	mockInstance,
} from '@n8n/backend-test-utils';
import type { User } from '@n8n/db';
import { stringify } from 'flatted';
import type { INode, IRunExecutionData, IWorkflowBase, WorkflowExecuteMode } from 'n8n-workflow';
import { createRunExecutionData, NodeApiError, NodeOperationError } from 'n8n-workflow';

import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
import { WaitTracker } from '@/wait-tracker';

import { createExecution } from './shared/db/executions';
import {
	createMember,
	createMemberWithApiKey,
	createOwner,
	createOwnerWithApiKey,
} from './shared/db/users';
import { setupTestServer } from './shared/utils';

mockInstance(WaitTracker);
mockInstance(ConcurrencyControlService, {
	// @ts-expect-error Private property
	isEnabled: false,
});

const testServer = setupTestServer({
	endpointGroups: ['executions', 'workflows', 'publicApi'],
	modules: ['redaction'],
});

let owner: User;
let member: User;
let publicApiOwner: User;
let publicApiMember: User;

beforeEach(async () => {
	await testDb.truncate(['ExecutionEntity', 'WorkflowEntity', 'SharedWorkflow']);
	testServer.license.reset();
	testServer.license.enable('feat:dataRedaction');
	owner = await createOwner();
	member = await createMember();
	publicApiOwner = await createOwnerWithApiKey();
	publicApiMember = await createMemberWithApiKey();
});

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const SENSITIVE_JSON = { secret: 'sensitive-value', apiKey: 'sk-1234' };

const BINARY_DATA = {
	data: 'base64data',
	mimeType: 'text/plain',
	fileName: 'secret.txt',
};

function buildRunExecutionData(opts: {
	policy?: 'none' | 'non-manual' | 'all';
	mode?: WorkflowExecuteMode;
}): IRunExecutionData {
	return createRunExecutionData({
		resultData: {
			runData: {
				'Test Node': [
					{
						startTime: 0,
						executionIndex: 0,
						executionTime: 0,
						executionStatus: 'success',
						source: [],
						data: {
							main: [
								[
									{
										json: { ...SENSITIVE_JSON },
										binary: { file: { ...BINARY_DATA } },
									},
								],
							],
						},
					},
				],
			},
		},
		executionData: opts.policy
			? {
					runtimeData: {
						version: 1 as const,
						establishedAt: Date.now(),
						source: opts.mode ?? 'trigger',
						redaction: { version: 1 as const, policy: opts.policy },
					},
				}
			: undefined,
	});
}

async function createExecutionWithRedaction(opts: {
	workflow: IWorkflowBase;
	mode?: WorkflowExecuteMode;
	policy?: 'none' | 'non-manual' | 'all';
}) {
	const runData = buildRunExecutionData({ policy: opts.policy, mode: opts.mode });
	return await createExecution(
		{ data: stringify(runData), mode: opts.mode ?? 'trigger' },
		opts.workflow,
	);
}

function parseResponseData(responseBody: { data: { data: string } }): IRunExecutionData {
	// The response data field is flatted-stringified
	const { parse } = require('flatted');
	return parse(responseBody.data.data) as IRunExecutionData;
}

function assertRedacted(
	data: IRunExecutionData,
	expectedReason = 'workflow_redaction_policy',
	expectedCanReveal = true,
) {
	const items = data.resultData.runData['Test Node'][0].data!.main[0]!;
	expect(items.length).toBeGreaterThan(0);
	for (const item of items) {
		expect(item.json).toEqual({});
		expect(item.binary).toBeUndefined();
		expect(item.redaction).toEqual({ redacted: true, reason: expectedReason });
	}
	expect(data.redactionInfo).toEqual({
		isRedacted: true,
		reason: expectedReason,
		canReveal: expectedCanReveal,
	});
}

function assertNotRedacted(data: IRunExecutionData) {
	const items = data.resultData.runData['Test Node'][0].data!.main[0]!;
	expect(items.length).toBeGreaterThan(0);
	for (const item of items) {
		expect(item.json).toEqual(expect.objectContaining({ secret: 'sensitive-value' }));
	}
	expect(data.redactionInfo).toBeUndefined();
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('GET /executions/:id — Execution Redaction', () => {
	describe('policy-based redaction (no query param)', () => {
		test('policy "none" with trigger mode — returns unredacted', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});

		test('policy "none" with manual mode — returns unredacted', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'manual',
				policy: 'none',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});

		test('policy "non-manual" with manual mode — returns unredacted', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'manual',
				policy: 'non-manual',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});

		test('policy "non-manual" with trigger mode — returns redacted', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'non-manual',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertRedacted(parseResponseData(response.body));
		});

		test('policy "non-manual" with webhook mode — returns redacted', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'webhook',
				policy: 'non-manual',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertRedacted(parseResponseData(response.body));
		});

		test('policy "all" with manual mode — returns redacted', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'manual',
				policy: 'all',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertRedacted(parseResponseData(response.body));
		});

		test('policy "all" with trigger mode — returns redacted', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertRedacted(parseResponseData(response.body));
		});

		test('no runtimeData and no workflow settings — defaults to "none", returns unredacted', async () => {
			const workflow = await createWorkflow({}, owner);
			// No policy passed → no runtimeData.redaction
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});

		test('no runtimeData but workflow settings has policy "all" — falls back to settings, returns redacted', async () => {
			const workflow = await createWorkflow({ settings: { redactionPolicy: 'all' } }, owner);
			// No runtimeData.redaction → falls back to workflowData.settings.redactionPolicy
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertRedacted(parseResponseData(response.body));
		});
	});

	describe('explicit redactExecutionData=true query param', () => {
		test('always redacts even when policy is "none"', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.query({ redactExecutionData: 'true' })
				.expect(200);

			assertRedacted(parseResponseData(response.body), 'user_requested');
		});
	});

	describe('explicit redactExecutionData=false query param (reveal)', () => {
		test('owner can reveal unredacted data', async () => {
			const workflow = await createWorkflow({}, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.query({ redactExecutionData: 'false' })
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});

		test('member can reveal data on a workflow in their own personal project', async () => {
			const workflow = await createWorkflow({}, member);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.authAgentFor(member)
				.get(`/executions/${execution.id}`)
				.query({ redactExecutionData: 'false' })
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});

		test('project editor without execution:reveal scope gets 403 with structured error', async () => {
			testServer.license.enable('feat:sharing');

			const teamProject = await createTeamProject();
			await linkUserToProject(member, teamProject, 'project:editor');

			const workflow = await createWorkflow({}, teamProject);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.authAgentFor(member)
				.get(`/executions/${execution.id}`)
				.query({ redactExecutionData: 'false' })
				.expect(403);

			expect(response.status).toBe(403);
			expect(response.body.message).toContain('execution:reveal');
		});

		test('project editor without execution:reveal scope can still reveal when policy allows it (policy=none)', async () => {
			testServer.license.enable('feat:sharing');

			const teamProject = await createTeamProject();
			await linkUserToProject(member, teamProject, 'project:editor');

			const workflow = await createWorkflow({}, teamProject);
			// policy='none' means policyAllowsReveal=true — no permission check needed
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none',
			});

			const response = await testServer
				.authAgentFor(member)
				.get(`/executions/${execution.id}`)
				.query({ redactExecutionData: 'false' })
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});
	});

	describe('policy fallback precedence', () => {
		test('runtimeData policy takes precedence over workflowData.settings', async () => {
			// Workflow settings say "all" but runtimeData says "none"
			const workflow = await createWorkflow({ settings: { redactionPolicy: 'all' } }, owner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none', // runtimeData wins
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertNotRedacted(parseResponseData(response.body));
		});

		test('falls back to workflowData.settings when runtimeData is missing', async () => {
			const workflow = await createWorkflow({ settings: { redactionPolicy: 'all' } }, owner);
			// No policy → no runtimeData.redaction → falls back to settings
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
			});

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			assertRedacted(parseResponseData(response.body));
		});
	});

	describe('error redaction through DB round-trip', () => {
		const mockNode: INode = {
			id: 'node-1',
			name: 'Test Node',
			type: 'n8n-nodes-base.httpRequest',
			typeVersion: 1,
			position: [0, 0],
			parameters: {},
		};

		test('task-level NodeApiError is redacted after DB round-trip', async () => {
			const workflow = await createWorkflow({}, owner);
			const error = new NodeApiError(mockNode, { message: 'Bad Gateway' }, { httpCode: '502' });
			const runData = buildRunExecutionData({ policy: 'all', mode: 'trigger' });
			runData.resultData.runData['Test Node'][0].error = error;

			const execution = await createExecution(
				{ data: stringify(runData), mode: 'trigger' },
				workflow,
			);

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			const data = parseResponseData(response.body);
			const taskData = data.resultData.runData['Test Node'][0];
			expect(taskData.error).toBeUndefined();
			expect(taskData.redactedError).toEqual({ type: 'NodeApiError', httpCode: null });
		});

		test('workflow-level NodeOperationError is redacted after DB round-trip', async () => {
			const workflow = await createWorkflow({}, owner);
			const error = new NodeOperationError(mockNode, 'Workflow operation failed');
			const runData = buildRunExecutionData({ policy: 'all', mode: 'trigger' });
			runData.resultData.error = error;

			const execution = await createExecution(
				{ data: stringify(runData), mode: 'trigger' },
				workflow,
			);

			const response = await testServer
				.authAgentFor(owner)
				.get(`/executions/${execution.id}`)
				.expect(200);

			const data = parseResponseData(response.body);
			expect(data.resultData.error).toBeUndefined();
			expect(data.resultData.redactedError).toEqual({ type: 'NodeOperationError' });
			expect(data.resultData.redactedError).not.toHaveProperty('httpCode');
		});
	});
});

// ---------------------------------------------------------------------------
// GET /workflows/:workflowId/executions/last-successful — Execution Redaction
// ---------------------------------------------------------------------------

describe('GET /workflows/:workflowId/executions/last-successful — Execution Redaction', () => {
	describe('policy-based redaction (no query param)', () => {
		test('policy "none" with trigger mode — returns unredacted', async () => {
			const workflow = await createWorkflow({}, owner);
			await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'none' });

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

			assertNotRedacted(response.body.data.data as IRunExecutionData);
		});

		test('policy "non-manual" with trigger mode — returns redacted', async () => {
			const workflow = await createWorkflow({}, owner);
			await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'non-manual' });

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

			assertRedacted(response.body.data.data as IRunExecutionData);
		});

		test('policy "non-manual" with manual mode — returns unredacted', async () => {
			const workflow = await createWorkflow({}, owner);
			await createExecutionWithRedaction({ workflow, mode: 'manual', policy: 'non-manual' });

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

			assertNotRedacted(response.body.data.data as IRunExecutionData);
		});

		test('policy "all" with trigger mode — returns redacted', async () => {
			const workflow = await createWorkflow({}, owner);
			await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'all' });

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

			assertRedacted(response.body.data.data as IRunExecutionData);
		});
	});

	describe('explicit redactExecutionData=true query param', () => {
		test('always redacts even when policy is "none"', async () => {
			const workflow = await createWorkflow({}, owner);
			await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'none' });

			const response = await testServer
				.authAgentFor(owner)
				.get(`/workflows/${workflow.id}/executions/last-successful`)
				.query({ redactExecutionData: 'true' })
				.expect(200);

			assertRedacted(response.body.data.data as IRunExecutionData, 'user_requested');
		});
	});

	describe('explicit redactExecutionData=false query param (reveal)', () => {
		test('owner can reveal unredacted data when policy is "all"', async () => {
			const workflow = await createWorkflow({}, owner);
			await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'all' });

			const response = await testServer
				.authAgentFor(owner)
				.get(`/workflows/${workflow.id}/executions/last-successful`)
				.query({ redactExecutionData: 'false' })
				.expect(200);

			assertNotRedacted(response.body.data.data as IRunExecutionData);
		});
	});

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

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

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

// ---------------------------------------------------------------------------
// GET /api/v1/executions/:id — Execution Redaction
// ---------------------------------------------------------------------------

describe('GET /api/v1/executions/:id — Execution Redaction', () => {
	describe('policy-based redaction (includeData=true)', () => {
		test('policy "none" with trigger mode — returns unredacted', async () => {
			const workflow = await createWorkflow({}, publicApiMember);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get(`/executions/${execution.id}?includeData=true`)
				.expect(200);

			assertNotRedacted(response.body.data as IRunExecutionData);
		});

		test('policy "non-manual" with trigger mode — redacts for member on own personal project workflow (canReveal=true)', async () => {
			const workflow = await createWorkflow({}, publicApiMember);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'non-manual',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get(`/executions/${execution.id}?includeData=true`)
				.expect(200);

			assertRedacted(response.body.data as IRunExecutionData, 'workflow_redaction_policy', true);
		});

		test('policy "non-manual" with trigger mode — redacts for owner (canReveal=true)', async () => {
			const workflow = await createWorkflow({}, publicApiOwner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'non-manual',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiOwner)
				.get(`/executions/${execution.id}?includeData=true`)
				.expect(200);

			assertRedacted(response.body.data as IRunExecutionData, 'workflow_redaction_policy', true);
		});

		test('policy "non-manual" with manual mode — returns unredacted', async () => {
			const workflow = await createWorkflow({}, publicApiMember);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'manual',
				policy: 'non-manual',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get(`/executions/${execution.id}?includeData=true`)
				.expect(200);

			assertNotRedacted(response.body.data as IRunExecutionData);
		});

		test('policy "all" with trigger mode — redacts for member on own personal project workflow (canReveal=true)', async () => {
			const workflow = await createWorkflow({}, publicApiMember);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get(`/executions/${execution.id}?includeData=true`)
				.expect(200);

			assertRedacted(response.body.data as IRunExecutionData, 'workflow_redaction_policy', true);
		});

		test('includeData=false — returns execution without data field', async () => {
			const workflow = await createWorkflow({}, publicApiMember);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get(`/executions/${execution.id}`)
				.expect(200);

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

	describe('explicit redactExecutionData=true query param', () => {
		test('always redacts even when policy is "none"', async () => {
			const workflow = await createWorkflow({}, publicApiOwner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiOwner)
				.get(`/executions/${execution.id}?includeData=true&redactExecutionData=true`)
				.expect(200);

			assertRedacted(response.body.data as IRunExecutionData, 'user_requested');
		});
	});

	describe('explicit redactExecutionData=false query param (reveal)', () => {
		test('owner can reveal unredacted data', async () => {
			const workflow = await createWorkflow({}, publicApiOwner);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiOwner)
				.get(`/executions/${execution.id}?includeData=true&redactExecutionData=false`)
				.expect(200);

			assertNotRedacted(response.body.data as IRunExecutionData);
		});

		test('member without execution:reveal scope gets 403', async () => {
			testServer.license.enable('feat:sharing');

			const teamProject = await createTeamProject();
			await linkUserToProject(publicApiMember, teamProject, 'project:editor');

			const workflow = await createWorkflow({}, teamProject);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get(`/executions/${execution.id}?includeData=true&redactExecutionData=false`)
				.expect(403);

			expect(response.status).toBe(403);
			expect(response.body.message).toContain('execution:reveal');
		});

		test('member without execution:reveal scope can still reveal when policy allows it (policy=none)', async () => {
			testServer.license.enable('feat:sharing');

			const teamProject = await createTeamProject();
			await linkUserToProject(publicApiMember, teamProject, 'project:editor');

			const workflow = await createWorkflow({}, teamProject);
			const execution = await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get(`/executions/${execution.id}?includeData=true&redactExecutionData=false`)
				.expect(200);

			assertNotRedacted(response.body.data as IRunExecutionData);
		});
	});
});

// ---------------------------------------------------------------------------
// GET /api/v1/executions — Execution Redaction
// ---------------------------------------------------------------------------

describe('GET /api/v1/executions — Execution Redaction', () => {
	test('policy "non-manual" with trigger mode — redacts all executions in list', async () => {
		const workflow = await createWorkflow({}, publicApiMember);
		await createExecutionWithRedaction({
			workflow,
			mode: 'trigger',
			policy: 'non-manual',
		});

		const response = await testServer
			.publicApiAgentFor(publicApiMember)
			.get('/executions?includeData=true')
			.expect(200);

		expect(response.body.data).toHaveLength(1);
		assertRedacted(
			response.body.data[0].data as IRunExecutionData,
			'workflow_redaction_policy',
			true,
		);
	});

	test('policy "none" — list returns unredacted when includeData=true', async () => {
		const workflow = await createWorkflow({}, publicApiMember);
		await createExecutionWithRedaction({
			workflow,
			mode: 'trigger',
			policy: 'none',
		});

		const response = await testServer
			.publicApiAgentFor(publicApiMember)
			.get('/executions?includeData=true')
			.expect(200);

		expect(response.body.data).toHaveLength(1);
		assertNotRedacted(response.body.data[0].data as IRunExecutionData);
	});

	test('includeData=false — returns executions without data field', async () => {
		const workflow = await createWorkflow({}, publicApiMember);
		await createExecutionWithRedaction({
			workflow,
			mode: 'trigger',
			policy: 'all',
		});

		const response = await testServer
			.publicApiAgentFor(publicApiMember)
			.get('/executions')
			.expect(200);

		expect(response.body.data).toHaveLength(1);
		expect(response.body.data[0].data).toBeUndefined();
	});

	test('batch: multiple executions across multiple workflows — each redacted independently', async () => {
		const workflow1 = await createWorkflow({}, publicApiMember);
		const workflow2 = await createWorkflow({}, publicApiMember);

		// workflow1: policy "non-manual", trigger → should be redacted
		await createExecutionWithRedaction({
			workflow: workflow1,
			mode: 'trigger',
			policy: 'non-manual',
		});
		// workflow1: policy "non-manual", manual → should NOT be redacted
		await createExecutionWithRedaction({
			workflow: workflow1,
			mode: 'manual',
			policy: 'non-manual',
		});
		// workflow2: policy "none", trigger → should NOT be redacted
		await createExecutionWithRedaction({ workflow: workflow2, mode: 'trigger', policy: 'none' });
		// workflow2: policy "all", webhook → should be redacted
		await createExecutionWithRedaction({ workflow: workflow2, mode: 'webhook', policy: 'all' });

		const response = await testServer
			.publicApiAgentFor(publicApiMember)
			.get('/executions?includeData=true')
			.expect(200);

		expect(response.body.data).toHaveLength(4);

		const results = response.body.data as Array<{
			workflowId: string;
			mode: string;
			data: IRunExecutionData;
		}>;

		const wf1Trigger = results.find((e) => e.workflowId === workflow1.id && e.mode === 'trigger');
		const wf1Manual = results.find((e) => e.workflowId === workflow1.id && e.mode === 'manual');
		const wf2Trigger = results.find((e) => e.workflowId === workflow2.id && e.mode === 'trigger');
		const wf2Webhook = results.find((e) => e.workflowId === workflow2.id && e.mode === 'webhook');

		assertRedacted(wf1Trigger!.data, 'workflow_redaction_policy', true);
		assertNotRedacted(wf1Manual!.data);
		assertNotRedacted(wf2Trigger!.data);
		assertRedacted(wf2Webhook!.data, 'workflow_redaction_policy', true);
	});

	describe('explicit redactExecutionData=true query param', () => {
		test('always redacts even when policy is "none"', async () => {
			const workflow = await createWorkflow({}, publicApiOwner);
			await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'none',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiOwner)
				.get('/executions?includeData=true&redactExecutionData=true')
				.expect(200);

			expect(response.body.data).toHaveLength(1);
			assertRedacted(response.body.data[0].data as IRunExecutionData, 'user_requested');
		});
	});

	describe('explicit redactExecutionData=false query param (reveal)', () => {
		test('owner can reveal unredacted data in list', async () => {
			const workflow = await createWorkflow({}, publicApiOwner);
			await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiOwner)
				.get('/executions?includeData=true&redactExecutionData=false')
				.expect(200);

			expect(response.body.data).toHaveLength(1);
			assertNotRedacted(response.body.data[0].data as IRunExecutionData);
		});

		test('member without execution:reveal scope gets 403 in list', async () => {
			testServer.license.enable('feat:sharing');

			const teamProject = await createTeamProject();
			await linkUserToProject(publicApiMember, teamProject, 'project:editor');

			const workflow = await createWorkflow({}, teamProject);
			await createExecutionWithRedaction({
				workflow,
				mode: 'trigger',
				policy: 'all',
			});

			const response = await testServer
				.publicApiAgentFor(publicApiMember)
				.get('/executions?includeData=true&redactExecutionData=false')
				.expect(403);

			expect(response.status).toBe(403);
			expect(response.body.message).toContain('execution:reveal');
		});
	});
});
