import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import type { IConnections, INode } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
import type { WorkflowDataCreate } from '@n8n/rest-api-client/api/workflows';

const {
	mockWorkflowsStore,
	mockWorkflowDocumentStore,
	mockNodeTypesStore,
	mockCanvasOperations,
	mockTelemetry,
	mockHistoryStore,
} = vi.hoisted(() => ({
	mockWorkflowsStore: {
		workflowId: 'parent-workflow-id',
		createNewWorkflow: vi.fn(),
		publishWorkflow: vi.fn(),
	},
	mockWorkflowDocumentStore: {
		allNodes: [] as INodeUi[],
		connectionsBySourceNode: {} as IConnections,
		homeProject: { id: 'home-project' },
		parentFolder: null as { id: string } | null,
		getNodeById: vi.fn(),
		getNodeByName: vi.fn(),
		getChildNodes: vi.fn().mockReturnValue([]),
		getExpressionHandler: vi.fn().mockReturnValue({}),
		getGroupForNode: vi.fn(),
		getGroupById: vi.fn(),
		addNodesToGroup: vi.fn(),
	},
	mockNodeTypesStore: {
		getNodeType: vi.fn().mockReturnValue({
			inputs: ['main'],
			outputs: ['main'],
		}),
	},
	mockCanvasOperations: {
		addNodes: vi.fn().mockResolvedValue([{ id: 'execute-node-id' }]),
		replaceNodeConnections: vi.fn(),
		deleteNodes: vi.fn(),
		replaceNodeParameters: vi.fn(),
	},
	mockTelemetry: {
		track: vi.fn(),
	},
	mockHistoryStore: {
		startRecordingUndo: vi.fn(),
		stopRecordingUndo: vi.fn(),
		pushCommandToUndo: vi.fn(),
	},
}));

vi.mock('@/app/stores/workflows.store', () => ({
	useWorkflowsStore: vi.fn().mockReturnValue(mockWorkflowsStore),
}));

vi.mock('@/app/stores/workflowDocument.store', () => ({
	injectWorkflowDocumentStore: vi.fn().mockReturnValue({ value: mockWorkflowDocumentStore }),
}));

vi.mock('@/app/stores/nodeTypes.store', () => ({
	useNodeTypesStore: vi.fn().mockReturnValue(mockNodeTypesStore),
}));

vi.mock('@/app/composables/useCanvasOperations', () => ({
	useCanvasOperations: vi.fn().mockReturnValue(mockCanvasOperations),
}));

vi.mock('@/app/stores/ui.store', () => ({
	useUIStore: vi.fn().mockReturnValue({
		resetLastInteractedWith: vi.fn(),
		markStateDirty: vi.fn(),
		openModalWithData: vi.fn(),
	}),
}));

vi.mock('@/app/stores/history.store', () => ({
	useHistoryStore: vi.fn().mockReturnValue(mockHistoryStore),
}));

vi.mock('@/app/composables/useToast', () => ({
	useToast: vi.fn().mockReturnValue({
		showMessage: vi.fn(),
		showError: vi.fn(),
	}),
}));

vi.mock('@/app/composables/useTelemetry', () => ({
	useTelemetry: vi.fn().mockReturnValue(mockTelemetry),
}));

vi.mock('@n8n/i18n', () => ({
	useI18n: vi.fn().mockReturnValue({
		baseText: vi.fn().mockImplementation((key: string) => key),
	}),
}));

vi.mock('vue-router', () => ({
	useRouter: vi.fn().mockReturnValue({
		resolve: vi.fn().mockReturnValue({ href: '/workflow/new-id' }),
	}),
}));

import { useWorkflowExtraction } from '@/app/composables/useWorkflowExtraction';
import { UpdateNodeGroupCommand } from '@/app/models/history';

function makeNode(name: string, position: [number, number] = [0, 0]): INodeUi {
	return {
		id: `id-${name}`,
		name,
		type: 'n8n-nodes-base.set',
		typeVersion: 1,
		position,
		parameters: {},
		disabled: false,
		issues: undefined,
		typeUnknown: false,
	} as INodeUi;
}

