import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';

import type { SortRule, WhereClause } from '../../v2/helpers/interfaces';
import * as utils from '../../v2/helpers/utils';
import {
	prepareQueryAndReplacements,
	wrapData,
	addWhereClauses,
	addSortRules,
	replaceEmptyStringsByNulls,
	escapeSqlIdentifier,
	splitQueryToStatements,
} from '../../v2/helpers/utils';

const mySqlMockNode: INode = {
	id: '1',
	name: 'MySQL node',
	typeVersion: 2,
	type: 'n8n-nodes-base.mySql',
	position: [60, 760],
	parameters: {
		operation: 'select',
	},
};

describe('Test MySql V2, prepareQueryAndReplacements', () => {
	it('should transform query and values', () => {
		const preparedQuery = prepareQueryAndReplacements(
			'SELECT * FROM $1:name WHERE id = $2 AND name = $4 AND $3:name = 28',
			2.5,
			['table', 15, 'age', 'Name'],
		);
		expect(preparedQuery).toBeDefined();
		expect(preparedQuery.query).toEqual(
			'SELECT * FROM `table` WHERE id = ? AND name = ? AND `age` = 28',
		);
		expect(preparedQuery.values.length).toEqual(2);
		expect(preparedQuery.values[0]).toEqual(15);
		expect(preparedQuery.values[1]).toEqual('Name');
	});

	it('should not replace dollar amounts inside quoted strings', () => {
		const preparedQuery = prepareQueryAndReplacements(
			"INSERT INTO test_table(content) VALUES('This is for testing $60')",
			2.5,
			[],
		);
		expect(preparedQuery).toBeDefined();
		expect(preparedQuery.query).toEqual(
			"INSERT INTO test_table(content) VALUES('This is for testing $60')",
		);
		expect(preparedQuery.values.length).toEqual(0);
	});

	it('should handle mixed parameters and dollar amounts in quotes', () => {
		const preparedQuery = prepareQueryAndReplacements(
			"INSERT INTO $1:name(content, price) VALUES('Product costs $60', $2)",
			2.5,
			['products', 59.99],
		);
		expect(preparedQuery).toBeDefined();
		expect(preparedQuery.query).toEqual(
			"INSERT INTO `products`(content, price) VALUES('Product costs $60', ?)",
		);
		expect(preparedQuery.values.length).toEqual(1);
		expect(preparedQuery.values[0]).toEqual(59.99);
	});

	it('should handle parameters in double quotes', () => {
		const preparedQuery = prepareQueryAndReplacements(
			'INSERT INTO test_table(content) VALUES("Price is $100 and $200")',
			2.5,
			[],
		);
		expect(preparedQuery).toBeDefined();
		expect(preparedQuery.query).toEqual(
			'INSERT INTO test_table(content) VALUES("Price is $100 and $200")',
		);
		expect(preparedQuery.values.length).toEqual(0);
	});

	it('should process parameters in correct order despite reverse processing', () => {
		const preparedQuery = prepareQueryAndReplacements(
			'SELECT * FROM table WHERE col1 = $1 AND col2 = $2 AND col3 = $3 AND col4 = $4 AND col5 = $5',
			2.5,
			['value1', 'value2', 'value3', 'value4', 'value5'],
		);
		expect(preparedQuery).toBeDefined();
		expect(preparedQuery.query).toEqual(
			'SELECT * FROM table WHERE col1 = ? AND col2 = ? AND col3 = ? AND col4 = ? AND col5 = ?',
		);
		expect(preparedQuery.values.length).toEqual(5);
		expect(preparedQuery.values[0]).toEqual('value1');
		expect(preparedQuery.values[1]).toEqual('value2');
		expect(preparedQuery.values[2]).toEqual('value3');
		expect(preparedQuery.values[3]).toEqual('value4');
		expect(preparedQuery.values[4]).toEqual('value5');
	});

	it('should handle escaped single quotes correctly', () => {
		const preparedQuery = prepareQueryAndReplacements(
			"INSERT INTO test_table(content) VALUES('Don''t replace $1 here')",
			2.5,
			['should_not_appear', 123],
		);
		expect(preparedQuery).toBeDefined();
		expect(preparedQuery.query).toEqual(
			"INSERT INTO test_table(content) VALUES('Don''t replace $1 here')",
		);
	});

	it('should handle escaped double quotes correctly', () => {
		const preparedQuery = prepareQueryAndReplacements(
			"INSERT INTO test_table(content) VALUES('Don\"'t replace $1 here')",
			2.5,
			['should_not_appear', 123],
		);
		expect(preparedQuery).toBeDefined();
		expect(preparedQuery.query).toEqual(
			"INSERT INTO test_table(content) VALUES('Don\"'t replace $1 here')",
		);
	});

	it('should use legacy processing for versions < 2.5', () => {
		const legacySpy = jest.spyOn(utils, 'prepareQueryLegacy');

		prepareQueryAndReplacements('SELECT * FROM $1:name WHERE id = $2', 2.4, ['users', 123]);

		expect(legacySpy).toHaveBeenCalledWith('SELECT * FROM $1:name WHERE id = $2', ['users', 123]);

		legacySpy.mockRestore();
	});

	it('should use new processing for versions >= 2.5', () => {
		const legacySpy = jest.spyOn(utils, 'prepareQueryLegacy');

		prepareQueryAndReplacements('SELECT * FROM $1:name WHERE id = $2', 2.5, ['users', 123]);

		expect(legacySpy).not.toHaveBeenCalled();
	});

	it('should throw error when parameter is referenced but no replacement value provided', () => {
		expect(() => {
			prepareQueryAndReplacements(
				'SELECT * FROM users WHERE id = $4',
				2.5,
				['value1', 'value2'], // Only 2 values but query references $4
			);
		}).toThrow('Parameter $4 referenced in query but no replacement value provided at index 4');
	});

	it('should throw error when multiple parameters are missing replacement values', () => {
		expect(() => {
			prepareQueryAndReplacements(
				'SELECT * FROM users WHERE id = $3 AND name = $5',
				2.5,
				['value1'], // Only 1 value but query references $3 and $5
			);
		}).toThrow('Parameter $3 referenced in query but no replacement value provided at index 3');
	});

	it('should not throw error when all referenced parameters have replacement values', () => {
		expect(() => {
			prepareQueryAndReplacements(
				'SELECT * FROM users WHERE id = $1 AND name = $2',
				2.5,
				['123', 'John'], // Correct number of values
			);
		}).not.toThrow();
	});
});

