import { Logger } from '@n8n/backend-common';
import { mockInstance, mockLogger, testDb, testModules } from '@n8n/backend-test-utils';
import { SecretsProviderConnectionRepository, SettingsRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { Cipher } from 'n8n-core';

import { ExternalSecretsProviders } from '@/modules/external-secrets.ee/external-secrets-providers.ee';
import { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config';
import { ExternalSecretsModule } from '@/modules/external-secrets.ee/external-secrets.module';
import { PubSubEventBus } from '@/scaling/pubsub/pubsub.eventbus';
import { PubSubRegistry } from '@/scaling/pubsub/pubsub.registry';

import {
	AnotherDummyProvider,
	DummyProvider,
	MockProviders,
} from '../../shared/external-secrets/utils';

const mockProvidersInstance = new MockProviders();
mockInstance(ExternalSecretsProviders, mockProvidersInstance);

const waitForEventHandler = async (ms = 500) => {
	await new Promise((resolve) => setTimeout(resolve, ms));
};

const testReloadEvent = async (
	pubsubEventBus: PubSubEventBus,
	{ connectsAllProviders = false }: { connectsAllProviders?: boolean } = {},
) => {
	const initSpy = jest.spyOn(DummyProvider.prototype, 'init');
	const connectSpy = jest.spyOn(DummyProvider.prototype, 'connect');
	const disconnectSpy = jest.spyOn(DummyProvider.prototype, 'disconnect');
	const updateSpy = jest.spyOn(DummyProvider.prototype, 'update');

	const initAnotherSpy = jest.spyOn(AnotherDummyProvider.prototype, 'init');
	const connectAnotherSpy = jest.spyOn(AnotherDummyProvider.prototype, 'connect');
	const disconnectAnotherSpy = jest.spyOn(AnotherDummyProvider.prototype, 'disconnect');
	const updateAnotherSpy = jest.spyOn(AnotherDummyProvider.prototype, 'update');

	// Emit event and wait for async handler to complete
	pubsubEventBus.emit('reload-external-secrets-providers');
	await waitForEventHandler();

	expect(disconnectSpy).toHaveBeenCalled();
	expect(initSpy).toHaveBeenCalled();
	expect(connectSpy).toHaveBeenCalled();
	expect(updateSpy).toHaveBeenCalled();

	expect(disconnectAnotherSpy).toHaveBeenCalled();
	expect(initAnotherSpy).toHaveBeenCalled();
	if (connectsAllProviders) {
		expect(connectAnotherSpy).toHaveBeenCalled();
		expect(updateAnotherSpy).toHaveBeenCalled();
	} else {
		expect(connectAnotherSpy).not.toHaveBeenCalled();
		expect(updateAnotherSpy).not.toHaveBeenCalled();
	}
};

describe('External Secrets Event Handling', () => {
	let pubsubEventBus: PubSubEventBus;
	let pubSubRegistry: PubSubRegistry;

	beforeAll(async () => {
		await testModules.loadModules(['external-secrets']);
		await testDb.init();

		Container.set(Logger, mockLogger());

		// Get pub/sub infrastructure from DI container
		pubsubEventBus = Container.get(PubSubEventBus);
		pubSubRegistry = Container.get(PubSubRegistry);
	});

	afterEach(() => {
		jest.clearAllMocks();
	});

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

	describe('using settings store', () => {
		let module: ExternalSecretsModule;

		beforeAll(async () => {
			mockProvidersInstance.setProviders({
				dummy: DummyProvider,
				another_dummy: AnotherDummyProvider,
			});

			const config = Container.get(ExternalSecretsConfig);
			(config as any).externalSecretsForProjects = false;
			(config as any).externalSecretsMultipleConnections = false;

			const settingsRepository = Container.get(SettingsRepository);
			const cipher = Container.get(Cipher);

			const settings = {
				dummy: {
					connected: true,
					connectedAt: new Date(),
					settings: {},
				},
				another_dummy: {
					connected: false,
					connectedAt: new Date(),
					settings: {},
				},
			};

			await settingsRepository.save({
				key: 'feature.externalSecrets',
				value: cipher.encrypt(settings),
				loadOnStartup: false,
			});

			module = Container.get(ExternalSecretsModule);
			await module.init();

			// IMPORTANT: Initialize PubSubRegistry AFTER module.init() to wire up decorators
			pubSubRegistry.init();
		});

		afterAll(async () => {
			await module.shutdown();
		});

		it('should reload providers when reload-external-secrets-providers event is emitted', async () =>
			await testReloadEvent(pubsubEventBus));
	});

	describe('using providers connections', () => {
		let module: ExternalSecretsModule;

		beforeAll(async () => {
			mockProvidersInstance.setProviders({
				dummy: DummyProvider,
				another_dummy: AnotherDummyProvider,
			});

			const config = Container.get(ExternalSecretsConfig);
			(config as any).externalSecretsForProjects = true;

			const cipher = Container.get(Cipher);
			const encryptedSettings = cipher.encrypt(JSON.stringify({}));

			const connectionRepository = Container.get(SecretsProviderConnectionRepository);
			await connectionRepository.save({
				providerKey: 'my-vault',
				type: 'dummy',
				encryptedSettings,
			});

			await connectionRepository.save({
				providerKey: 'another-vault',
				type: 'another_dummy',
				encryptedSettings,
			});

			module = Container.get(ExternalSecretsModule);
			await module.init();

			// IMPORTANT: Initialize PubSubRegistry AFTER module.init() to wire up decorators
			pubSubRegistry.init();
		});

		afterAll(async () => {
			await module.shutdown();
		});

		it('should reload providers when reload-external-secrets-providers event is emitted', async () =>
			await testReloadEvent(pubsubEventBus, { connectsAllProviders: true }));
	});
});
