import {
	createWorkflow,
	shareWorkflowWithUsers,
	testDb,
	mockInstance,
} from '@n8n/backend-test-utils';
import type { User } from '@n8n/db';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import type { IWorkflowBase } from 'n8n-workflow';

import type {
	WorkflowClosedMessage,
	WorkflowOpenedMessage,
	WriteAccessRequestedMessage,
	WriteAccessReleaseRequestedMessage,
} from '@/collaboration/collaboration.message';
import { CollaborationService } from '@/collaboration/collaboration.service';
import { Push } from '@/push';
import { CacheService } from '@/services/cache/cache.service';
import { createMember, createOwner } from '@test-integration/db/users';

describe('CollaborationService', () => {
	mockInstance(Push, new Push(mock(), mock(), mock(), mock(), mock()));
	let pushService: Push;
	let collaborationService: CollaborationService;
	let owner: User;
	let memberWithoutAccess: User;
	let memberWithAccess: User;
	let workflow: IWorkflowBase;
	let cacheService: CacheService;

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

		pushService = Container.get(Push);
		collaborationService = Container.get(CollaborationService);
		cacheService = Container.get(CacheService);

		await cacheService.init();

		[owner, memberWithAccess, memberWithoutAccess] = await Promise.all([
			createOwner(),
			createMember(),
			createMember(),
		]);
		workflow = await createWorkflow({}, owner);
		await shareWorkflowWithUsers(workflow, [memberWithAccess]);
	});

	afterEach(async () => {
		jest.resetAllMocks();
		await cacheService.reset();
	});

	const sendWorkflowOpenedMessage = async (
		workflowId: string,
		userId: string,
		clientId = 'test-client-id',
	) => {
		const openMessage: WorkflowOpenedMessage = {
			type: 'workflowOpened',
			workflowId,
		};

		return await collaborationService.handleUserMessage(userId, clientId, openMessage);
	};

	const sendWorkflowClosedMessage = async (
		workflowId: string,
		userId: string,
		clientId = 'test-client-id',
	) => {
		const openMessage: WorkflowClosedMessage = {
			type: 'workflowClosed',
			workflowId,
		};

		return await collaborationService.handleUserMessage(userId, clientId, openMessage);
	};

	const sendWriteAccessRequestedMessage = async (
		workflowId: string,
		userId: string,
		clientId = 'test-client-id',
	) => {
		const message: WriteAccessRequestedMessage = {
			type: 'writeAccessRequested',
			workflowId,
		};

		return await collaborationService.handleUserMessage(userId, clientId, message);
	};

	const sendWriteAccessReleaseRequestedMessage = async (
		workflowId: string,
		userId: string,
		clientId = 'test-client-id',
	) => {
		const message: WriteAccessReleaseRequestedMessage = {
			type: 'writeAccessReleaseRequested',
			workflowId,
		};

		return await collaborationService.handleUserMessage(userId, clientId, message);
	};

	describe('workflow opened message', () => {
		it('should emit collaboratorsChanged after workflowOpened', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');

			// Act
			await sendWorkflowOpenedMessage(workflow.id, owner.id, 'owner-client-id');
			await sendWorkflowOpenedMessage(workflow.id, memberWithAccess.id, 'member-client-id');

			// Assert
			expect(sendToUsersSpy).toHaveBeenNthCalledWith(
				1,
				{
					type: 'collaboratorsChanged',
					data: {
						collaborators: [
							{
								lastSeen: expect.any(String),
								user: owner.toIUser(),
							},
						],
						workflowId: workflow.id,
					},
				},
				[owner.id],
			);
			expect(sendToUsersSpy).toHaveBeenNthCalledWith(
				2,
				{
					type: 'collaboratorsChanged',
					data: {
						collaborators: expect.arrayContaining([
							expect.objectContaining({
								lastSeen: expect.any(String),
								user: expect.objectContaining({
									id: owner.id,
								}),
							}),
							expect.objectContaining({
								lastSeen: expect.any(String),
								user: expect.objectContaining({
									id: memberWithAccess.id,
								}),
							}),
						]),
						workflowId: workflow.id,
					},
				},
				[owner.id, memberWithAccess.id],
			);
		});

		it("should not emit collaboratorsChanged if user don't have access to the workflow", async () => {
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');

			// Act
			await sendWorkflowOpenedMessage(workflow.id, memberWithoutAccess.id);

			// Assert
			expect(sendToUsersSpy).not.toHaveBeenCalled();
		});
	});

	describe('workflow closed message', () => {
		it('should not emit collaboratorsChanged after workflowClosed when there are no active users', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, owner.id);
			sendToUsersSpy.mockClear();

			// Act
			await sendWorkflowClosedMessage(workflow.id, owner.id);

			// Assert
			expect(sendToUsersSpy).not.toHaveBeenCalled();
		});

		it('should emit collaboratorsChanged after workflowClosed when there are active users', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, owner.id, 'owner-client-id');
			await sendWorkflowOpenedMessage(workflow.id, memberWithAccess.id, 'member-client-id');
			sendToUsersSpy.mockClear();

			// Act
			await sendWorkflowClosedMessage(workflow.id, owner.id, 'owner-client-id');

			// Assert
			expect(sendToUsersSpy).toHaveBeenCalledWith(
				{
					type: 'collaboratorsChanged',
					data: {
						collaborators: expect.arrayContaining([
							expect.objectContaining({
								lastSeen: expect.any(String),
								user: expect.objectContaining({
									id: memberWithAccess.id,
								}),
							}),
						]),
						workflowId: workflow.id,
					},
				},
				[memberWithAccess.id],
			);
		});

		it("should not emit collaboratorsChanged if user don't have access to the workflow", async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, owner.id);
			sendToUsersSpy.mockClear();

			// Act
			await sendWorkflowClosedMessage(workflow.id, memberWithoutAccess.id);

			// Assert
			expect(sendToUsersSpy).not.toHaveBeenCalled();
		});
	});

	describe('write lock acquisition', () => {
		it('should grant write lock to first user', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, owner.id);
			sendToUsersSpy.mockClear();

			// Act
			await sendWriteAccessRequestedMessage(workflow.id, owner.id);

			// Assert
			expect(sendToUsersSpy).toHaveBeenCalledWith(
				{
					type: 'writeAccessAcquired',
					data: {
						workflowId: workflow.id,
						userId: owner.id,
						clientId: 'test-client-id',
					},
				},
				[owner.id],
			);
		});

		it('should deny write lock if another user holds it', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, owner.id, 'owner-client-id');
			await sendWorkflowOpenedMessage(workflow.id, memberWithAccess.id, 'member-client-id');

			// Owner acquires the lock first
			await sendWriteAccessRequestedMessage(workflow.id, owner.id, 'owner-client-id');
			sendToUsersSpy.mockClear();

			// Act - Member tries to acquire the lock
			await sendWriteAccessRequestedMessage(workflow.id, memberWithAccess.id, 'member-client-id');

			// Assert - No message should be sent (silent rejection)
			expect(sendToUsersSpy).not.toHaveBeenCalled();
		});

		it('should allow lock acquisition after release', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, owner.id, 'owner-client-id');
			await sendWorkflowOpenedMessage(workflow.id, memberWithAccess.id, 'member-client-id');

			// Owner acquires the lock
			await sendWriteAccessRequestedMessage(workflow.id, owner.id, 'owner-client-id');

			// Owner releases the lock
			await sendWriteAccessReleaseRequestedMessage(workflow.id, owner.id, 'owner-client-id');
			sendToUsersSpy.mockClear();

			// Act - Member tries to acquire the lock
			await sendWriteAccessRequestedMessage(workflow.id, memberWithAccess.id, 'member-client-id');

			// Assert - Member should successfully acquire the lock
			expect(sendToUsersSpy).toHaveBeenCalledWith(
				{
					type: 'writeAccessAcquired',
					data: {
						workflowId: workflow.id,
						userId: memberWithAccess.id,
						clientId: 'member-client-id',
					},
				},
				[owner.id, memberWithAccess.id],
			);
		});

		it('should allow same user to re-acquire lock they already hold', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, owner.id);

			// Owner acquires the lock
			await sendWriteAccessRequestedMessage(workflow.id, owner.id);
			sendToUsersSpy.mockClear();

			// Act - Owner tries to acquire the lock again
			await sendWriteAccessRequestedMessage(workflow.id, owner.id);

			// Assert - Should succeed (idempotent)
			expect(sendToUsersSpy).toHaveBeenCalledWith(
				{
					type: 'writeAccessAcquired',
					data: {
						workflowId: workflow.id,
						userId: owner.id,
						clientId: 'test-client-id',
					},
				},
				[owner.id],
			);
		});

		it('should not grant write lock to user without write access', async () => {
			// Arrange
			const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers');
			await sendWorkflowOpenedMessage(workflow.id, memberWithoutAccess.id);
			sendToUsersSpy.mockClear();

			// Act - User without access tries to acquire lock
			await sendWriteAccessRequestedMessage(workflow.id, memberWithoutAccess.id);

			// Assert - No messages should be sent
			expect(sendToUsersSpy).not.toHaveBeenCalled();
		});
	});

	describe('getWriteLock', () => {
		it('should return lock holder for user with read access', async () => {
			// Arrange
			await sendWorkflowOpenedMessage(workflow.id, owner.id);
			await sendWriteAccessRequestedMessage(workflow.id, owner.id);

			// Act
			const lockHolder = await collaborationService.getWriteLock(memberWithAccess.id, workflow.id);

			// Assert
			expect(lockHolder).toEqual({
				clientId: 'test-client-id',
				userId: owner.id,
			});
		});

		it('should return null for user without read access', async () => {
			// Arrange
			await sendWorkflowOpenedMessage(workflow.id, owner.id);
			await sendWriteAccessRequestedMessage(workflow.id, owner.id);

			// Act
			const lockHolder = await collaborationService.getWriteLock(
				memberWithoutAccess.id,
				workflow.id,
			);

			// Assert
			expect(lockHolder).toBeNull();
		});

		it('should return null when no lock is held', async () => {
			// Act
			const lockHolder = await collaborationService.getWriteLock(owner.id, workflow.id);

			// Assert
			expect(lockHolder).toBeNull();
		});
	});
});