describe('Test MySql V2, wrapData', () => {
	it('should wrap object in json', () => {
		const data = {
			id: 1,
			name: 'Name',
		};
		const wrappedData = wrapData(data);
		expect(wrappedData).toBeDefined();
		expect(wrappedData).toEqual([{ json: data }]);
	});
	it('should wrap each object in array in json', () => {
		const data = [
			{
				id: 1,
				name: 'Name',
			},
			{
				id: 2,
				name: 'Name 2',
			},
		];
		const wrappedData = wrapData(data);
		expect(wrappedData).toBeDefined();
		expect(wrappedData).toEqual([{ json: data[0] }, { json: data[1] }]);
	});
	it('json key from source should be inside json', () => {
		const data = {
			json: {
				id: 1,
				name: 'Name',
			},
		};
		const wrappedData = wrapData(data);
		expect(wrappedData).toBeDefined();
		expect(wrappedData).toEqual([{ json: data }]);
		expect(Object.keys(wrappedData[0].json)).toContain('json');
	});
});

describe('Test MySql V2, addWhereClauses', () => {
	it('add where clauses to query', () => {
		const whereClauses: WhereClause[] = [
			{ column: 'species', condition: 'equal', value: 'dog' },
			{ column: 'name', condition: 'equal', value: 'Hunter' },
		];
		const [query, values] = addWhereClauses(
			mySqlMockNode,
			0,
			'SELECT * FROM `pet`',
			whereClauses,
			[],
		);
		expect(query).toEqual('SELECT * FROM `pet` WHERE `species` = ? AND `name` = ?');
		expect(values.length).toEqual(2);
		expect(values[0]).toEqual('dog');
		expect(values[1]).toEqual('Hunter');
	});
	it('add where clauses to query combined by OR', () => {
		const whereClauses: WhereClause[] = [
			{ column: 'species', condition: 'equal', value: 'dog' },
			{ column: 'name', condition: 'equal', value: 'Hunter' },
		];
		const [query, values] = addWhereClauses(
			mySqlMockNode,
			0,
			'SELECT * FROM `pet`',
			whereClauses,
			[],
			'OR',
		);
		expect(query).toEqual('SELECT * FROM `pet` WHERE `species` = ? OR `name` = ?');
		expect(values.length).toEqual(2);
		expect(values[0]).toEqual('dog');
		expect(values[1]).toEqual('Hunter');
	});
});

describe('Test MySql V2, addSortRules', () => {
	it('should add ORDER by', () => {
		const sortRules: SortRule[] = [
			{ column: 'name', direction: 'ASC' },
			{ column: 'age', direction: 'DESC' },
		];
		const [query, values] = addSortRules('SELECT * FROM `pet`', sortRules, []);

		expect(query).toEqual('SELECT * FROM `pet` ORDER BY `name` ASC, `age` DESC');
		expect(values.length).toEqual(0);
	});
});

describe('Test MySql V2, replaceEmptyStringsByNulls', () => {
	it('should replace empty strings', () => {
		const data = [
			{ json: { id: 1, name: '' } },
			{ json: { id: '', name: '' } },
			{ json: { id: null, data: '' } },
		];
		const replacedData = replaceEmptyStringsByNulls(data, true);
		expect(replacedData).toBeDefined();
		expect(replacedData).toEqual([
			{ json: { id: 1, name: null } },
			{ json: { id: null, name: null } },
			{ json: { id: null, data: null } },
		]);
	});
	it('should not replace empty strings', () => {
		const data = [{ json: { id: 1, name: '' } }];
		const replacedData = replaceEmptyStringsByNulls(data);
		expect(replacedData).toBeDefined();
		expect(replacedData).toEqual([{ json: { id: 1, name: '' } }]);
	});
});

