import { removePreviewToken } from '@/features/shared/nodeCreator/nodeCreator.utils';
import { useCommunityNodesStore } from '../communityNodes.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
	useWorkflowDocumentStore,
	createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import type { CommunityNodeType } from '@n8n/api-types';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import { useInstallNode } from './useInstallNode';
import { useToast } from '@/app/composables/useToast';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { DEFAULT_SETTINGS } from '@/app/constants/workflows';

vi.mock('@/app/composables/useCanvasOperations', () => ({
	useCanvasOperations: vi.fn().mockReturnValue({
		initializeUnknownNodes: vi.fn(),
	}),
}));

vi.mock('@/app/composables/useToast', () => ({
	useToast: vi.fn().mockReturnValue({
		showError: vi.fn(),
		showMessage: vi.fn(),
	}),
}));

vi.mock('@/app/composables/useTelemetry', () => {
	const track = vi.fn();
	return {
		useTelemetry: () => {
			return {
				track,
			};
		},
	};
});

vi.mock('@n8n/i18n', () => ({
	i18n: {
		baseText: vi.fn((key: string) => key),
	},
	useI18n: vi.fn(() => ({
		baseText: vi.fn((key: string) => key),
	})),
}));

vi.mock('@/features/shared/nodeCreator/nodeCreator.utils', () => ({
	removePreviewToken: vi.fn((key: string) => key.replace('preview:', '')),
}));

let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
let communityNodesStore: ReturnType<typeof useCommunityNodesStore>;
let credentialsStore: ReturnType<typeof useCredentialsStore>;
let usersStore: ReturnType<typeof useUsersStore>;
let settingsStore: ReturnType<typeof useSettingsStore>;
let toast: ReturnType<typeof useToast>;
let canvasOperations: ReturnType<typeof useCanvasOperations>;

const initializeUnknownNodes = vi.fn();
const showError = vi.fn();
const showMessage = vi.fn();

beforeEach(() => {
	const pinia = createTestingPinia({ stubActions: false });
	setActivePinia(pinia);

	nodeTypesStore = useNodeTypesStore(pinia);
	communityNodesStore = useCommunityNodesStore(pinia);
	credentialsStore = useCredentialsStore(pinia);
	workflowsStore = useWorkflowsStore(pinia);
	usersStore = useUsersStore(pinia);
	settingsStore = useSettingsStore(pinia);

	canvasOperations = {
		initializeUnknownNodes,
	} as unknown as ReturnType<typeof useCanvasOperations>;

	vi.mocked(useCanvasOperations).mockReturnValue(canvasOperations);

	toast = {
		showError,
		showMessage,
	} as unknown as ReturnType<typeof useToast>;

	vi.mocked(useToast).mockReturnValue(toast);

	Object.defineProperty(usersStore, 'isAdmin', {
		value: true,
		writable: true,
	});
	Object.defineProperty(usersStore, 'isInstanceOwner', {
		value: false,
		writable: true,
	});
	Object.defineProperty(usersStore, 'isAdminOrOwner', {
		value: true,
		writable: true,
	});

	vi.spyOn(communityNodesStore, 'installPackage').mockResolvedValue(undefined);
	vi.spyOn(nodeTypesStore, 'getNodeTypes').mockResolvedValue(undefined);
	vi.spyOn(nodeTypesStore, 'fetchCommunityNodePreviews').mockResolvedValue(undefined);
	vi.spyOn(credentialsStore, 'fetchCredentialTypes').mockResolvedValue(undefined);
	vi.spyOn(nodeTypesStore, 'getCommunityNodeAttributes').mockResolvedValue({
		npmVersion: '1.0.0',
		authorGithubUrl: 'https://github.com/test',
		authorName: 'Test Author',
		checksum: 'test-checksum',
		description: 'Test description',
		documentationUrl: 'https://docs.test.com',
		icon: 'test-icon',
		iconUrl: 'https://icon.test.com',
		id: 'test-id',
		license: 'MIT',
		name: 'Test Node',
		packageName: 'test-package',
		version: '1.0.0',
	} as unknown as CommunityNodeType);

	workflowsStore.workflowId = 'test-workflow';
	const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
	workflowDocumentStore.hydrate({
		id: 'test-workflow',
		name: 'Test Workflow',
		active: false,
		isArchived: false,
		createdAt: new Date().toISOString(),
		updatedAt: new Date().toISOString(),
		nodes: [],
		connections: {},
		settings: { ...DEFAULT_SETTINGS },
		tags: [],
		versionId: '1',
		activeVersionId: '1',
	});

	vi.clearAllMocks();
});

