import {
	createTeamProject,
	createWorkflow,
	createWorkflowHistory,
	createWorkflowWithHistory,
	testDb,
} from '@n8n/backend-test-utils';
import {
	WorkflowHistoryRepository,
	WorkflowPublishedVersionRepository,
	WorkflowReviewRequestRepository,
	WorkflowReviewRequestWorkflowRepository,
} from '@n8n/db';
import { Container } from '@n8n/di';
import { RULES, type INode } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';

describe('WorkflowHistoryRepository', () => {
	const testNode1 = {
		id: uuid(),
		name: 'testNode1',
		parameters: {},
		type: 'aNodeType',
		typeVersion: 1,
		position: [0, 0],
	} satisfies INode;

	const alwaysMergeRule = () => true;

	beforeAll(async () => {
		await testDb.init();
	});

	beforeEach(async () => {
		await testDb.truncate([
			'WorkflowReviewRequestWorkflow',
			'WorkflowReviewRequest',
			'WorkflowPublishedVersion',
			'WorkflowPublishHistory',
			'WorkflowHistory',
			'WorkflowEntity',
			'Project',
			'User',
		]);
	});

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

	describe('pruneHistory', () => {
		it('should prune superseded version', async () => {
			const id1 = uuid();
			const id2 = uuid();

			const workflow = await createWorkflowWithHistory({
				versionId: id1,
				nodes: [{ ...testNode1, parameters: { a: 'a' } }],
			});
			await createWorkflowHistory({
				...workflow,
				versionId: uuid(),
				nodes: [{ ...testNode1, parameters: { a: 'ab' } }],
			});
			await createWorkflowHistory({
				...workflow,
				versionId: uuid(),
				nodes: [{ ...testNode1, parameters: { a: 'abc' } }],
			});
			await createWorkflowHistory({
				...workflow,
				versionId: id2,
				nodes: [{ ...testNode1, parameters: { a: 'abcd' } }],
			});

			// ACT
			const repository = Container.get(WorkflowHistoryRepository);

			const tenMinsAgo = new Date();
			tenMinsAgo.setMinutes(tenMinsAgo.getMinutes() - 10);

			const aMinAgo = new Date();
			aMinAgo.setMinutes(aMinAgo.getMinutes() - 1);

			const nextMin = new Date();
			nextMin.setMinutes(nextMin.getMinutes() + 1);

			const inTenMins = new Date();
			inTenMins.setMinutes(inTenMins.getMinutes() + 10);

			{
				// Don't touch workflows younger than range
				const { deleted, seen } = await repository.pruneHistory(workflow.id, tenMinsAgo, aMinAgo, [
					RULES.mergeAdditiveChanges,
				]);
				expect(deleted).toBe(0);
				expect(seen).toBe(0);

				const history = await repository.find();
				expect(history.length).toBe(4);
			}

			{
				// Don't touch workflows older
				const { deleted, seen } = await repository.pruneHistory(workflow.id, nextMin, inTenMins, [
					RULES.mergeAdditiveChanges,
				]);
				expect(deleted).toBe(0);
				expect(seen).toBe(0);

				const history = await repository.find();
				expect(history.length).toBe(4);
			}

			{
				const { deleted, seen } = await repository.pruneHistory(workflow.id, aMinAgo, nextMin, [
					RULES.mergeAdditiveChanges,
				]);
				expect(seen).toBe(4);
				expect(deleted).toBe(3);

				const history = await repository.find();
				expect(history.length).toBe(1);
				expect(history).toEqual([expect.objectContaining({ versionId: id2 })]);
			}
		});
		it('should not prune non-additive version', async () => {
			const id1 = uuid();
			const id2 = uuid();

			const workflow = await createWorkflowWithHistory({
				versionId: id1,
				nodes: [{ ...testNode1, parameters: { a: 'abcde' } }],
			});
			await createWorkflowHistory({
				...workflow,
				versionId: uuid(),
				nodes: [{ ...testNode1, parameters: { a: 'ab' } }],
			});
			await createWorkflowHistory({
				...workflow,
				versionId: uuid(),
				nodes: [{ ...testNode1, parameters: { a: 'abc' } }],
			});
			await createWorkflowHistory({
				...workflow,
				versionId: id2,
				nodes: [{ ...testNode1, parameters: { a: 'abcd' } }],
			});

			// ACT
			const repository = Container.get(WorkflowHistoryRepository);

			const tenMinsAgo = new Date();
			tenMinsAgo.setMinutes(tenMinsAgo.getMinutes() - 10);

			const aMinAgo = new Date();
			aMinAgo.setMinutes(aMinAgo.getMinutes() - 1);

			const nextMin = new Date();
			nextMin.setMinutes(nextMin.getMinutes() + 1);

			const inTenMins = new Date();
			inTenMins.setMinutes(inTenMins.getMinutes() + 10);

			{
				const { deleted, seen } = await repository.pruneHistory(workflow.id, aMinAgo, nextMin, [
					RULES.mergeAdditiveChanges,
				]);
				expect(seen).toBe(4);
				expect(deleted).toBe(2);

				const history = await repository.find();
				expect(history.length).toBe(2);
				expect(history).toEqual([
					expect.objectContaining({ versionId: id1 }),
					expect.objectContaining({ versionId: id2 }),
				]);
			}
		});

		it('should never prune previously active or named versions', async () => {
			// ARRANGE
			const id1 = uuid();
			const id2 = uuid();
			const id3 = uuid();
			const id4 = uuid();
			const id5 = uuid();

			const workflow = await createWorkflowWithHistory({
				versionId: id1,
				nodes: [{ ...testNode1, parameters: { a: 'a' } }],
			});
			await createWorkflowHistory(
				{
					...workflow,
					versionId: id2,
					nodes: [{ ...testNode1, parameters: { a: 'ab' } }],
				},
				undefined,
				undefined,
				{ name: 'aVersionName' },
			);
			await createWorkflowHistory({
				...workflow,
				versionId: id3,
				nodes: [{ ...testNode1, parameters: { a: 'abc' } }],
			});
			await createWorkflowHistory(
				{
					...workflow,
					versionId: id4,
					nodes: [{ ...testNode1, parameters: { a: 'abcd' } }],
				},
				undefined,
				{ event: 'activated' },
			);
			await createWorkflowHistory({
				...workflow,
				versionId: id5,
				nodes: [{ ...testNode1, parameters: { a: 'abcde' } }],
			});

			// ACT
			const repository = Container.get(WorkflowHistoryRepository);

			const aDayAgo = new Date();
			aDayAgo.setDate(aDayAgo.getDate() - 1);

			const nextDay = new Date();
			nextDay.setDate(nextDay.getDate() + 1);

			const { deleted, seen } = await repository.pruneHistory(workflow.id, aDayAgo, nextDay, [
				alwaysMergeRule,
			]);

			// ASSERT
			expect(seen).toBe(5);
			expect(deleted).toBe(2);

			const history = await repository.find();
			expect(history).toEqual([
				expect.objectContaining({ versionId: id2 }),
				expect.objectContaining({ versionId: id4 }),
				expect.objectContaining({ versionId: id5 }),
			]);

			const redo = await repository.pruneHistory(workflow.id, aDayAgo, nextDay, [alwaysMergeRule]);

			// ASSERT
			expect(redo.deleted).toBe(0);
			expect(redo.seen).toBe(3);
		});
	});
	describe('deleteEarlierThanExceptCurrentAndActive', () => {
		// Happy path: proves the setup deletes old, unreferenced versions while
		// keeping the current version.
		it('should delete old versions but keep the current version', async () => {
			const vCurrent = uuid();
			const vOld = uuid();

			const tenDaysAgo = new Date();
			tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);

			const oneDayAgo = new Date();
			oneDayAgo.setDate(oneDayAgo.getDate() - 1);

			const workflow = await createWorkflow({
				versionId: vCurrent,
				nodes: [{ ...testNode1, parameters: { a: 'current' } }],
			});
			// Old, unreferenced version (should be pruned)
			await createWorkflowHistory(
				{ ...workflow, versionId: vOld, nodes: [{ ...testNode1, parameters: { a: 'old' } }] },
				undefined,
				undefined,
				{ createdAt: tenDaysAgo },
			);
			// Current version history, also old — so it is a prune candidate by date
			// and survives only via the current-version (workflow_entity.versionId)
			// exclusion, not because it is too recent to delete.
			await createWorkflowHistory(workflow, undefined, undefined, { createdAt: tenDaysAgo });

			const repository = Container.get(WorkflowHistoryRepository);
			await repository.deleteEarlierThanExceptCurrentAndActive(oneDayAgo);

			const remainingIds = (await repository.find()).map((r) => r.versionId);
			expect(remainingIds).toContain(vCurrent);
			expect(remainingIds).not.toContain(vOld);
		});

		// A workflow_history row referenced by
		// workflow_published_version.publishedVersionId carries an ON DELETE
		// RESTRICT FK, so deleting it aborts the whole prune DELETE with
		// "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed". The prune must
		// exclude published versions, mirroring the sibling pruneHistory().
		it('should preserve versions referenced by a published version', async () => {
			const vCurrent = uuid();
			const vPublished = uuid();
			const vOther = uuid();

			const tenDaysAgo = new Date();
			tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);

			const oneDayAgo = new Date();
			oneDayAgo.setDate(oneDayAgo.getDate() - 1);

			const workflow = await createWorkflow({
				versionId: vCurrent,
				nodes: [{ ...testNode1, parameters: { a: 'current' } }],
			});

			// Old version that is still the published version (RESTRICT FK)
			await createWorkflowHistory(
				{
					...workflow,
					versionId: vPublished,
					nodes: [{ ...testNode1, parameters: { a: 'published' } }],
				},
				undefined,
				undefined,
				{ createdAt: tenDaysAgo },
			);
			// Old version that is NOT referenced anywhere (should be pruned)
			await createWorkflowHistory(
				{ ...workflow, versionId: vOther, nodes: [{ ...testNode1, parameters: { a: 'other' } }] },
				undefined,
				undefined,
				{ createdAt: tenDaysAgo },
			);
			// Current version history (recent)
			await createWorkflowHistory(workflow);

			// The published version has advanced away from current/active
			await Container.get(WorkflowPublishedVersionRepository).setPublishedVersion(
				workflow.id,
				vPublished,
			);

			const repository = Container.get(WorkflowHistoryRepository);

			// Must not throw SQLITE_CONSTRAINT: FOREIGN KEY constraint failed
			await repository.deleteEarlierThanExceptCurrentAndActive(oneDayAgo);

			const remainingIds = (await repository.find()).map((r) => r.versionId);
			expect(remainingIds).toContain(vPublished); // preserved: referenced as published
			expect(remainingIds).toContain(vCurrent); // preserved: current version
			expect(remainingIds).not.toContain(vOther); // pruned: old and unreferenced
		});

		// The open-review exclusion must hold even when named-version preservation
		// is off (unlicensed), because review pins are named versions and would
		// otherwise be pruned mid-review. A closed review no longer protects its pin.
		it('should preserve versions pinned by an open review but not by a closed one', async () => {
			const vCurrent = uuid();
			const vOpenPinned = uuid();
			const vClosedPinned = uuid();

			const tenDaysAgo = new Date();
			tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);

			const oneDayAgo = new Date();
			oneDayAgo.setDate(oneDayAgo.getDate() - 1);

			const workflow = await createWorkflow({
				versionId: vCurrent,
				nodes: [{ ...testNode1, parameters: { a: 'current' } }],
			});
			await createWorkflowHistory(
				{
					...workflow,
					versionId: vOpenPinned,
					nodes: [{ ...testNode1, parameters: { a: 'open-pinned' } }],
				},
				undefined,
				undefined,
				{ createdAt: tenDaysAgo, name: 'Open review pin' },
			);
			await createWorkflowHistory(
				{
					...workflow,
					versionId: vClosedPinned,
					nodes: [{ ...testNode1, parameters: { a: 'closed-pinned' } }],
				},
				undefined,
				undefined,
				{ createdAt: tenDaysAgo, name: 'Closed review pin' },
			);
			await createWorkflowHistory(workflow);

			const project = await createTeamProject('Reviews Project');
			const requestRepository = Container.get(WorkflowReviewRequestRepository);
			const linkRepository = Container.get(WorkflowReviewRequestWorkflowRepository);

			const openRequest = await requestRepository.createRequest(
				{ projectId: project.id, title: 'Open review', createdById: null },
				{},
			);
			await linkRepository.createWorkflowRow(
				{
					workflowReviewRequestId: openRequest.id,
					workflowId: workflow.id,
					workflowVersionId: vOpenPinned,
				},
				{},
			);

			const closedRequest = await requestRepository.createRequest(
				{ projectId: project.id, title: 'Closed review', state: 'closed', createdById: null },
				{},
			);
			await linkRepository.createWorkflowRow(
				{
					workflowReviewRequestId: closedRequest.id,
					workflowId: workflow.id,
					workflowVersionId: vClosedPinned,
				},
				{},
			);

			const repository = Container.get(WorkflowHistoryRepository);

			// preserveNamedVersions = false: the name on the pins must not save them here
			await repository.deleteEarlierThanExceptCurrentAndActive(oneDayAgo, false);

			const remainingIds = (await repository.find()).map((r) => r.versionId);
			expect(remainingIds).toContain(vOpenPinned); // preserved: pinned by an open review
			expect(remainingIds).not.toContain(vClosedPinned); // pruned: its review is closed

			// The pin was nulled by the FK, not left dangling
			const closedLinkRows = await linkRepository.findByRequestId(closedRequest.id, {});
			expect(closedLinkRows[0]?.workflowVersionId).toBeNull();
		});
	});

	describe('getWorkflowIdsInRange', () => {
		it('should return versions in range', async () => {
			const now = Date.now();
			const twoSecondsAhead = new Date(now + 2 * 1000);
			const fourSecondsAhead = new Date(now + 4 * 1000);
			const sixSecondsAhead = new Date(now + 6 * 1000);

			const workflowA = await createWorkflow({
				versionId: uuid(),
				nodes: [{ ...testNode1, parameters: { a: 'a' } }],
			});

			// Create workflow history for the initial version
			await createWorkflowHistory(workflowA, undefined, undefined, { createdAt: new Date(now) });

			await createWorkflowHistory(
				{
					...workflowA,
					versionId: uuid(),
					nodes: [{ ...testNode1, parameters: { a: 'abcd' } }],
				},
				undefined,
				undefined,
				{ createdAt: twoSecondsAhead },
			);

			const workflowB = await createWorkflow({
				versionId: uuid(),
				nodes: [{ ...testNode1, parameters: { a: 'a' } }],
			});
			await createWorkflowHistory(workflowB, undefined, undefined, { createdAt: fourSecondsAhead });

			// ACT
			const repository = Container.get(WorkflowHistoryRepository);
			{
				const ids = await repository.getWorkflowIdsInRange(sixSecondsAhead, sixSecondsAhead);
				expect(ids).toEqual([]);
			}
			{
				const ids = await repository.getWorkflowIdsInRange(fourSecondsAhead, sixSecondsAhead);
				expect(ids).toEqual([workflowB.id]);
			}
			{
				const ids = await repository.getWorkflowIdsInRange(twoSecondsAhead, sixSecondsAhead);
				expect(ids).toEqual(expect.arrayContaining([workflowA.id, workflowB.id]));
			}
		});
	});
});