describe('Test MySql V2, escapeSqlIdentifier', () => {
	it('should escape fully qualified identifier', () => {
		const input = 'db_name.tbl_name.col_name';
		const escapedIdentifier = escapeSqlIdentifier(input);
		expect(escapedIdentifier).toEqual('`db_name`.`tbl_name`.`col_name`');
	});

	it('should escape table name only', () => {
		const input = 'tbl_name';
		const escapedIdentifier = escapeSqlIdentifier(input);
		expect(escapedIdentifier).toEqual('`tbl_name`');
	});

	it('should escape fully qualified identifier with backticks', () => {
		const input = '`db_name`.`tbl_name`.`col_name`';
		const escapedIdentifier = escapeSqlIdentifier(input);
		expect(escapedIdentifier).toEqual('`db_name`.`tbl_name`.`col_name`');
	});

	it('should escape identifier with dots', () => {
		const input = '`db_name`.`some.dotted.tbl_name`';
		const escapedIdentifier = escapeSqlIdentifier(input);
		expect(escapedIdentifier).toEqual('`db_name`.`some.dotted.tbl_name`');
	});
});

describe('Test MySql V2, splitQueryToStatements', () => {
	it('should split query into statements', () => {
		const query =
			"insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:26:20', 'some random; data with a semicolon', 1); insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:27:55', 'random data without semicolon\n', 2);";

		const statements = splitQueryToStatements(query);

		expect(statements).toBeDefined();
		expect(statements).toEqual([
			"insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:26:20', 'some random; data with a semicolon', 1)",
			"insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:27:55', 'random data without semicolon', 2)",
		]);
	});
	it('should not split by ; inside string literal', () => {
		const query =
			"SELECT custom_ship_time FROM models WHERE models.custom_ship_time LIKE CONCAT('%', ';', '%') LIMIT 10";

		const statements = splitQueryToStatements(query);

		expect(statements).toBeDefined();
		expect(statements).toEqual([
			"SELECT custom_ship_time FROM models WHERE models.custom_ship_time LIKE CONCAT('%', ';', '%') LIMIT 10",
		]);
	});

	describe('where clause handling', () => {
		const validOperations = [
			'equal',
			'=',
			'!=',
			'LIKE',
			'>',
			'<',
			'>=',
			'<=',
			'IS NULL',
			'IS NOT NULL',
		];
		const invalidOperations = ['=1 or 1--', '=>', ''];

		test.each(validOperations)('isWhereClause returns true for "%s" operation', (operation) => {
			expect(
				utils.isWhereClause({
					column: 'id',
					condition: operation,
					value: '1',
				}),
			).toBe(true);
		});

		test.each(invalidOperations)('isWhereClause returns false for "%s" operation', (operation) => {
			expect(
				utils.isWhereClause({
					column: 'name',
					condition: operation,
					value: 'ok',
				}),
			).toBe(false);
		});

		test('isWhereClause returns false for when column is missing', () => {
			expect(
				utils.isWhereClause({
					condition: 'equal',
					value: 'ok',
				}),
			).toBe(false);
		});

		test('isWhereClause returns false for when condition is missing', () => {
			expect(
				utils.isWhereClause({
					column: 'id',
					value: 'ok',
				}),
			).toBe(false);
		});

		test.each(invalidOperations)(
			'getWhereClauses throws an exception for "%s" operation',
			(operation) => {
				const getNodeParameterMock = jest.fn().mockReturnValue({
					values: [
						{
							column: 'test',
							condition: '=',
							value: '3',
						},
						{
							column: 'id',
							condition: operation,
							value: '1',
						},
					],
				});
				const ctx = mock<IExecuteFunctions>({ getNodeParameter: getNodeParameterMock });
				expect(() => utils.getWhereClauses(ctx, 0)).toThrow();
			},
		);

		test.each(validOperations)(
			'getWhereClauses returns valid clauses for "%s" operation',
			(operation) => {
				const clauses = [
					{
						column: 'name',
						condition: 'LIKE',
						value: 'Wohn Jick',
					},
					{
						column: 'id',
						condition: operation,
						value: '1',
					},
					{
						column: 'condition',
						condition: 'equal',
						value: 'angry',
					},
				];
				const getNodeParameterMock = jest.fn().mockReturnValue({
					values: clauses,
				});
				const ctx = mock<IExecuteFunctions>({ getNodeParameter: getNodeParameterMock });
				expect(utils.getWhereClauses(ctx, 0)).toBe(clauses);
			},
		);
	});
});
