import {jest, describe, it, expect, beforeEach} from '@jest/globals';
import type { IExecuteFunctions, INodeExecutionData, INodeType } from 'n8n-workflow';
import { PassThrough } from 'stream';

const mockAttach = jest.fn();
const mockStart = jest.fn();
const mockContainer = {
    attach: mockAttach,
    start: mockStart,
};
const mockCreateContainer = jest.fn().mockResolvedValue(mockContainer);

jest.mock('dockerode', () => {
  return jest.fn().mockImplementation(() => {
    return {
      createContainer: mockCreateContainer,
    };
  });
});

// A helper to create the mock Docker stream header for multiplexed streams
const createDockerStreamChunk = (channel: 1 | 2, data: string): Buffer => {
	const header = Buffer.alloc(8);
	header.writeUInt8(channel, 0); // 1 for stdout, 2 for stderr
	header.writeUInt32BE(data.length, 4);
	return Buffer.concat([header, Buffer.from(data)]);
};

describe('Pytenable Node with Docker', () => {
	let executeFunctions: IExecuteFunctions;
    let Pytenable: new () => INodeType;

	beforeEach(async () => {
        const module = await import('./Pytenable.node.js');
        Pytenable = module.Pytenable;

		jest.clearAllMocks();
        mockAttach.mockClear();
        mockStart.mockClear();
        mockCreateContainer.mockClear();
		jest.setTimeout(30000);

		executeFunctions = {
			getNode: jest.fn().mockReturnValue({
				getContext: jest.fn().mockReturnValue({
					executionId: 'test-execution',
					node: { name: 'Pytenable', type: 'Pytenable', id: 'test-node-id' },
				}),
			}),
			getNodeParameter: jest.fn(),
			getCredentials: jest.fn().mockResolvedValue({}),
			getInputData: jest.fn(),
			helpers: {
				returnJsonArray: jest.fn(data => data as INodeExecutionData[]),
				constructExecutionMetaData: jest.fn((data, options) =>
					data.map((item: Record<string, unknown>) => ({ json: item, pairedItem: options.pairedItem }))
				),
			},
		} as unknown as IExecuteFunctions;
	});

	it('should execute for all items at once successfully', async () => {
		// Arrange
		(executeFunctions.getNodeParameter as jest.Mock)
			.mockReturnValueOnce('runOnceForAllItems')
			.mockReturnValueOnce('return [{"status": "ok"}]');
		(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: { id: 1 } }]);
        (executeFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation((data) => data.map((item: Record<string, unknown>) => ({json: item})));

		const mockStream = new PassThrough();
        (mockStream.end as jest.Mock) = jest.fn();

        mockAttach.mockResolvedValue(mockStream);
        mockStart.mockImplementation((cb: (err: Error | null) => void) => cb(null));

		// Act
		const executionPromise = new Pytenable().execute.call(executeFunctions);

		// Simulate container output and end the stream manually for the node's listener
		await new Promise(resolve => setImmediate(resolve)); // Wait for listeners to attach
		const output = JSON.stringify([{ status: 'ok' }]);
		mockStream.write(createDockerStreamChunk(1, output));
		mockStream.emit('end');

		const result = await executionPromise;

		// Assert
		expect(result[0]).toEqual([expect.objectContaining({ json: { status: 'ok' } })]);
		expect(mockCreateContainer).toHaveBeenCalledTimes(1);
	});

	it('should execute for each item individually and pair results', async () => {
		// Arrange
		(executeFunctions.getNodeParameter as jest.Mock)
			.mockReturnValueOnce('runOnceForEachItem')
			.mockReturnValueOnce('return [{"id": _item["id"] * 2}]');
		(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: { id: 1 } }, { json: { id: 2 } }]);
        (executeFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation(data => data);
        (executeFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation((data) => data.map((item: Record<string, unknown>) => ({ json: item })));

		const mockStreams = [new PassThrough(), new PassThrough()];
        mockStreams.forEach(s => { (s.end as jest.Mock) = jest.fn(); });

        mockAttach
            .mockResolvedValueOnce(mockStreams[0])
            .mockResolvedValueOnce(mockStreams[1]);
        mockStart.mockImplementation((cb: (err: Error | null) => void) => cb(null));


		// Act
		const executionPromise = new Pytenable().execute.call(executeFunctions);

		// Simulate responses for each item
		await new Promise(resolve => setImmediate(resolve));
		mockStreams[0].write(createDockerStreamChunk(1, JSON.stringify([{ id: 2 }])));
		mockStreams[0].emit('end');

		await new Promise(resolve => setImmediate(resolve));
		mockStreams[1].write(createDockerStreamChunk(1, JSON.stringify([{ id: 4 }])));
		mockStreams[1].emit('end');

		const result = await executionPromise;

		// Assert
		const returnData = result[0];
		expect(returnData.length).toBe(2);
		expect(returnData[0]).toEqual({ json: { id: 2 }, pairedItem: { item: 0 } });
		expect(returnData[1]).toEqual({ json: { id: 4 }, pairedItem: { item: 1 } });
	});

	it('should handle Python script error from stderr', async () => {
		// Arrange
		(executeFunctions.getNodeParameter as jest.Mock)
			.mockReturnValueOnce('runOnceForAllItems')
			.mockReturnValueOnce('raise Exception("Boom!")');
		(executeFunctions.getInputData as jest.Mock).mockReturnValue([{}]);

		const mockStream = new PassThrough();
        (mockStream.end as jest.Mock) = jest.fn();

        mockAttach.mockResolvedValue(mockStream);
        mockStart.mockImplementation((cb: (err: Error | null) => void) => cb(null));

		// Act
		const executionPromise = new Pytenable().execute.call(executeFunctions);

		await new Promise(resolve => setImmediate(resolve));
		const errorOutput = "Traceback... Exception: Boom!";
		mockStream.write(createDockerStreamChunk(2, errorOutput));
		mockStream.emit('end');

		// Assert
		await expect(executionPromise).rejects.toThrow('Error en el contenedor de Python: Traceback... Exception: Boom!');
	});
});
