import { renderComponent } from '@/__tests__/render';
import ParameterInput from '@/components/ParameterInput.vue';
import type { useNDVStore } from '@/stores/ndv.store';
import type { CompletionResult } from '@codemirror/autocomplete';
import { createTestingPinia } from '@pinia/testing';
import { faker } from '@faker-js/faker';
import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import type { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
import { createEventBus } from '@n8n/utils/event-bus';

let mockNdvState: Partial<ReturnType<typeof useNDVStore>>;
let mockNodeTypesState: Partial<ReturnType<typeof useNodeTypesStore>>;
let mockCompletionResult: Partial<CompletionResult>;

beforeEach(() => {
	mockNdvState = {
		hasInputData: true,
		activeNode: {
			id: faker.string.uuid(),
			name: faker.word.words(3),
			parameters: {},
			position: [faker.number.int(), faker.number.int()],
			type: 'test',
			typeVersion: 1,
		},
		isInputPanelEmpty: false,
		isOutputPanelEmpty: false,
	};
	mockNodeTypesState = {
		allNodeTypes: [],
	};
	mockCompletionResult = {};
	createAppModals();
});

vi.mock('@/stores/ndv.store', () => {
	return {
		useNDVStore: vi.fn(() => mockNdvState),
	};
});

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

vi.mock('@/plugins/codemirror/completions/datatype.completions', () => {
	return {
		datatypeCompletions: vi.fn(() => mockCompletionResult),
	};
});

vi.mock('vue-router', () => {
	const push = vi.fn();
	return {
		useRouter: () => ({
			push,
		}),
		useRoute: () => ({}),
		RouterLink: vi.fn(),
	};
});

describe('ParameterInput.vue', () => {
	beforeEach(() => {
		mockNdvState = {
			hasInputData: true,
			activeNode: {
				id: faker.string.uuid(),
				name: faker.word.words(3),
				parameters: {},
				position: [faker.number.int(), faker.number.int()],
				type: 'test',
				typeVersion: 1,
			},
			isInputPanelEmpty: false,
			isOutputPanelEmpty: false,
		};
		mockNodeTypesState = {
			allNodeTypes: [],
			getNodeType: vi.fn().mockReturnValue(null),
		};
		createAppModals();
	});

	afterEach(() => {
		cleanupAppModals();
	});

	test('should render an options parameter (select)', async () => {
		const { container, baseElement, emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'operation',
				parameter: {
					displayName: 'Operation',
					name: 'operation',
					type: 'options',
					noDataExpression: true,
					displayOptions: { show: { resource: ['sheet'] } },
					options: [
						{
							name: 'Append or Update Row',
							value: 'appendOrUpdate',
							description: 'Append a new row or update an existing one (upsert)',
							action: 'Append or update row in sheet',
						},
						{
							name: 'Append Row',
							value: 'append',
							description: 'Create a new row in a sheet',
							action: 'Append row in sheet',
						},
					],
					default: 'appendOrUpdate',
				},
				modelValue: 'appendOrUpdate',
			},
		});
		const select = container.querySelector('input') as HTMLInputElement;
		const selectTrigger = container.querySelector('.select-trigger') as HTMLElement;
		expect(select).toBeInTheDocument();
		expect(selectTrigger).toBeInTheDocument();
		await waitFor(() => expect(select).toHaveValue('Append or Update Row'));

		await userEvent.click(selectTrigger);

		const options = baseElement.querySelectorAll('.list-option');
		expect(options.length).toEqual(2);
		expect(options[0].querySelector('.option-headline')).toHaveTextContent('Append or Update Row');
		expect(options[0].querySelector('.option-description')).toHaveTextContent(
			'Append a new row or update an existing one (upsert)',
		);
		expect(options[1].querySelector('.option-headline')).toHaveTextContent('Append Row');
		expect(options[1].querySelector('.option-description')).toHaveTextContent(
			'Create a new row in a sheet',
		);

		await userEvent.click(options[1]);

		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'append' })]);
	});

	test('should render an options parameter even if it has invalid fields (like displayName)', async () => {
		// Test case based on the Schedule node
		// type=options parameters shouldn't have a displayName field, but some do
		const { container, baseElement, emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'operation',
				parameter: {
					displayName: 'Trigger at Hour',
					name: 'triggerAtHour',
					type: 'options',
					default: 0,
					options: [
						{
							name: 'Midnight',
							displayName: 'Midnight',
							value: 0,
						},
						{
							name: '1am',
							displayName: '1am',
							value: 1,
						},
					],
					description: 'The hour of the day to trigger',
				},
				modelValue: 0,
			},
		});
		const select = container.querySelector('input') as HTMLInputElement;
		const selectTrigger = container.querySelector('.select-trigger') as HTMLElement;

		await waitFor(() => expect(select).toHaveValue('Midnight'));

		await userEvent.click(selectTrigger);

		const options = baseElement.querySelectorAll('.list-option');
		expect(options.length).toEqual(2);
		expect(options[0].querySelector('.option-headline')).toHaveTextContent('Midnight');
		expect(options[1].querySelector('.option-headline')).toHaveTextContent('1am');

		await userEvent.click(options[1]);

		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 1 })]);
	});

	test('should render a string parameter', async () => {
		const { container, emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'tag',
				parameter: {
					displayName: 'Tag',
					name: 'tag',
					type: 'string',
				},
				modelValue: '',
			},
		});
		const input = container.querySelector('input') as HTMLInputElement;
		expect(input).toBeInTheDocument();

		await userEvent.type(input, 'foo');

		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]);
	});

	test('should correctly handle paste events', async () => {
		const { container, emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'tag',
				parameter: {
					displayName: 'Tag',
					name: 'tag',
					type: 'string',
				},
				modelValue: '',
			},
		});
		const input = container.querySelector('input') as HTMLInputElement;
		expect(input).toBeInTheDocument();
		await userEvent.click(input);

		async function paste(text: string) {
			const expression = new DataTransfer();
			expression.setData('text', text);
			await userEvent.clear(input);
			await userEvent.paste(expression);
		}

		await paste('foo');
		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]);

		await paste('={{ $json.foo }}');
		expect(emitted('update')).toContainEqual([
			expect.objectContaining({ value: '={{ $json.foo }}' }),
		]);

		await paste('=flDvzj%y1nP');
		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: '==flDvzj%y1nP' })]);
	});

	test('should not reset the value of a multi-select with loadOptionsMethod on load', async () => {
		mockNodeTypesState.getNodeParameterOptions = vi.fn(async () => [
			{ name: 'ID', value: 'id' },
			{ name: 'Title', value: 'title' },
			{ name: 'Description', value: 'description' },
		]);

		const { emitted, container } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'columns',
				parameter: {
					displayName: 'Columns',
					name: 'columns',
					type: 'multiOptions',
					typeOptions: { loadOptionsMethod: 'getColumnsMultiOptions' },
				},
				modelValue: ['id', 'title'],
			},
		});

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input).toBeInTheDocument();

		// Nothing should be emitted
		expect(emitted('update')).toBeUndefined();
	});

	test('should show message when can not load options without credentials', async () => {
		mockNodeTypesState.getNodeParameterOptions = vi.fn(async () => {
			throw new Error('Node does not have any credentials set');
		});

		// @ts-expect-error Readonly property
		mockNodeTypesState.getNodeType = vi.fn().mockReturnValue({
			displayName: 'Test',
			credentials: [
				{
					name: 'openAiApi',
					required: true,
				},
			],
		});

		const { emitted, container, getByTestId } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'columns',
				parameter: {
					displayName: 'Columns',
					name: 'columns',
					type: 'options',
					typeOptions: { loadOptionsMethod: 'getColumnsMultiOptions' },
				},
				modelValue: 'id',
			},
		});

		await waitFor(() => expect(getByTestId('parameter-input-field')).toBeInTheDocument());

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input).toBeInTheDocument();

		expect(mockNodeTypesState.getNodeParameterOptions).toHaveBeenCalled();

		expect(input.value.toLowerCase()).not.toContain('error');
		expect(input).toHaveValue('Set up credential to see options');

		expect(emitted('update')).toBeUndefined();
	});

	test('should reset bool on eventBus:removeExpression', async () => {
		const eventBus = createEventBus();
		const { emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'aSwitch',
				parameter: {
					displayName: 'A Switch',
					name: 'aSwitch',
					type: 'boolean',
					default: true,
				},
				modelValue: '={{ }}', // note that this makes a syntax error
				eventBus,
			},
		});

		eventBus.emit('optionSelected', 'removeExpression');
		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: true })]);
	});

	test('should reset bool with undefined evaluation on eventBus:removeExpression', async () => {
		const eventBus = createEventBus();
		const { emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'aSwitch',
				parameter: {
					displayName: 'A Switch',
					name: 'aSwitch',
					type: 'boolean',
					default: true,
				},
				modelValue: undefined,
				eventBus,
			},
		});

		eventBus.emit('optionSelected', 'removeExpression');
		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: true })]);
	});

	test('should reset number on eventBus:removeExpression', async () => {
		const eventBus = createEventBus();
		const { emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'aNum',
				parameter: {
					displayName: 'A Num',
					name: 'aNum',
					type: 'number',
					default: 6,
				},
				modelValue: '={{ }}', // note that this makes a syntax error
				eventBus,
			},
		});

		eventBus.emit('optionSelected', 'removeExpression');
		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 6 })]);
	});

	test('should reset string on eventBus:removeExpression', async () => {
		const eventBus = createEventBus();
		const { emitted } = renderComponent(ParameterInput, {
			pinia: createTestingPinia(),
			props: {
				path: 'aStr',
				parameter: {
					displayName: 'A Str',
					name: 'aStr',
					type: 'string',
					default: 'some default',
				},
				modelValue: '={{ }}', // note that this makes a syntax error
				eventBus,
			},
		});

		eventBus.emit('optionSelected', 'removeExpression');
		expect(emitted('update')).toContainEqual([expect.objectContaining({ value: '{{ }}' })]);
	});
});