describe('useInstallNode', () => {
	describe('installNode', () => {
		it('should return error when user is not an owner or admin', async () => {
			Object.defineProperty(usersStore, 'isAdmin', {
				value: false,
				writable: true,
			});
			Object.defineProperty(usersStore, 'isInstanceOwner', {
				value: false,
				writable: true,
			});
			Object.defineProperty(usersStore, 'isAdminOrOwner', {
				value: false,
				writable: true,
			});
			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(false);
			expect(result.error).toBeInstanceOf(Error);
			expect(result.error?.message).toBe('User is not an owner or admin');
			expect(showError).toHaveBeenCalledWith(
				expect.any(Error),
				'settings.communityNodes.messages.install.error',
			);
		});

		it.each([
			{ isAdmin: true, isInstanceOwner: false, label: 'admin' },
			{ isAdmin: false, isInstanceOwner: true, label: 'instance owner' },
		])('should allow installing when user is $label', async ({ isAdmin, isInstanceOwner }) => {
			Object.defineProperty(usersStore, 'isAdmin', {
				value: isAdmin,
				writable: true,
			});
			Object.defineProperty(usersStore, 'isInstanceOwner', {
				value: isInstanceOwner,
				writable: true,
			});
			Object.defineProperty(usersStore, 'isAdminOrOwner', {
				value: isAdmin || isInstanceOwner,
				writable: true,
			});
			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(true);
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith(
				'test-package',
				true,
				'1.0.0',
			);
		});

		it('should install verified node with npm version', async () => {
			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(true);
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith(
				'test-package',
				true,
				'1.0.0',
			);
			expect(nodeTypesStore.getNodeTypes).toHaveBeenCalled();
			expect(nodeTypesStore.fetchCommunityNodePreviews).toHaveBeenCalled();
			expect(credentialsStore.fetchCredentialTypes).toHaveBeenCalledWith(true);
			expect(showMessage).toHaveBeenCalledWith({
				title: 'settings.communityNodes.messages.install.success',
				type: 'success',
			});
		});

		it('should install verified node with pinned version when unverified packages are disabled', async () => {
			Object.defineProperty(settingsStore, 'isUnverifiedPackagesEnabled', {
				value: false,
				writable: true,
			});
			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(true);
			expect(nodeTypesStore.getCommunityNodeAttributes).toHaveBeenCalledWith('test-node');
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith(
				'test-package',
				true,
				'1.0.0',
			);
		});

		it('should install verified node as latest when unverified packages are enabled', async () => {
			Object.defineProperty(settingsStore, 'isUnverifiedPackagesEnabled', {
				value: true,
				writable: true,
			});
			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(true);
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith('test-package');
			expect(nodeTypesStore.getCommunityNodeAttributes).not.toHaveBeenCalled();
		});

		it('should install unverified node without version regardless of unverifiedEnabled setting', async () => {
			Object.defineProperty(settingsStore, 'isUnverifiedPackagesEnabled', {
				value: false,
				writable: true,
			});
			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'unverified',
				packageName: 'test-package',
			});

			expect(result.success).toBe(true);
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith('test-package');
			expect(nodeTypesStore.getCommunityNodeAttributes).not.toHaveBeenCalled();
		});

		it('should install unverified node without npm version', async () => {
			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'unverified',
				packageName: 'test-package',
			});

			expect(result.success).toBe(true);
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith('test-package');
			expect(nodeTypesStore.getNodeTypes).toHaveBeenCalled();
			expect(nodeTypesStore.fetchCommunityNodePreviews).toHaveBeenCalled();
			expect(credentialsStore.fetchCredentialTypes).toHaveBeenCalledWith(true);
		});

		it('should initialize unknown nodes when nodeType is provided and workflow has nodes', async () => {
			const mockNodes = [
				{
					id: 'node-1',
					type: 'test-node',
					name: 'Node 1',
					typeVersion: 1,
					position: [128, 128] as [number, number],
					parameters: {},
				},
				{
					id: 'node-2',
					type: 'other-node',
					name: 'Node 2',
					typeVersion: 1,
					position: [256, 256] as [number, number],
					parameters: {},
				},
			];
			const store = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
			store.setNodes(mockNodes);

			const { installNode } = useInstallNode();

			await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'preview:test-node',
			});

			expect(removePreviewToken).toHaveBeenCalledWith('preview:test-node');
			expect(initializeUnknownNodes).toHaveBeenCalledWith([
				{
					id: 'node-1',
					type: 'test-node',
					name: 'Node 1',
					typeVersion: 1,
					position: [128, 128] as [number, number],
					parameters: {},
				},
			]);
		});

		it('should not initialize nodes when nodeType is not provided', async () => {
			useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow')).setNodes([
				{
					id: 'node-1',
					type: 'test-node',
					name: 'Node 1',
					typeVersion: 1,
					position: [128, 128] as [number, number],
					parameters: {},
				},
			]);

			const { installNode } = useInstallNode();

			await installNode({
				type: 'unverified',
				packageName: 'test-package',
			});

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

		it('should not initialize nodes when workflow has no nodes', async () => {
			useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow')).setNodes([]);

			const { installNode } = useInstallNode();

			await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

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

		it('should handle installation errors', async () => {
			const error = new Error('Installation failed');
			vi.mocked(communityNodesStore.installPackage).mockRejectedValue(error);

			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(false);
			expect(result.error).toBe(error);
			expect(showError).toHaveBeenCalledWith(
				error,
				'settings.communityNodes.messages.install.error',
			);
		});

		it('should handle getNpmVersion returning undefined', async () => {
			vi.mocked(nodeTypesStore.getCommunityNodeAttributes).mockResolvedValue(null);

			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(true);
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith(
				'test-package',
				true,
				undefined,
			);
		});

		it('should handle getNpmVersion errors', async () => {
			const error = new Error('Failed to get npm version');
			vi.mocked(nodeTypesStore.getCommunityNodeAttributes).mockRejectedValue(error);

			const { installNode } = useInstallNode();

			const result = await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(result.success).toBe(false);
			expect(result.error).toBe(error);
		});

		it('should not track telemetry events when telemetry is not provided', async () => {
			const { installNode } = useInstallNode();
			const { track } = useTelemetry();

			const result = await installNode({
				type: 'unverified',
				packageName: 'test-package',
			});

			expect(result.success).toBe(true);
			expect(track).not.toHaveBeenCalled();
		});

		it('should track telemetry events when telemetry is provided', async () => {
			const { installNode } = useInstallNode();
			const { track } = useTelemetry();

			const result = await installNode({
				type: 'unverified',
				packageName: 'test-package',
				telemetry: {
					hasQuickConnect: true,
					source: 'node detail view',
				},
			});

			expect(result.success).toBe(true);
			expect(track).toHaveBeenCalledWith('user started cnr package install', {
				input_string: 'test-package',
				has_quick_connect: true,
				source: 'node detail view',
			});
		});
	});

	describe('loading state', () => {
		it('should set loading to true during installation', async () => {
			vi.mocked(communityNodesStore.installPackage).mockImplementation(async () => {
				await new Promise((resolve) => setTimeout(resolve, 10));
			});

			const { installNode, loading } = useInstallNode();

			const installPromise = installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(loading.value).toBe(true);
			await installPromise;
			expect(loading.value).toBe(false);
		});

		it('should set loading to false even when installation fails', async () => {
			vi.mocked(communityNodesStore.installPackage).mockRejectedValue(
				new Error('Installation failed'),
			);

			const { installNode, loading } = useInstallNode();

			await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(loading.value).toBe(false);
		});
	});

	describe('getNpmVersion', () => {
		it('should return npm version from community node attributes', async () => {
			const mockAttributes = {
				npmVersion: '2.0.0',
				authorGithubUrl: 'https://github.com/test',
				authorName: 'Test Author',
				checksum: 'test-checksum',
				description: 'Test description',
				documentationUrl: 'https://docs.test.com',
				icon: 'test-icon',
				iconUrl: 'https://icon.test.com',
				id: 'test-id',
				license: 'MIT',
				name: 'Test Node',
				packageName: 'test-package',
				version: '2.0.0',
			};
			vi.mocked(nodeTypesStore.getCommunityNodeAttributes).mockResolvedValue(
				mockAttributes as unknown as CommunityNodeType,
			);

			// We need to access the private getNpmVersion function through the composable
			// Since it's not exported, we'll test it indirectly through the installNode function
			const { installNode } = useInstallNode();

			await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'test-node',
			});

			expect(nodeTypesStore.getCommunityNodeAttributes).toHaveBeenCalledWith('test-node');
			expect(communityNodesStore.installPackage).toHaveBeenCalledWith(
				'test-package',
				true,
				'2.0.0',
			);
		});
	});

	describe('node filtering and initialization', () => {
		it('should filter nodes by exact type match after removing preview token', async () => {
			const mockNodes = [
				{
					id: 'node-1',
					type: 'preview:test-node',
					name: 'Node 1',
					typeVersion: 1,
					position: [128, 128] as [number, number],
					parameters: {},
				},
				{
					id: 'node-2',
					type: 'test-node',
					name: 'Node 2',
					typeVersion: 1,
					position: [256, 256] as [number, number],
					parameters: {},
				},
				{
					id: 'node-3',
					type: 'other-node',
					name: 'Node 3',
					typeVersion: 1,
					position: [384, 384] as [number, number],
					parameters: {},
				},
			];
			const store = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
			store.setNodes(mockNodes);

			vi.mocked(removePreviewToken).mockReturnValue('test-node');

			const { installNode } = useInstallNode();

			await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'preview:test-node',
			});

			expect(removePreviewToken).toHaveBeenCalledWith('preview:test-node');
			expect(initializeUnknownNodes).toHaveBeenCalledWith([
				{
					id: 'node-2',
					type: 'test-node',
					name: 'Node 2',
					typeVersion: 1,
					position: [256, 256] as [number, number],
					parameters: {},
				},
			]);
		});

		it('should handle multiple nodes of the same type', async () => {
			const mockNodes = [
				{
					id: 'node-1',
					type: 'test-node',
					name: 'Node 1',
					typeVersion: 1,
					position: [128, 128] as [number, number],
					parameters: {},
				},
				{
					id: 'node-2',
					type: 'test-node',
					name: 'Node 2',
					typeVersion: 1,
					position: [256, 256] as [number, number],
					parameters: {},
				},
				{
					id: 'node-3',
					type: 'other-node',
					name: 'Node 3',
					typeVersion: 1,
					position: [384, 384] as [number, number],
					parameters: {},
				},
			];
			const store = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
			store.setNodes(mockNodes);

			vi.mocked(removePreviewToken).mockReturnValue('test-node');

			const { installNode } = useInstallNode();

			await installNode({
				type: 'verified',
				packageName: 'test-package',
				nodeType: 'preview:test-node',
			});

			expect(initializeUnknownNodes).toHaveBeenCalledWith([
				{
					id: 'node-1',
					type: 'test-node',
					name: 'Node 1',
					typeVersion: 1,
					position: [128, 128] as [number, number],
					parameters: {},
				},
				{
					id: 'node-2',
					type: 'test-node',
					name: 'Node 2',
					typeVersion: 1,
					position: [256, 256] as [number, number],
					parameters: {},
				},
			]);
		});
	});
});
