import { createWorkflow, testDb } from '@n8n/backend-test-utils';
import type { User } from '@n8n/db';
import { ExecutionRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { createExecution } from '@test-integration/db/executions';
import { createOwner } from '@test-integration/db/users';
import { stringify, parse } from 'flatted';
import { DateTime } from 'luxon';
import type { ExecutionStatus } from 'n8n-workflow';
import { createEmptyRunExecutionData, createRunExecutionData } from 'n8n-workflow';

describe('UserRepository', () => {
	let executionRepository: ExecutionRepository;
	let owner: User;

	beforeAll(async () => {
		await testDb.init();
		executionRepository = Container.get(ExecutionRepository);
		owner = await createOwner();
	});

	beforeEach(async () => {
		await testDb.truncate(['ExecutionEntity']);
	});

	afterAll(async () => {
		await testDb.terminate();
	});

	describe('findManyByRangeQuery', () => {
		test('sort by `createdAt` if `startedAt` is null', async () => {
			const now = DateTime.utc();
			const workflow = await createWorkflow({}, owner);
			const execution1 = await createExecution(
				{
					createdAt: now.plus({ minute: 1 }).toJSDate(),
					startedAt: now.plus({ minute: 1 }).toJSDate(),
				},
				workflow,
			);
			const execution2 = await createExecution(
				{
					createdAt: now.plus({ minute: 2 }).toJSDate(),
					startedAt: null,
				},
				workflow,
			);
			const execution3 = await createExecution(
				{
					createdAt: now.plus({ minute: 3 }).toJSDate(),
					startedAt: now.plus({ minute: 3 }).toJSDate(),
				},
				workflow,
			);

			const executions = await executionRepository.findManyByRangeQuery({
				workflowId: workflow.id,
				user: owner,
				kind: 'range',
				range: { limit: 10 },
				order: { startedAt: 'DESC' },
			});

			// Executions are returned in reverse order, and if `startedAt` is not
			// defined `createdAt` is used.
			expect(executions.map((e) => e.id)).toStrictEqual([
				execution3.id,
				execution2.id,
				execution1.id,
			]);
		});
	});

	describe('setRunning', () => {
		test('should set startedAt when execution has no startedAt', async () => {
			const workflow = await createWorkflow();
			const execution = await createExecution({ status: 'new', startedAt: null }, workflow);

			const result = await executionRepository.setRunning(execution.id);

			expect(result).toBeInstanceOf(Date);

			const row = await executionRepository.findOneBy({ id: execution.id });
			expect(row?.status).toBe('running');
			expect(row?.startedAt).toEqual(result);
		});

		test('should preserve original startedAt for resumed executions', async () => {
			const originalStartedAt = new Date('2025-12-02T09:04:47.150Z');
			const workflow = await createWorkflow();
			const execution = await createExecution(
				{ status: 'waiting', startedAt: originalStartedAt },
				workflow,
			);

			const result = await executionRepository.setRunning(execution.id);

			expect(result.getTime()).toBe(originalStartedAt.getTime());

			const row = await executionRepository.findOneBy({ id: execution.id });
			expect(row?.status).toBe('running');
			expect(row?.startedAt?.getTime()).toBe(originalStartedAt.getTime());
		});
	});

	describe('updateExistingExecution with conditions', () => {
		test.each([
			{
				statusInDB: 'waiting' as ExecutionStatus,
				statusUpdate: 'running' as ExecutionStatus,
				conditions: { requireStatus: 'waiting' as ExecutionStatus },
				updateExpected: true,
			},
			{
				statusInDB: 'success' as ExecutionStatus,
				statusUpdate: 'running' as ExecutionStatus,
				conditions: { requireStatus: 'waiting' as ExecutionStatus },
				updateExpected: false,
			},
			{
				statusInDB: 'running' as ExecutionStatus,
				statusUpdate: 'success' as ExecutionStatus,
				conditions: undefined,
				updateExpected: true,
			},
		])(
			'should return $updateExpected with status before: "$statusInDB", status after: "$statusUpdate" and conditions: $conditions',
			async ({ statusInDB, statusUpdate, conditions, updateExpected }) => {
				// ARRANGE

				const workflow = await createWorkflow({}, owner);
				const executionData = createEmptyRunExecutionData();
				const execution = await createExecution(
					{ status: statusInDB, data: stringify(executionData) },
					workflow,
				);

				const updatedExecutionData = createRunExecutionData({
					resultData: { lastNodeExecuted: 'foobar' },
				});

				// ACT
				const result = await executionRepository.updateExistingExecution(
					execution.id,
					{ status: statusUpdate, data: updatedExecutionData },
					conditions,
				);

				// ASSERT
				expect(result).toBe(updateExpected);

				const row = await executionRepository.findOne({
					where: { id: execution.id },
					relations: { executionData: true },
				});
				expect(row?.status).toBe(updateExpected ? statusUpdate : statusInDB);
				expect(parse(row!.executionData.data)).toEqual(
					updateExpected ? updatedExecutionData : executionData,
				);
			},
		);

		test('returns false if no execution was found', async () => {
			const result = await executionRepository.updateExistingExecution('1', { status: 'success' });

			expect(result).toBe(false);

			const rowCount = await executionRepository.count();
			expect(rowCount).toBe(0);
		});

		test('requireNotFinished: should update when finished is false', async () => {
			const workflow = await createWorkflow({}, owner);
			const executionData = createEmptyRunExecutionData();
			const execution = await createExecution(
				{ status: 'running', finished: false, data: stringify(executionData) },
				workflow,
			);

			const result = await executionRepository.updateExistingExecution(
				execution.id,
				{ status: 'running' },
				{ requireNotFinished: true },
			);

			expect(result).toBe(true);
		});

		test('requireNotFinished: should not update when finished is true', async () => {
			const workflow = await createWorkflow({}, owner);
			const executionData = createEmptyRunExecutionData();
			const execution = await createExecution(
				{ status: 'success', finished: true, data: stringify(executionData) },
				workflow,
			);

			const result = await executionRepository.updateExistingExecution(
				execution.id,
				{ status: 'running' },
				{ requireNotFinished: true },
			);

			expect(result).toBe(false);
			const row = await executionRepository.findOne({ where: { id: execution.id } });
			expect(row?.status).toBe('success');
		});

		test('requireNotCanceled: should update when status is not canceled', async () => {
			const workflow = await createWorkflow({}, owner);
			const executionData = createEmptyRunExecutionData();
			const execution = await createExecution(
				{ status: 'running', data: stringify(executionData) },
				workflow,
			);

			const result = await executionRepository.updateExistingExecution(
				execution.id,
				{ status: 'running' },
				{ requireNotCanceled: true },
			);

			expect(result).toBe(true);
		});

		test('requireNotCanceled: should not update when status is canceled', async () => {
			const workflow = await createWorkflow({}, owner);
			const executionData = createEmptyRunExecutionData();
			const execution = await createExecution(
				{ status: 'canceled', data: stringify(executionData) },
				workflow,
			);

			const result = await executionRepository.updateExistingExecution(
				execution.id,
				{ status: 'running' },
				{ requireNotCanceled: true },
			);

			expect(result).toBe(false);
			const row = await executionRepository.findOne({ where: { id: execution.id } });
			expect(row?.status).toBe('canceled');
		});

		test('requireNotFinished + requireNotCanceled: should update running unfinished execution', async () => {
			const workflow = await createWorkflow({}, owner);
			const executionData = createEmptyRunExecutionData();
			const execution = await createExecution(
				{ status: 'running', finished: false, data: stringify(executionData) },
				workflow,
			);

			const updatedData = createRunExecutionData({
				resultData: { lastNodeExecuted: 'Node1' },
			});

			const result = await executionRepository.updateExistingExecution(
				execution.id,
				{ status: 'running', data: updatedData },
				{ requireNotFinished: true, requireNotCanceled: true },
			);

			expect(result).toBe(true);

			const row = await executionRepository.findOne({
				where: { id: execution.id },
				relations: { executionData: true },
			});
			expect(parse(row!.executionData.data)).toEqual(updatedData);
		});

		test('requireNotFinished + requireNotCanceled: should not update canceled execution', async () => {
			const workflow = await createWorkflow({}, owner);
			const executionData = createEmptyRunExecutionData();
			const execution = await createExecution(
				{ status: 'canceled', finished: false, data: stringify(executionData) },
				workflow,
			);

			const result = await executionRepository.updateExistingExecution(
				execution.id,
				{ status: 'running' },
				{ requireNotFinished: true, requireNotCanceled: true },
			);

			expect(result).toBe(false);
			const row = await executionRepository.findOne({ where: { id: execution.id } });
			expect(row?.status).toBe('canceled');
		});
	});
});