describe('useWorkflowExtraction', () => {
	beforeEach(() => {
		const pinia = createTestingPinia({ stubActions: false });
		setActivePinia(pinia);

		mockWorkflowsStore.createNewWorkflow.mockReset();
		mockWorkflowsStore.publishWorkflow.mockReset();
		mockCanvasOperations.addNodes.mockClear();
		mockCanvasOperations.replaceNodeConnections.mockClear();
		mockCanvasOperations.deleteNodes.mockClear();
		mockCanvasOperations.replaceNodeParameters.mockClear();
		mockTelemetry.track.mockClear();
		mockWorkflowDocumentStore.getChildNodes.mockReturnValue([]);
		mockWorkflowDocumentStore.getGroupForNode.mockReset();
		mockWorkflowDocumentStore.getGroupById.mockReset();
		mockWorkflowDocumentStore.addNodesToGroup.mockReset();
		mockHistoryStore.startRecordingUndo.mockClear();
		mockHistoryStore.stopRecordingUndo.mockClear();
		mockHistoryStore.pushCommandToUndo.mockClear();
		mockWorkflowDocumentStore.allNodes = [];
		mockWorkflowDocumentStore.connectionsBySourceNode = {};
	});

	describe('extractWorkflow', () => {
		it('does not track start telemetry for an empty selection', () => {
			const { extractWorkflow } = useWorkflowExtraction();

			extractWorkflow([]);

			expect(mockTelemetry.track).not.toHaveBeenCalled();
		});
	});

	describe('extractNodesIntoSubworkflow', () => {
		it('strips connections referencing nodes outside the extracted selection', async () => {
			const nodeA = makeNode('A', [0, 0]);
			const nodeB = makeNode('B', [200, 0]);
			const nodeC = makeNode('C', [400, 0]); // outside the extracted selection

			mockWorkflowDocumentStore.allNodes = [nodeA, nodeB, nodeC];
			mockWorkflowDocumentStore.connectionsBySourceNode = {
				A: {
					[NodeConnectionTypes.Main]: [
						[
							{ node: 'B', type: NodeConnectionTypes.Main, index: 0 },
							{ node: 'C', type: NodeConnectionTypes.Main, index: 0 },
						],
					],
				},
			};

			mockWorkflowsStore.createNewWorkflow.mockResolvedValue({
				id: 'new-id',
				versionId: 'v1',
			});
			mockWorkflowsStore.publishWorkflow.mockResolvedValue(undefined);

			const { extractNodesIntoSubworkflow } = useWorkflowExtraction();

			await extractNodesIntoSubworkflow(
				{ start: 'A', end: undefined },
				[nodeA, nodeB],
				'Sub-workflow',
			);

			expect(mockWorkflowsStore.createNewWorkflow).toHaveBeenCalledTimes(1);
			const created = mockWorkflowsStore.createNewWorkflow.mock.calls[0][0] as WorkflowDataCreate;

			const aMainBuckets = created.connections?.A?.[NodeConnectionTypes.Main];
			expect(aMainBuckets).toBeDefined();
			const targetsFromA = (aMainBuckets?.[0] ?? []).map((c) => c?.node);
			expect(targetsFromA).toContain('B');
			expect(targetsFromA).not.toContain('C');

			const allTargets = Object.values(created.connections ?? {}).flatMap((byType) =>
				Object.values(byType).flatMap((buckets) =>
					(buckets ?? []).flatMap((bucket) => (bucket ?? []).map((c) => c?.node)),
				),
			);
			expect(allTargets).not.toContain('C');

			const createdNodeNames = (created.nodes ?? []).map((n: INode) => n.name);
			expect(createdNodeNames).not.toContain('C');
		});

		it('records the Execute Workflow node joining the group as undoable history', async () => {
			const nodeA = makeNode('A', [0, 0]);
			const nodeB = makeNode('B', [200, 0]);
			const group = { id: 'g1', name: 'Group 1', nodeIds: [nodeA.id, nodeB.id] };

			mockWorkflowDocumentStore.allNodes = [nodeA, nodeB];
			mockWorkflowDocumentStore.getGroupForNode.mockImplementation((nodeId: string) =>
				group.nodeIds.includes(nodeId) ? group : undefined,
			);
			mockWorkflowDocumentStore.getGroupById
				.mockReturnValueOnce({ ...group })
				.mockReturnValueOnce({ ...group, nodeIds: [...group.nodeIds, 'execute-node-id'] });

			mockWorkflowsStore.createNewWorkflow.mockResolvedValue({ id: 'new-id', versionId: 'v1' });
			mockWorkflowsStore.publishWorkflow.mockResolvedValue(undefined);

			const { extractNodesIntoSubworkflow } = useWorkflowExtraction();

			await extractNodesIntoSubworkflow({ start: 'A', end: undefined }, [nodeA, nodeB], 'Sub');

			expect(mockWorkflowDocumentStore.addNodesToGroup).toHaveBeenCalledWith(group.id, [
				'execute-node-id',
			]);

			const groupCommand = mockHistoryStore.pushCommandToUndo.mock.calls
				.map(([command]) => command)
				.find((command) => command instanceof UpdateNodeGroupCommand) as
				| UpdateNodeGroupCommand
				| undefined;
			expect(groupCommand).toBeInstanceOf(UpdateNodeGroupCommand);
			expect(groupCommand?.before.nodeIds).toEqual([nodeA.id, nodeB.id]);
			expect(groupCommand?.after.nodeIds).toEqual([nodeA.id, nodeB.id, 'execute-node-id']);
		});
	});
});
