// Import zod alias support before importing Start command
import '@/zod-alias-support';

import { mockInstance } from '@n8n/backend-test-utils';
import { AuthRolesService, DbConnection, DeploymentKeyRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { InstanceSettings } from 'n8n-core';

import { BinaryDataConfig } from 'n8n-core';

import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import { JwtService } from '@/services/jwt.service';
import { ActiveWorkflowManager } from '@/active-workflow-manager';
import { AuthHandlerRegistry } from '@/auth/auth-handler.registry';
import { DeprecationService } from '@/deprecation/deprecation.service';
import { CredentialsOverwrites } from '@/credentials-overwrites';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { License } from '@/license';
import { MultiMainSetup } from '@/scaling/multi-main-setup.ee';
import { Start } from '../start';
import { WaitTracker } from '@/wait-tracker';
import { ErrorReporter } from 'n8n-core';
import { NodeTypes } from '@/node-types';
import { ShutdownService } from '@/shutdown/shutdown.service';
import type { AbstractServer } from '@/abstract-server';
import { PostHogClient } from '@/posthog';
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
import { WorkflowFailureNotificationEventRelay } from '@/events/relays/workflow-failure-notification.event-relay';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { CommunityPackagesConfig } from '@/modules/community-packages/community-packages.config';
import { CommunityPackagesService } from '@/modules/community-packages/community-packages.service';
import { TaskRunnerModule } from '@/task-runners/task-runner-module';

const authRolesService = mockInstance(AuthRolesService);
authRolesService.init.mockResolvedValue(undefined);

const deploymentKeyRepository = mockInstance(DeploymentKeyRepository);
deploymentKeyRepository.findActiveByType.mockResolvedValue(null);
deploymentKeyRepository.insertOrIgnore.mockResolvedValue(undefined);

const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
loadNodesAndCredentials.init.mockResolvedValue(undefined);
loadNodesAndCredentials.postProcessLoaders.mockResolvedValue(undefined);

const dbConnection = mockInstance(DbConnection);
dbConnection.init.mockResolvedValue(undefined);
dbConnection.migrate.mockResolvedValue(undefined);

mockInstance(ActiveWorkflowManager);
mockInstance(DeprecationService);
mockInstance(CredentialsOverwrites);
mockInstance(WaitTracker);
const license = mockInstance(License, {
	loadCertStr: async () => '',
	isMultiMainLicensed: () => true,
});
const multiMainSetup = mockInstance(MultiMainSetup);
multiMainSetup.registerEventHandlers.mockReturnValue(undefined);
const authHandlerRegistry = mockInstance(AuthHandlerRegistry);
authHandlerRegistry.init.mockResolvedValue(undefined);
const errorReporter = mockInstance(ErrorReporter);
errorReporter.init.mockResolvedValue(undefined);
mockInstance(NodeTypes);
const shutdownService = mockInstance(ShutdownService);
shutdownService.validate.mockReturnValue(undefined);
mockInstance(PostHogClient);
mockInstance(TelemetryEventRelay);
mockInstance(WorkflowFailureNotificationEventRelay);
mockInstance(MessageEventBus);
mockInstance(CommunityPackagesConfig);
const communityPackagesService = mockInstance(CommunityPackagesService);
communityPackagesService.init.mockResolvedValue(undefined);
const taskRunnerModule = mockInstance(TaskRunnerModule);
taskRunnerModule.start.mockResolvedValue(undefined);

const instanceSettings = Container.get(InstanceSettings);

describe('Start - AuthRolesService initialization', () => {
	let start: Start;

	const setupInstanceSettings = (
		instanceType: 'main' | 'worker' | 'webhook',
		isMultiMain: boolean,
		isLeader: boolean,
	) => {
		// @ts-expect-error - Read-only property, but needed for testing
		instanceSettings.instanceType = instanceType;
		Object.defineProperty(instanceSettings, 'isMultiMain', {
			get: jest.fn(() => isMultiMain),
			configurable: true,
		});
		Object.defineProperty(instanceSettings, 'isLeader', {
			get: jest.fn(() => isLeader),
			configurable: true,
		});
	};

	beforeEach(() => {
		jest.clearAllMocks();
		Container.reset();

		// Re-register all mocks
		Container.set(AuthRolesService, authRolesService);
		Container.set(LoadNodesAndCredentials, loadNodesAndCredentials);
		Container.set(DbConnection, dbConnection);
		Container.set(InstanceSettings, instanceSettings);
		Container.set(License, license);
		Container.set(ErrorReporter, errorReporter);
		Container.set(NodeTypes, mockInstance(NodeTypes));
		Container.set(ShutdownService, shutdownService);
		Container.set(ActiveWorkflowManager, mockInstance(ActiveWorkflowManager));
		Container.set(DeprecationService, mockInstance(DeprecationService));
		Container.set(CredentialsOverwrites, mockInstance(CredentialsOverwrites));
		Container.set(WaitTracker, mockInstance(WaitTracker));
		Container.set(MultiMainSetup, multiMainSetup);
		Container.set(AuthHandlerRegistry, authHandlerRegistry);
		Container.set(PostHogClient, mockInstance(PostHogClient));
		Container.set(TelemetryEventRelay, mockInstance(TelemetryEventRelay));
		Container.set(
			WorkflowFailureNotificationEventRelay,
			mockInstance(WorkflowFailureNotificationEventRelay),
		);
		Container.set(MessageEventBus, mockInstance(MessageEventBus));
		Container.set(CommunityPackagesConfig, mockInstance(CommunityPackagesConfig));
		Container.set(CommunityPackagesService, communityPackagesService);
		Container.set(TaskRunnerModule, taskRunnerModule);
		Container.set(DeploymentKeyRepository, deploymentKeyRepository);
		Container.set(
			JwtService,
			mockInstance(JwtService, { initialize: jest.fn().mockResolvedValue(undefined) }),
		);
		Container.set(
			BinaryDataConfig,
			mockInstance(BinaryDataConfig, { initialize: jest.fn().mockResolvedValue(undefined) }),
		);

		start = new Start();
		// @ts-expect-error - Accessing protected property for testing
		start.globalConfig = {
			executions: { mode: 'regular' },
			multiMainSetup: { enabled: false },
			endpoints: { disableUi: true, metrics: { enable: false }, health: '/health' },
			database: { type: 'sqlite' },
			sentry: {
				backendDsn: '',
				environment: 'test',
				deploymentName: 'test',
				profilesSampleRate: 0,
				tracesSampleRate: 0,
				eventLoopBlockThreshold: 0,
			},
			cache: { backend: 'memory' },
			taskRunners: {},
			expressionEngine: { engine: 'legacy', poolSize: 1, maxCodeCacheSize: 1024 },
		};
		// @ts-expect-error - Accessing protected method for testing
		start.initCrashJournal = jest.fn().mockResolvedValue(undefined);
		start.initLicense = jest.fn().mockResolvedValue(undefined);
		start.initOrchestration = jest.fn().mockResolvedValue(undefined);
		start.initBinaryDataService = jest.fn().mockResolvedValue(undefined);
		// @ts-expect-error - Accessing protected method for testing
		start.initDataDeduplicationService = jest.fn().mockResolvedValue(undefined);
		start.initExternalHooks = jest.fn().mockResolvedValue(undefined);
		start.initWorkflowHistory = jest.fn();
		// @ts-expect-error - Accessing private method for testing
		start.initInstanceSettingsLoader = jest.fn().mockResolvedValue(undefined);
		start.cleanupTestRunner = jest.fn().mockResolvedValue(undefined);
		// @ts-expect-error - Accessing private method for testing
		start.generateStaticAssets = jest.fn().mockResolvedValue(undefined);
		// @ts-expect-error - Accessing protected property for testing
		start.moduleRegistry = { initModules: jest.fn().mockResolvedValue(undefined) };
		// @ts-expect-error - Accessing protected property for testing
		start.executionContextHookRegistry = { init: jest.fn().mockResolvedValue(undefined) };
		// @ts-expect-error - Accessing protected property for testing
		start.license = license;
		// @ts-expect-error - Accessing protected property for testing
		start.server = mock<AbstractServer>({ init: jest.fn().mockResolvedValue(undefined) });
	});

	describe('init - conditional initialization based on instance type and leader status', () => {
		it('should initialize AuthRolesService when instanceType is main and not multi-main', async () => {
			setupInstanceSettings('main', false, false);

			await start.init();

			expect(authRolesService.init).toHaveBeenCalledTimes(1);
		});

		it('should initialize AuthRolesService when instanceType is main, multi-main enabled, and is leader', async () => {
			setupInstanceSettings('main', true, true);
			// @ts-expect-error - Accessing protected property for testing
			start.globalConfig = {
				executions: { mode: 'queue' },
				multiMainSetup: { enabled: true },
				endpoints: { disableUi: true, metrics: { enable: false }, health: '/health' },
				database: { type: 'sqlite' },
				sentry: {
					backendDsn: '',
					environment: 'test',
					deploymentName: 'test',
					profilesSampleRate: 0,
					tracesSampleRate: 0,
					eventLoopBlockThreshold: 0,
				},
				cache: { backend: 'memory' },
				taskRunners: {},
				expressionEngine: { engine: 'legacy', poolSize: 1, maxCodeCacheSize: 1024 },
			};

			await start.init();

			expect(authRolesService.init).toHaveBeenCalledTimes(1);
		});

		it('should NOT initialize AuthRolesService when instanceType is not main', async () => {
			setupInstanceSettings('worker', false, false);

			await start.init();

			expect(authRolesService.init).not.toHaveBeenCalled();
		});

		it('should initialize AuthRolesService when instanceType is main, multi-main enabled, but NOT leader (advisory lock serializes)', async () => {
			setupInstanceSettings('main', true, false);
			// @ts-expect-error - Accessing protected property for testing
			start.globalConfig = {
				executions: { mode: 'queue' },
				multiMainSetup: { enabled: true },
				endpoints: { disableUi: true, metrics: { enable: false }, health: '/health' },
				database: { type: 'sqlite' },
				sentry: {
					backendDsn: '',
					environment: 'test',
					deploymentName: 'test',
					profilesSampleRate: 0,
					tracesSampleRate: 0,
					eventLoopBlockThreshold: 0,
				},
				cache: { backend: 'memory' },
				taskRunners: {},
				expressionEngine: { engine: 'legacy', poolSize: 1, maxCodeCacheSize: 1024 },
			};

			await start.init();

			expect(authRolesService.init).toHaveBeenCalledTimes(1);
		});
	});

	describe('init - instance settings loader initialization', () => {
		it('should initialize instance settings loader when instanceType is main', async () => {
			setupInstanceSettings('main', false, false);

			await start.init();

			// @ts-expect-error - Accessing private method for testing
			expect(start.initInstanceSettingsLoader).toHaveBeenCalledTimes(1);
		});

		it('should NOT initialize instance settings loader when instanceType is not main', async () => {
			setupInstanceSettings('worker', false, false);

			await start.init();

			// @ts-expect-error - Accessing private method for testing
			expect(start.initInstanceSettingsLoader).not.toHaveBeenCalled();
		});
	});

	describe('init - multi-main follower license retry', () => {
		const multiMainConfig = {
			executions: { mode: 'queue' as const },
			multiMainSetup: { enabled: true },
			endpoints: { disableUi: true, metrics: { enable: false }, health: '/health' },
			database: { type: 'sqlite' },
			sentry: {
				backendDsn: '',
				environment: 'test',
				deploymentName: 'test',
				profilesSampleRate: 0,
				tracesSampleRate: 0,
				eventLoopBlockThreshold: 0,
			},
			cache: { backend: 'memory' },
			taskRunners: {},
			expressionEngine: { engine: 'legacy' as const, poolSize: 1, maxCodeCacheSize: 1024 },
		};

		beforeEach(() => {
			jest.useFakeTimers();
		});

		afterEach(() => {
			jest.useRealTimers();
			// Restore original mock so other tests aren't affected
			license.isMultiMainLicensed = (() => true) as unknown as typeof license.isMultiMainLicensed;
		});

		it('should retry and succeed when follower finds license cert on retry', async () => {
			setupInstanceSettings('main', true, false);
			// @ts-expect-error - Accessing protected property for testing
			start.globalConfig = multiMainConfig;

			// First call returns false (no cert yet), second call returns true (leader wrote cert)
			license.isMultiMainLicensed = jest
				.fn()
				.mockReturnValueOnce(false)
				.mockReturnValue(true) as unknown as typeof license.isMultiMainLicensed;

			const initPromise = start.init();

			// Advance past the first retry delay (2s)
			await jest.advanceTimersByTimeAsync(2_000);

			await initPromise;

			expect(license.reload).toHaveBeenCalledTimes(1);
		});

		it('should throw FeatureNotLicensedError when follower exhausts all retries', async () => {
			setupInstanceSettings('main', true, false);
			// @ts-expect-error - Accessing protected property for testing
			start.globalConfig = multiMainConfig;

			license.isMultiMainLicensed = jest
				.fn()
				.mockReturnValue(false) as unknown as typeof license.isMultiMainLicensed;

			const initPromise = start.init().catch((error) => {
				expect(error).toBeInstanceOf(FeatureNotLicensedError);
				return 'rejected';
			});

			// Advance past all retry delays: 2s + 4s + 8s + 16s + 32s = 62s
			await jest.advanceTimersByTimeAsync(62_000);

			const result = await initPromise;
			expect(result).toBe('rejected');
			// 5 retries = 5 reload calls
			expect(license.reload).toHaveBeenCalledTimes(5);
		});

		it('should not retry when leader fails the license check', async () => {
			setupInstanceSettings('main', true, true);
			// @ts-expect-error - Accessing protected property for testing
			start.globalConfig = multiMainConfig;

			license.isMultiMainLicensed = jest
				.fn()
				.mockReturnValue(false) as unknown as typeof license.isMultiMainLicensed;

			await expect(start.init()).rejects.toThrow(FeatureNotLicensedError);

			// Followers only retry via reload; leaders should fail immediately
			expect(license.reload).not.toHaveBeenCalled();
		});
	});
});
