import { readFileSync } from 'fs';
import type {
	AssignmentCollectionValue,
	INodeParameterResourceLocator,
	IWorkflowBase,
	NodeParameterValueType,
} from 'n8n-workflow';

import { test, expect } from '../../../../../fixtures/base';
import { resolveFromRoot } from '../../../../../utils/path-helper';

function isAssignmentCollectionValue(
	param: NodeParameterValueType,
): param is AssignmentCollectionValue {
	return (
		typeof param === 'object' &&
		param !== null &&
		'assignments' in param &&
		Array.isArray((param as AssignmentCollectionValue).assignments)
	);
}

function isResourceLocator(param: NodeParameterValueType): param is INodeParameterResourceLocator {
	return typeof param === 'object' && param !== null && '__rl' in param && 'value' in param;
}

function assertAssignmentCollectionValue(
	param: NodeParameterValueType,
): asserts param is AssignmentCollectionValue {
	if (!isAssignmentCollectionValue(param)) {
		throw new Error('Expected AssignmentCollectionValue');
	}
}

function assertResourceLocator(
	param: NodeParameterValueType,
): asserts param is INodeParameterResourceLocator {
	if (!isResourceLocator(param)) {
		throw new Error('Expected INodeParameterResourceLocator');
	}
}

function assertIsError(error: unknown): asserts error is Error {
	if (!(error instanceof Error)) {
		throw new Error('Expected Error instance');
	}
}

