import { computed } from 'vue';
import { createComponentRenderer } from '@/__tests__/render';
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
import ParameterInput from './ParameterInput.vue';
import type { useNDVStore } from '@/features/ndv/shared/ndv.store';
import type { CompletionResult } from '@codemirror/autocomplete';
import { createTestingPinia } from '@pinia/testing';
import { faker } from '@faker-js/faker';
import { fireEvent, waitFor, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import type { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { mockedStore } from '@/__tests__/utils';
import { createEventBus } from '@n8n/utils/event-bus';
import {
	createTestExpressionLocalResolveContext,
	createMockEnterpriseSettings,
	createTestNodeProperties,
} from '@/__tests__/mocks';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { type INodeParameterResourceLocator } from 'n8n-workflow';
import type { IWorkflowDb, WorkflowListResource } from '@/Interface';
import { mock } from 'vitest-mock-extended';
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
import { nextTick, reactive } from 'vue';

function getNdvStateMock(): Partial<ReturnType<typeof useNDVStore>> {
	return {
		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,
		ndvInputDataWithPinnedData: [],
		getHoveringItem: undefined,
		expressionOutputItemIndex: 0,
		isTableHoverOnboarded: false,
		setHighlightDraggables: vi.fn(),
		setNDVPanelDataIsEmpty: vi.fn(),
		setNDVBranchIndex: vi.fn(),
	};
}

function getNodeTypesStateMock(): Partial<ReturnType<typeof useNodeTypesStore>> {
	return {
		allNodeTypes: [],
		getAllNodeTypes: vi.fn().mockReturnValue({
			nodeTypes: {},
			init: async () => {},
			getByNameAndVersion: () => undefined,
		}),
	};
}

let mockNdvState = getNdvStateMock();
let mockNodeTypesState = getNodeTypesStateMock();
let mockCompletionResult: Partial<CompletionResult> = {};

beforeEach(() => {
	mockNdvState = getNdvStateMock();
	mockNodeTypesState = getNodeTypesStateMock();
	mockCompletionResult = {};
	mockBuilderState.trackWorkflowBuilderJourney.mockClear();
	mockIsPlaceholderValue.mockReset();
	mockExtractPlaceholderLabels.mockReset().mockReturnValue([]);
	mockBuilderState.isAIBuilderEnabled = true;
});

vi.mock('@/features/ndv/shared/ndv.store', () => {
	return {
		useNDVStore: vi.fn(() => mockNdvState),
		injectNDVStore: vi.fn(() => ({ value: mockNdvState })),
		injectNDVStoreIfProvided: vi.fn(() => ({ value: mockNdvState })),
	};
});

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

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

vi.mock('vue-router', () => {
	const push = vi.fn();
	return {
		useRouter: () => ({
			push,
			resolve: vi.fn().mockReturnValue({
				href: '/projects/1/folders/1',
			}),
		}),
		useRoute: () => ({}),
		RouterLink: vi.fn(),
	};
});

vi.mock('@/app/composables/useWorkflowId', async () => {
	const { computed } = await import('vue');
	const { useWorkflowsStore } = await import('@/app/stores/workflows.store');
	return {
		useWorkflowId: () => computed(() => useWorkflowsStore().workflowId),
		useRouteWorkflowId: () => computed(() => useWorkflowsStore().workflowId),
	};
});

const mockBuilderState = {
	trackWorkflowBuilderJourney: vi.fn(),
	isAIBuilderEnabled: true,
};

vi.mock('@/features/ai/assistant/builder.store', () => {
	return {
		useBuilderStore: vi.fn(() => mockBuilderState),
	};
});

const mockIsPlaceholderValue = vi.fn();
const mockExtractPlaceholderLabels = vi.fn().mockReturnValue([]);

vi.mock('@/features/ai/assistant/composables/useBuilderTodos', () => {
	return {
		isPlaceholderValue: (value: unknown) => mockIsPlaceholderValue(value),
		extractPlaceholderLabels: (value: unknown) => mockExtractPlaceholderLabels(value),
	};
});

const renderComponent = createComponentRenderer(ParameterInput, {
	pinia: createTestingPinia(),
	global: {
		provide: {
			[WorkflowIdKey as unknown as string]: computed(() => 'test-workflow-id'),
		},
	},
});

const settingsStore = mockedStore(useSettingsStore);
const workflowsListStore = mockedStore(useWorkflowsListStore);

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,
			ndvInputDataWithPinnedData: [],
			getHoveringItem: undefined,
			expressionOutputItemIndex: 0,
			isTableHoverOnboarded: false,
			setHighlightDraggables: vi.fn(),
			setNDVPanelDataIsEmpty: vi.fn(),
			setNDVBranchIndex: vi.fn(),
		};
		mockNodeTypesState = {
			allNodeTypes: [],
			getNodeType: vi.fn().mockReturnValue(null),
			getAllNodeTypes: vi.fn().mockReturnValue({
				nodeTypes: {},
				init: async () => {},
				getByNameAndVersion: () => undefined,
			}),
		};
		settingsStore.settings.enterprise = createMockEnterpriseSettings();
	});

	afterEach(() => {
		vi.clearAllMocks();
	});

	test('should render an options parameter (select)', async () => {
		const { container, baseElement, emitted } = renderComponent({
			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({
			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]);

		await waitFor(() =>
			expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 1 })]),
		);
	});

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

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

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

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

		test('should handle pasting into a string parameter', async () => {
			const { container, emitted } = renderComponent({
				props: {
					path: 'tag',
					parameter: createTestNodeProperties({
						displayName: 'Tag',
						name: 'tag',
						type: 'string',
					}),
					modelValue: '',
				},
			});
			const input = container.querySelector('input') as HTMLInputElement;
			expect(input).toBeInTheDocument();
			await userEvent.click(input);

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

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

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

		test('should handle pasting an expression into a number parameter', async () => {
			const { container, emitted } = renderComponent({
				props: {
					path: 'percentage',
					parameter: createTestNodeProperties({
						displayName: 'Percentage',
						name: 'percentage',
						type: 'number',
					}),
					modelValue: 1,
				},
			});
			const input = container.querySelector('input') as HTMLInputElement;
			expect(input).toBeInTheDocument();
			await userEvent.click(input);

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

	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({
			props: {
				path: 'columns',
				parameter: createTestNodeProperties({
					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',
			properties: [],
			credentials: [
				{
					name: 'openAiApi',
					required: true,
				},
			],
		});

		const { emitted, container, getByTestId } = renderComponent({
			props: {
				path: 'columns',
				parameter: createTestNodeProperties({
					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 render workflow selector without issues when selected workflow is not archived', async () => {
		const workflowId = faker.string.uuid();
		const modelValue = {
			mode: 'id',
			value: workflowId,
		};

		workflowsListStore.fetchWorkflowsPage.mockResolvedValue([
			mock<WorkflowListResource>({
				id: workflowId,
				name: 'Test',
				active: false,
				isArchived: false,
				createdAt: new Date().toISOString(),
				updatedAt: new Date().toISOString(),
				// nodes: [],
				// connections: {},
				versionId: faker.string.uuid(),
			}),
		]);

		const { emitted, container, getByTestId, queryByTestId } = renderComponent({
			props: {
				path: 'columns',
				parameter: {
					displayName: 'Workflow',
					name: 'workflowId',
					type: 'workflowSelector',
					default: '',
				},
				modelValue,
			},
		});

		await waitFor(() => expect(getByTestId('resource-locator-workflowId')).toBeInTheDocument());

		expect(container.querySelector('.has-issues')).not.toBeInTheDocument();

		const inputs = container.querySelectorAll('input');
		const mode = inputs[0];
		expect(mode).toBeInTheDocument();
		expect(mode).toHaveValue('By ID');

		const value = inputs[1];
		expect(value).toBeInTheDocument();
		expect(value).toHaveValue(workflowId);

		expect(queryByTestId('parameter-issues')).not.toBeInTheDocument();

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

	test('should show error when workflow selector has archived workflow selected', async () => {
		const workflowId = faker.string.uuid();
		const modelValue: INodeParameterResourceLocator = {
			__rl: true,
			mode: 'id',
			value: workflowId,
		};

		const workflowBase = {
			id: workflowId,
			name: 'Test',
			active: false,
			isArchived: true,
			createdAt: new Date().toISOString(),
			updatedAt: new Date().toISOString(),
			versionId: faker.string.uuid(),
		};
		workflowsListStore.allWorkflows = [mock<IWorkflowDb>(workflowBase)];
		workflowsListStore.fetchWorkflowsPage.mockResolvedValue([
			mock<WorkflowListResource>(workflowBase),
		]);

		const { emitted, container, getByTestId } = renderComponent({
			props: {
				path: 'columns',
				parameter: {
					displayName: 'Workflow',
					name: 'workflowId',
					type: 'workflowSelector',
					default: '',
				},
				modelValue,
			},
		});

		await waitFor(() => expect(getByTestId('resource-locator-workflowId')).toBeInTheDocument());

		expect(container.querySelector('.has-issues')).toBeInTheDocument();

		const inputs = container.querySelectorAll('input');
		const mode = inputs[0];
		expect(mode).toBeInTheDocument();
		expect(mode).toHaveValue('By ID');

		const value = inputs[1];
		expect(value).toBeInTheDocument();
		expect(value).toHaveValue(workflowId);

		expect(getByTestId('parameter-issues')).toBeInTheDocument();

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

	test('should reset bool on eventBus:removeExpression', async () => {
		const eventBus = createEventBus();
		const { emitted } = renderComponent({
			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({
			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({
			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 () => {
		mockNdvState = {
			...getNdvStateMock(),
			activeNode: {
				id: faker.string.uuid(),
				name: 'Test Node',
				parameters: {
					aStr: 'test',
				},
				position: [0, 0],
				type: 'n8n-nodes-base.httpRequest',
				typeVersion: 1,
			},
		};

		const eventBus = createEventBus();
		const { emitted } = renderComponent({
			props: {
				path: 'parameters.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: '{{ }}' })]);
	});

	test('should maintain focus after changing to expression', async () => {
		const { rerender, getByRole, getByTestId } = renderComponent({
			props: {
				path: 'name',
				parameter: createTestNodeProperties({
					displayName: 'Name',
					name: 'name',
					type: 'string',
				}),
				modelValue: 'test',
			},
		});
		const input = getByRole('textbox');
		expect(input).toBeInTheDocument();
		await userEvent.click(input);
		await rerender({ modelValue: '={{ $json.foo }}' });

		const expressionEditor = getByTestId('inline-expression-editor-input');
		expect(expressionEditor).toBeInTheDocument();
		const expressionEditorInput = within(expressionEditor).getByRole('textbox');
		await waitFor(() => expect(expressionEditorInput).toHaveFocus());
	});

	describe('when not in focus', () => {
		test('should not focus after changing to expression ', async () => {
			const { rerender, getByRole, getByTestId } = renderComponent({
				props: {
					path: 'name',
					parameter: createTestNodeProperties({
						displayName: 'Name',
						name: 'name',
						type: 'string',
					}),
					modelValue: 'test',
				},
			});
			const input = getByRole('textbox');
			expect(input).toBeInTheDocument();
			await rerender({ modelValue: '={{ $json.foo }}' });

			const expressionEditor = getByTestId('inline-expression-editor-input');
			expect(expressionEditor).toBeInTheDocument();
			const expressionEditorInput = within(expressionEditor).getByRole('textbox');
			await waitFor(() => expect(expressionEditorInput).not.toHaveFocus());
		});
	});

	describe('debounced input', () => {
		test('should debounce text input and emit update event only once', async () => {
			const { container, emitted } = renderComponent({
				props: {
					path: 'textField',
					parameter: createTestNodeProperties({
						displayName: 'Text Field',
						name: 'textField',
						type: 'string',
					}),
					modelValue: '',
				},
			});

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

			await userEvent.click(input);

			await userEvent.type(input, 'h');
			await userEvent.type(input, 'e');
			await userEvent.type(input, 'l');
			await userEvent.type(input, 'l');
			await userEvent.type(input, 'o');
			// by now the update event should not have been emitted because of debouncing
			expect(emitted('update')).not.toContainEqual([expect.objectContaining({ value: 'hello' })]);

			// Now the update event should have been emitted
			await waitFor(() => {
				const updateEvents = emitted('update');
				expect(updateEvents).toBeDefined();
				expect(updateEvents.length).toBeLessThan(5);
				expect(updateEvents).toContainEqual([expect.objectContaining({ value: 'hello' })]);
			});
		});
	});

	describe('data mapper', () => {
		const ctx = createTestExpressionLocalResolveContext({
			nodeName: 'n0',
			inputNode: { name: 'n1', runIndex: 0, branchIndex: 0 },
		});

		it('should render mapper when the current value is empty', async () => {
			const rendered = renderComponent({
				global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
				props: {
					path: 'name',
					parameter: createTestNodeProperties(),
					modelValue: '',
				},
			});

			await nextTick();
			await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);

			expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
		});

		it('should render mapper when editor type is specified in the parameter', async () => {
			const rendered = renderComponent({
				global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
				props: {
					path: 'name',
					parameter: createTestNodeProperties({ typeOptions: { editor: 'sqlEditor' } }),
					modelValue: 'SELECT 1;',
				},
			});

			await nextTick();
			await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);

			expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
		});

		it('should render mapper when the current value is an expression', async () => {
			const rendered = renderComponent({
				global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
				props: {
					path: 'name',
					parameter: createTestNodeProperties(),
					modelValue: '={{$today}}',
				},
			});

			await nextTick();
			await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);

			expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
		});

		it('should not render mapper if given node property is a node setting', async () => {
			const rendered = renderComponent({
				global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
				props: {
					path: 'name',
					parameter: createTestNodeProperties({ isNodeSetting: true }),
					modelValue: '',
				},
			});

			await nextTick();
			await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);

			expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
		});

		it('should not render mapper if given node property has datetime type', async () => {
			const rendered = renderComponent({
				global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
				props: {
					path: 'name',
					parameter: createTestNodeProperties({ type: 'dateTime' }),
					modelValue: '',
				},
			});

			await nextTick();
			await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);

			expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
		});
	});

	describe('placeholder tracking', () => {
		it('tracks field_focus_placeholder_in_ndv when focusing placeholder value', async () => {
			mockIsPlaceholderValue.mockReturnValue(true);
			mockNdvState = {
				...getNdvStateMock(),
				activeNode: {
					id: faker.string.uuid(),
					name: 'Test Node',
					parameters: {},
					position: [0, 0],
					type: 'n8n-nodes-base.httpRequest',
					typeVersion: 1,
				},
			};

			const rendered = renderComponent({
				props: {
					path: 'url',
					parameter: createTestNodeProperties({ name: 'url', type: 'string' }),
					modelValue: '<__PLACEHOLDER_VALUE__API URL__>',
				},
			});

			await nextTick();
			const input = rendered.container.querySelector('input');
			if (input) {
				await fireEvent.focus(input);
			}

			// Wait for debounced tracking call
			await waitFor(() => {
				expect(mockBuilderState.trackWorkflowBuilderJourney).toHaveBeenCalledWith(
					'field_focus_placeholder_in_ndv',
					{ node_type: 'n8n-nodes-base.httpRequest' },
				);
			});
		});

		it('does not track when value is not a placeholder', async () => {
			mockIsPlaceholderValue.mockReturnValue(false);
			mockNdvState = {
				...getNdvStateMock(),
				activeNode: {
					id: faker.string.uuid(),
					name: 'Test Node',
					parameters: {},
					position: [0, 0],
					type: 'n8n-nodes-base.httpRequest',
					typeVersion: 1,
				},
			};

			const rendered = renderComponent({
				props: {
					path: 'url',
					parameter: createTestNodeProperties({ name: 'url', type: 'string' }),
					modelValue: 'https://api.example.com',
				},
			});

			await nextTick();
			const input = rendered.container.querySelector('input');
			if (input) {
				await fireEvent.focus(input);
			}

			expect(mockBuilderState.trackWorkflowBuilderJourney).not.toHaveBeenCalled();
		});

		it('does not track when AI builder is disabled', async () => {
			mockIsPlaceholderValue.mockReturnValue(true);
			mockBuilderState.isAIBuilderEnabled = false;
			mockNdvState = {
				...getNdvStateMock(),
				activeNode: {
					id: faker.string.uuid(),
					name: 'Test Node',
					parameters: {},
					position: [0, 0],
					type: 'n8n-nodes-base.httpRequest',
					typeVersion: 1,
				},
			};

			const rendered = renderComponent({
				props: {
					path: 'url',
					parameter: createTestNodeProperties({ name: 'url', type: 'string' }),
					modelValue: '<__PLACEHOLDER_VALUE__API URL__>',
				},
			});

			await nextTick();
			const input = rendered.container.querySelector('input');
			if (input) {
				await fireEvent.focus(input);
			}

			expect(mockBuilderState.trackWorkflowBuilderJourney).not.toHaveBeenCalled();
		});
	});

	describe('credential change resets options value', () => {
		test('should reset options value when credentials change', async () => {
			mockNodeTypesState.getNodeParameterOptions = vi.fn(async () => [
				{ name: 'GPT-4', value: 'gpt-4' },
				{ name: 'GPT-3.5', value: 'gpt-3.5-turbo' },
			]);

			const activeNode = reactive({
				id: faker.string.uuid(),
				name: 'Test Node',
				parameters: { model: 'gpt-3.5-turbo' },
				position: [0, 0] as [number, number],
				type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
				typeVersion: 1,
				credentials: {
					openAiApi: { id: '1', name: 'OpenAI Account 1' },
				},
			});

			mockNdvState = {
				...getNdvStateMock(),
				activeNode,
			};

			const { emitted } = renderComponent({
				props: {
					path: 'model',
					parameter: createTestNodeProperties({
						displayName: 'Model',
						name: 'model',
						type: 'options',
						default: 'gpt-4',
						typeOptions: { loadOptionsMethod: 'getModels' },
					}),
					modelValue: 'gpt-3.5-turbo',
				},
			});

			await waitFor(() => {
				expect(mockNodeTypesState.getNodeParameterOptions).toHaveBeenCalled();
			});

			// Change credentials — the previously selected model should be reset to the default
			activeNode.credentials = {
				openAiApi: { id: '2', name: 'OpenAI Account 2' },
			};
			await nextTick();

			await waitFor(() => {
				expect(emitted('update')).toContainEqual([
					expect.objectContaining({ name: 'model', value: 'gpt-4' }),
				]);
			});
		});

		test('should not reset non-options parameter when credentials change', async () => {
			const activeNode = reactive({
				id: faker.string.uuid(),
				name: 'Test Node',
				parameters: { temperature: 0.9 },
				position: [0, 0] as [number, number],
				type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
				typeVersion: 1,
				credentials: {
					openAiApi: { id: '1', name: 'OpenAI Account 1' },
				},
			});

			mockNodeTypesState.getNodeParameterOptions = vi.fn(async () => []);

			mockNdvState = {
				...getNdvStateMock(),
				activeNode,
			};

			const { emitted } = renderComponent({
				props: {
					path: 'temperature',
					parameter: createTestNodeProperties({
						displayName: 'Temperature',
						name: 'temperature',
						type: 'number',
						default: 0.7,
						typeOptions: { loadOptionsMethod: 'getTemperature' },
					}),
					modelValue: 0.9,
				},
			});

			await waitFor(() => {
				expect(mockNodeTypesState.getNodeParameterOptions).toHaveBeenCalled();
			});

			activeNode.credentials = {
				openAiApi: { id: '2', name: 'OpenAI Account 2' },
			};
			await nextTick();

			await waitFor(() => {
				expect(mockNodeTypesState.getNodeParameterOptions).toHaveBeenCalledTimes(2);
			});

			const updates = emitted('update') ?? [];
			expect(updates).not.toContainEqual([expect.objectContaining({ name: 'temperature' })]);
		});

		test('should not reset options value on initial load', async () => {
			mockNodeTypesState.getNodeParameterOptions = vi.fn(async () => [
				{ name: 'GPT-4', value: 'gpt-4' },
			]);

			mockNdvState = {
				...getNdvStateMock(),
				activeNode: reactive({
					id: faker.string.uuid(),
					name: 'Test Node',
					parameters: { model: 'gpt-4' },
					position: [0, 0] as [number, number],
					type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
					typeVersion: 1,
					credentials: {
						openAiApi: { id: '1', name: 'OpenAI Account 1' },
					},
				}),
			};

			const { emitted } = renderComponent({
				props: {
					path: 'model',
					parameter: createTestNodeProperties({
						displayName: 'Model',
						name: 'model',
						type: 'options',
						default: 'gpt-4',
						typeOptions: { loadOptionsMethod: 'getModels' },
					}),
					modelValue: 'gpt-4',
				},
			});

			await waitFor(() => {
				expect(mockNodeTypesState.getNodeParameterOptions).toHaveBeenCalled();
			});

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

		test('should not reset options value on first credential assignment', async () => {
			mockNodeTypesState.getNodeParameterOptions = vi.fn(async () => [
				{ name: 'GPT-4', value: 'gpt-4' },
				{ name: 'GPT-3.5', value: 'gpt-3.5-turbo' },
			]);

			const activeNode = reactive({
				id: faker.string.uuid(),
				name: 'Test Node',
				parameters: { model: 'gpt-3.5-turbo' },
				position: [0, 0] as [number, number],
				type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
				typeVersion: 1,
				credentials: {} as Record<string, { id: string; name: string }>,
			});

			mockNdvState = {
				...getNdvStateMock(),
				activeNode,
			};

			const { emitted } = renderComponent({
				props: {
					path: 'model',
					parameter: createTestNodeProperties({
						displayName: 'Model',
						name: 'model',
						type: 'options',
						default: 'gpt-4',
						typeOptions: { loadOptionsMethod: 'getModels' },
					}),
					modelValue: 'gpt-3.5-turbo',
				},
			});

			await waitFor(() => {
				expect(mockNodeTypesState.getNodeParameterOptions).toHaveBeenCalled();
			});

			// Assign credentials for the first time (from empty to having credentials)
			activeNode.credentials = {
				openAiApi: { id: '1', name: 'OpenAI Account 1' },
			};
			await nextTick();

			await waitFor(() => {
				expect(mockNodeTypesState.getNodeParameterOptions).toHaveBeenCalledTimes(2);
			});

			// Should NOT reset the value since this is first credential assignment, not a change
			const updates = emitted('update') ?? [];
			expect(updates).not.toContainEqual([expect.objectContaining({ name: 'model' })]);
		});
	});

	describe('multi-line string handling', () => {
		test('should render multi-line string value as textarea', async () => {
			const multiLineValue = 'line1\nline2\nline3';
			const { container } = renderComponent({
				props: {
					path: 'description',
					parameter: createTestNodeProperties({
						displayName: 'Description',
						name: 'description',
						type: 'string',
					}),
					modelValue: multiLineValue,
					expressionEvaluated: undefined,
				},
			});

			await nextTick();
			const textarea = container.querySelector('textarea') as HTMLTextAreaElement;
			await waitFor(() => {
				expect(textarea.value).toBe(multiLineValue);
			});
		});

		test('should preserve newlines in multi-row textarea', async () => {
			const multiLineValue = 'line1\nline2\nline3';
			const { container } = renderComponent({
				props: {
					path: 'description',
					parameter: createTestNodeProperties({
						displayName: 'Description',
						name: 'description',
						type: 'string',
						typeOptions: { rows: 5 },
					}),
					modelValue: multiLineValue,
					expressionEvaluated: undefined,
				},
			});

			await nextTick();
			const textarea = container.querySelector('textarea') as HTMLTextAreaElement;
			expect(textarea.value).toBe(multiLineValue);
		});

		test('should keep pipe characters as-is in single-line input', async () => {
			const { container } = renderComponent({
				props: {
					path: 'pattern',
					parameter: createTestNodeProperties({
						displayName: 'Pattern',
						name: 'pattern',
						type: 'string',
					}),
					modelValue: 'foo|bar',
					expressionEvaluated: undefined,
				},
			});

			await nextTick();
			const input = container.querySelector('input') as HTMLInputElement;
			await waitFor(() => {
				expect(input.value).toBe('foo|bar');
			});
		});

		test('should render string with consecutive newlines as textarea', async () => {
			const { container } = renderComponent({
				props: {
					path: 'test',
					parameter: createTestNodeProperties({
						displayName: 'Test',
						name: 'test',
						type: 'string',
					}),
					modelValue: 'a\n\n\nb',
					expressionEvaluated: undefined,
				},
			});

			await nextTick();
			const textarea = container.querySelector('textarea') as HTMLTextAreaElement;
			await waitFor(() => {
				expect(textarea.value).toBe('a\n\n\nb');
			});
		});
	});

	describe('JSON Password Field Validation', () => {
		const jsonPasswordParameter = createTestNodeProperties({
			displayName: 'Service Account Key',
			name: 'serviceAccountKey',
			type: 'json',
			default: '',
			required: true,
			typeOptions: {
				password: true,
			},
			noDataExpression: true,
		});

		it('renders as a password input', async () => {
			const { container } = renderComponent({
				props: {
					path: 'serviceAccountKey',
					parameter: jsonPasswordParameter,
					modelValue: '',
				},
			});

			await nextTick();
			const input = container.querySelector('input[type="password"]');
			expect(input).toBeInTheDocument();
		});

		it('shows validation error for invalid JSON input', async () => {
			const { container, queryByTestId } = renderComponent({
				props: {
					path: 'serviceAccountKey',
					parameter: jsonPasswordParameter,
					modelValue: '',
				},
			});

			await nextTick();
			const input = container.querySelector('input') as HTMLInputElement;
			await fireEvent.update(input, '{invalid');

			expect(queryByTestId('parameter-issues')).toBeInTheDocument();
		});

		it('does not show validation error for valid JSON input', async () => {
			const { container, queryByTestId } = renderComponent({
				props: {
					path: 'serviceAccountKey',
					parameter: jsonPasswordParameter,
					modelValue: '',
				},
			});

			await nextTick();
			const input = container.querySelector('input') as HTMLInputElement;
			await fireEvent.update(input, '{"type": "service_account"}');

			expect(queryByTestId('parameter-issues')).not.toBeInTheDocument();
		});

		it('does not show validation error for empty input', async () => {
			const { container, queryByTestId } = renderComponent({
				props: {
					path: 'serviceAccountKey',
					parameter: jsonPasswordParameter,
					modelValue: '',
				},
			});

			await nextTick();
			const input = container.querySelector('input') as HTMLInputElement;
			await fireEvent.update(input, '');

			expect(queryByTestId('parameter-issues')).not.toBeInTheDocument();
		});

		it('clears validation error when invalid JSON is corrected', async () => {
			const { container, queryByTestId } = renderComponent({
				props: {
					path: 'serviceAccountKey',
					parameter: jsonPasswordParameter,
					modelValue: '',
				},
			});

			await nextTick();
			const input = container.querySelector('input') as HTMLInputElement;

			await fireEvent.update(input, '{invalid');
			expect(queryByTestId('parameter-issues')).toBeInTheDocument();

			await fireEvent.update(input, '{"valid": true}');
			expect(queryByTestId('parameter-issues')).not.toBeInTheDocument();
		});
	});
});