test.describe('Sub-workflow Version Resolution', {
	annotation: [
		{ type: 'owner', description: 'Catalysts' },
	],
}, () => {
	test('manual execution should use draft version of sub-workflow', async ({ api }) => {
		const { workflowId: childWorkflowId, createdWorkflow: childWorkflow } =
			await api.workflows.importWorkflowFromFile('subworkflow-version-child.json');

		await api.workflows.activate(childWorkflowId, childWorkflow.versionId!);

		const assignmentsParam = childWorkflow.nodes[1].parameters.assignments;
		assertAssignmentCollectionValue(assignmentsParam);
		assignmentsParam.assignments[0].value = 'draft-version';

		await api.request.patch(`/rest/workflows/${childWorkflowId}`, {
			data: {
				versionId: childWorkflow.versionId,
				name: childWorkflow.name,
				nodes: childWorkflow.nodes,
				connections: childWorkflow.connections,
			},
		});

		const { workflowId: parentWorkflowId, createdWorkflow: parentWorkflow } =
			await api.workflows.createWorkflowFromDefinition({
				name: 'Parent Workflow - Manual',
				nodes: [
					{
						parameters: {},
						id: 'manual-trigger',
						name: 'When clicking Test workflow',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						parameters: {
							source: 'database',
							workflowId: {
								__rl: true,
								value: childWorkflowId,
								mode: 'id',
							},
						},
						id: 'execute-workflow',
						name: 'Execute Workflow',
						type: 'n8n-nodes-base.executeWorkflow',
						typeVersion: 1.1,
						position: [200, 0],
					},
				],
				connections: {
					'When clicking Test workflow': {
						main: [
							[
								{
									node: 'Execute Workflow',
									type: 'main',
									index: 0,
								},
							],
						],
					},
				},
			});

		const runResponse = await api.request.post(`/rest/workflows/${parentWorkflowId}/run`, {
			data: {
				workflowData: parentWorkflow,
				triggerToStartFrom: { name: 'When clicking Test workflow' },
			},
		});

		expect(runResponse.ok()).toBe(true);

		const execution = await api.workflows.waitForExecution(parentWorkflowId, 10000, 'manual');
		expect(execution.status).toBe('success');

		const executionDetails = await api.workflows.getExecution(execution.id);
		expect(executionDetails.data).toContain('draft-version');
	});

	test('production execution should use published version of sub-workflow', async ({ api }) => {
		const childFilePath = resolveFromRoot('workflows', 'subworkflow-version-child.json');
		const childDefinition = JSON.parse(readFileSync(childFilePath, 'utf8')) as IWorkflowBase;
		const childAssignmentsParam = childDefinition.nodes[1].parameters.assignments;
		assertAssignmentCollectionValue(childAssignmentsParam);
		childAssignmentsParam.assignments[0].value = 'published-version';

		const { workflowId: childWorkflowId, createdWorkflow: childWorkflow } =
			await api.workflows.createWorkflowFromDefinition(childDefinition);
		await api.workflows.activate(childWorkflowId, childWorkflow.versionId!);

		assertAssignmentCollectionValue(childAssignmentsParam);
		childAssignmentsParam.assignments[0].value = 'draft-version';
		await api.request.patch(`/rest/workflows/${childWorkflowId}`, {
			data: {
				versionId: childWorkflow.versionId,
				name: childDefinition.name,
				nodes: childDefinition.nodes,
				connections: childDefinition.connections,
			},
		});

		const parentFilePath = resolveFromRoot('workflows', 'subworkflow-version-parent.json');
		const parentDefinition = JSON.parse(readFileSync(parentFilePath, 'utf8')) as IWorkflowBase;
		const workflowIdParam = parentDefinition.nodes[1].parameters.workflowId;
		assertResourceLocator(workflowIdParam);
		workflowIdParam.value = childWorkflowId;

		const {
			webhookPath,
			workflowId: parentWorkflowId,
			createdWorkflow: parentWorkflow,
		} = await api.workflows.importWorkflowFromDefinition(parentDefinition);

		await api.workflows.activate(parentWorkflowId, parentWorkflow.versionId!);

		const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`, {
			method: 'POST',
			data: { test: 'data' },
		});

		expect(webhookResponse).toBeDefined();
		expect(webhookResponse.ok()).toBe(true);

		const responseData = await webhookResponse.json();
		expect(responseData['wf-version']).toBe('published-version');
	});

	test('should prevent publishing parent workflow when sub-workflow is not published', async ({
		api,
	}) => {
		const childWorkflowId = (
			await api.workflows.importWorkflowFromFile('subworkflow-version-child.json')
		).workflowId;

		const { workflowId: parentWorkflowId, createdWorkflow: parentWorkflow } =
			await api.workflows.createWorkflowFromDefinition({
				name: 'Parent Workflow',
				nodes: [
					{
						parameters: {},
						id: 'manual-trigger',
						name: 'Manual Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						parameters: {
							source: 'database',
							workflowId: {
								__rl: true,
								value: childWorkflowId,
								mode: 'id',
							},
						},
						id: 'execute-workflow',
						name: 'Execute Workflow',
						type: 'n8n-nodes-base.executeWorkflow',
						typeVersion: 1.1,
						position: [200, 0],
					},
				],
				connections: {
					'Manual Trigger': {
						main: [
							[
								{
									node: 'Execute Workflow',
									type: 'main',
									index: 0,
								},
							],
						],
					},
				},
			});

		try {
			await api.workflows.activate(parentWorkflowId, parentWorkflow.versionId!);
			expect(true).toBe(false);
		} catch (error: unknown) {
			assertIsError(error);
			expect(error.message).toContain('Failed to activate workflow');
			expect(error.message).toContain('Workflow cannot be activated');
		}
	});

	test('should allow self-referencing workflows', async ({ api }) => {
		const { workflowId, createdWorkflow: workflow } =
			await api.workflows.createWorkflowFromDefinition({
				name: 'Self-Referencing Workflow',
				nodes: [
					{
						parameters: {
							path: 'self-ref-webhook',
							responseMode: 'lastNode',
							options: {},
						},
						id: 'webhook-trigger',
						name: 'Webhook',
						type: 'n8n-nodes-base.webhook',
						typeVersion: 2.1,
						position: [0, 0],
						webhookId: 'self-ref-webhook-id',
					},
					{
						parameters: {
							source: 'database',
							workflowId: {
								__rl: true,
								value: 'SELF',
								mode: 'id',
							},
						},
						id: 'execute-workflow',
						name: 'Execute Workflow',
						type: 'n8n-nodes-base.executeWorkflow',
						typeVersion: 1.1,
						position: [200, 0],
					},
				],
				connections: {
					Webhook: {
						main: [
							[
								{
									node: 'Execute Workflow',
									type: 'main',
									index: 0,
								},
							],
						],
					},
				},
			});

		workflow.nodes[1].parameters = {
			...workflow.nodes[1].parameters,
			workflowId: {
				__rl: true,
				value: workflowId,
				mode: 'id',
			},
		};

		const patchResponse = await api.request.patch(`/rest/workflows/${workflowId}`, {
			data: {
				versionId: workflow.versionId,
				name: workflow.name,
				nodes: workflow.nodes,
				connections: workflow.connections,
			},
		});

		const updatedWorkflowData = await patchResponse.json();

		await api.workflows.activate(workflowId, updatedWorkflowData.data.versionId);
	});
});
