import { Container } from '@n8n/di';
import fs from 'fs';
import { mock } from 'jest-mock-extended';
import { tmpdir } from 'node:os';
import path from 'node:path';

import type { UserManagementConfig } from '../src/configs/user-management.config';
import type { DatabaseConfig } from '../src/index';
import { GlobalConfig } from '../src/index';

jest.mock('fs');
const mockFs = mock<typeof fs>();
fs.readFileSync = mockFs.readFileSync;

const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});

describe('GlobalConfig', () => {
	beforeEach(() => {
		Container.reset();
		jest.clearAllMocks();
	});

	const originalEnv = process.env;
	afterEach(() => {
		process.env = originalEnv;
	});

	const defaultConfig: GlobalConfig = {
		path: '/',
		host: 'localhost',
		port: 5678,
		listen_address: '::',
		protocol: 'http',
		auth: {
			cookie: {
				samesite: 'lax',
				secure: true,
			},
		},
		defaultLocale: 'en',
		hideUsagePage: false,
		deployment: {
			type: 'default',
		},
		mfa: {
			enabled: true,
		},
		hiringBanner: {
			enabled: true,
		},
		personalization: {
			enabled: true,
		},
		proxy_hops: 0,
		ssl_key: '',
		ssl_cert: '',
		editorBaseUrl: '',
		dataTable: {
			maxSize: 50 * 1024 * 1024,
			sizeCheckCacheDuration: 5 * 1000,
			cleanupIntervalMs: 60 * 1000,
			fileMaxAgeMs: 2 * 60 * 1000,
			uploadDir: path.join(tmpdir(), 'n8nDataTableUploads'),
		},
		database: {
			logging: {
				enabled: false,
				maxQueryExecutionTime: 0,
				options: 'error',
			},
			mysqldb: {
				database: 'n8n',
				host: 'localhost',
				password: '',
				port: 3306,
				user: 'root',
				poolSize: 10,
			},
			postgresdb: {
				database: 'n8n',
				host: 'localhost',
				password: '',
				poolSize: 2,
				port: 5432,
				schema: 'public',
				connectionTimeoutMs: 20_000,
				ssl: {
					ca: '',
					cert: '',
					enabled: false,
					key: '',
					rejectUnauthorized: true,
				},
				user: 'postgres',
				idleTimeoutMs: 30_000,
			},
			sqlite: {
				database: 'database.sqlite',
				executeVacuumOnStartup: false,
				poolSize: 3,
			},
			tablePrefix: '',
			type: 'sqlite',
			pingIntervalSeconds: 2,
		} as DatabaseConfig,
		credentials: {
			defaultName: 'My credentials',
			overwrite: {
				data: '{}',
				endpoint: '',
				endpointAuthToken: '',
				persistence: false,
			},
		},
		userManagement: {
			inviteLinksEmailOnly: false,
			jwtSecret: '',
			jwtSessionDurationHours: 168,
			jwtRefreshTimeoutHours: 0,
			emails: {
				mode: 'smtp',
				smtp: {
					host: '',
					port: 465,
					secure: true,
					sender: '',
					startTLS: true,
					auth: {
						pass: '',
						user: '',
						privateKey: '',
						serviceClient: '',
					},
				},
				template: {
					'credentials-shared': '',
					'user-invited': '',
					'password-reset-requested': '',
					'workflow-deactivated': '',
					'workflow-failure': '',
					'workflow-shared': '',
					'project-shared': '',
				},
			},
		} as UserManagementConfig,
		eventBus: {
			checkUnsentInterval: 0,
			crashRecoveryMode: 'extensive',
			logWriter: {
				keepLogCount: 3,
				logBaseName: 'n8nEventLog',
				maxFileSizeInKB: 10240,
			},
		},
		externalHooks: {
			files: [],
		},
		nodes: {
			errorTriggerType: 'n8n-nodes-base.errorTrigger',
			include: [],
			exclude: ['n8n-nodes-base.executeCommand', 'n8n-nodes-base.localFileTrigger'],
			pythonEnabled: true,
		},
		publicApi: {
			disabled: false,
			path: 'api',
			swaggerUiDisabled: false,
		},
		templates: {
			enabled: true,
			host: 'https://api.n8n.io/api/',
		},
		versionNotifications: {
			enabled: true,
			endpoint: 'https://api.n8n.io/api/versions/',
			whatsNewEnabled: true,
			whatsNewEndpoint: 'https://api.n8n.io/api/whats-new',
			infoUrl: 'https://docs.n8n.io/hosting/installation/updating/',
		},
		dynamicBanners: {
			endpoint: 'https://api.n8n.io/api/banners',
			enabled: true,
		},
		workflows: {
			defaultName: 'My workflow',
			callerPolicyDefaultOption: 'workflowsFromSameOwner',
			activationBatchSize: 1,
			indexingEnabled: false,
		},
		endpoints: {
			metrics: {
				enable: false,
				prefix: 'n8n_',
				includeWorkflowIdLabel: false,
				includeWorkflowNameLabel: false,
				includeDefaultMetrics: true,
				includeMessageEventBusMetrics: false,
				includeNodeTypeLabel: false,
				includeCacheMetrics: false,
				includeApiEndpoints: false,
				includeApiPathLabel: false,
				includeApiMethodLabel: false,
				includeCredentialTypeLabel: false,
				includeApiStatusCodeLabel: false,
				includeQueueMetrics: false,
				queueMetricsInterval: 20,
				activeWorkflowCountInterval: 60,
				includeWorkflowStatistics: false,
				workflowStatisticsInterval: 300,
			},
			additionalNonUIRoutes: '',
			disableProductionWebhooksOnMainProcess: false,
			disableUi: false,
			form: 'form',
			formTest: 'form-test',
			formWaiting: 'form-waiting',
			mcp: 'mcp',
			mcpTest: 'mcp-test',
			payloadSizeMax: 16,
			formDataFileSizeMax: 200,
			rest: 'rest',
			webhook: 'webhook',
			webhookTest: 'webhook-test',
			webhookWaiting: 'webhook-waiting',
		},
		cache: {
			backend: 'auto',
			memory: {
				maxSize: 3145728,
				ttl: 3600000,
			},
			redis: {
				prefix: 'cache',
				ttl: 3600000,
			},
		},
		queue: {
			health: {
				active: false,
				port: 5678,
				address: '::',
			},
			bull: {
				redis: {
					db: 0,
					host: 'localhost',
					password: '',
					port: 6379,
					timeoutThreshold: 10_000,
					username: '',
					clusterNodes: '',
					tls: false,
					dualStack: false,
					slotsRefreshInterval: 5_000,
					slotsRefreshTimeout: 1_000,
					dnsResolveStrategy: 'LOOKUP',
				},
				gracefulShutdownTimeout: 30,
				prefix: 'bull',
				settings: {
					lockDuration: 60_000,
					lockRenewTime: 10_000,
					stalledInterval: 30_000,
				},
			},
		},
		taskRunners: {
			enabled: true,
			mode: 'internal',
			path: '/runners',
			authToken: '',
			listenAddress: '127.0.0.1',
			maxPayload: 1024 * 1024 * 1024,
			port: 5679,
			maxOldSpaceSize: '',
			maxConcurrency: 10,
			taskTimeout: 300,
			taskRequestTimeout: 60,
			heartbeatInterval: 30,
			insecureMode: false,
		},
		sentry: {
			backendDsn: '',
			frontendDsn: '',
			environment: '',
			deploymentName: '',
		},
		logging: {
			level: 'info',
			format: 'text',
			outputs: ['console'],
			file: {
				fileCountMax: 100,
				fileSizeMax: 16,
				location: 'logs/n8n.log',
			},
			scopes: [],
			cron: {
				activeInterval: 0,
			},
		},
		multiMainSetup: {
			enabled: false,
			ttl: 10,
			interval: 3,
		},
		generic: {
			timezone: 'America/New_York',
			releaseChannel: 'dev',
			gracefulShutdownTimeout: 30,
		},
		license: {
			serverUrl: 'https://license.n8n.io/v1',
			autoRenewalEnabled: true,
			detachFloatingOnShutdown: true,
			activationKey: '',
			tenantId: 1,
			cert: '',
		},
		security: {
			restrictFileAccessTo: '~/.n8n-files',
			blockFileAccessToN8nFiles: true,
			blockFilePatterns: '^(.*\\/)*\\.git(\\/.*)*$',
			daysAbandonedWorkflow: 90,
			contentSecurityPolicy: '{}',
			contentSecurityPolicyReportOnly: false,
			disableWebhookHtmlSandboxing: false,
			disableBareRepos: true,
			awsSystemCredentialsAccess: false,
			enableGitNodeHooks: false,
			enableGitNodeAllConfigKeys: false,
		},
		executions: {
			mode: 'regular',
			timeout: -1,
			maxTimeout: 3600,
			pruneData: true,
			pruneDataMaxAge: 336,
			pruneDataMaxCount: 10_000,
			pruneDataHardDeleteBuffer: 1,
			pruneDataIntervals: {
				hardDelete: 15,
				softDelete: 60,
			},
			concurrency: {
				productionLimit: -1,
				evaluationLimit: -1,
			},
			queueRecovery: {
				interval: 180,
				batchSize: 100,
			},
			recovery: {
				maxLastExecutions: 3,
				workflowDeactivationEnabled: false,
			},
			saveDataOnError: 'all',
			saveDataOnSuccess: 'all',
			saveExecutionProgress: false,
			saveDataManualExecutions: true,
		},
		diagnostics: {
			enabled: true,
			frontendConfig: '1zPn9bgWPzlQc0p8Gj1uiK6DOTn;https://telemetry.n8n.io',
			backendConfig: '1zPn7YoGC3ZXE9zLeTKLuQCB4F6;https://telemetry.n8n.io',
			posthogConfig: {
				apiKey: 'phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo',
				apiHost: 'https://us.i.posthog.com',
			},
		},
		aiAssistant: {
			baseUrl: '',
		},
		aiBuilder: {
			apiKey: '',
		},
		tags: {
			disabled: false,
		},
		workflowHistory: {
			pruneTime: -1,
		},
		sso: {
			justInTimeProvisioning: true,
			redirectLoginToSso: true,
			saml: {
				loginEnabled: false,
				loginLabel: '',
			},
			oidc: {
				loginEnabled: false,
			},
			ldap: {
				loginEnabled: false,
				loginLabel: '',
			},
			provisioning: {
				scopesProvisionInstanceRole: false,
				scopesProvisionProjectRoles: false,
				scopesName: 'n8n',
				scopesInstanceRoleClaimName: 'n8n_instance_role',
				scopesProjectsRolesClaimName: 'n8n_projects',
			},
		},
		redis: {
			prefix: 'n8n',
		},
		externalFrontendHooksUrls: '',
		// @ts-expect-error structuredClone ignores properties defined as a getter
		ai: {
			enabled: false,
			timeout: 3600000,
		},
		workflowHistoryCompaction: {
			batchDelayMs: 1_000,
			batchSize: 100,
			compactingMinimumAgeHours: 3,
			compactingTimeWindowHours: 2,
			compactOnStartUp: false,
			minimumTimeBetweenSessionsMs: 20 * 60 * 1000,
		},
	};

	it('should use all default values when no env variables are defined', () => {
		process.env = {};
		const config = Container.get(GlobalConfig);
		// Makes sure the objects are structurally equal while respecting getters,
		// which `toEqual` and `toBe` does not do.
		expect(defaultConfig).toMatchObject(config);
		expect(config).toMatchObject(defaultConfig);
		expect(mockFs.readFileSync).not.toHaveBeenCalled();
	});

	it('should use values from env variables when defined', () => {
		process.env = {
			DB_POSTGRESDB_HOST: 'some-host',
			DB_POSTGRESDB_USER: 'n8n',
			DB_POSTGRESDB_IDLE_CONNECTION_TIMEOUT: '10000',
			DB_TABLE_PREFIX: 'test_',
			DB_PING_INTERVAL_SECONDS: '2',
			NODES_INCLUDE: '["n8n-nodes-base.hackerNews"]',
			DB_LOGGING_MAX_EXECUTION_TIME: '0',
			N8N_METRICS: 'TRUE',
			N8N_TEMPLATES_ENABLED: '0',
			N8N_DYNAMIC_BANNERS_ENDPOINT: 'https://localhost:5678/api/banners',
			N8N_DYNAMIC_BANNERS_ENABLED: 'false',
		};
		const config = Container.get(GlobalConfig);

		expect(structuredClone(config)).toEqual({
			...defaultConfig,
			database: {
				logging: defaultConfig.database.logging,
				mysqldb: defaultConfig.database.mysqldb,
				postgresdb: {
					...defaultConfig.database.postgresdb,
					host: 'some-host',
					user: 'n8n',
					idleTimeoutMs: 10_000,
				},
				sqlite: defaultConfig.database.sqlite,
				tablePrefix: 'test_',
				type: 'sqlite',
				pingIntervalSeconds: 2,
			},
			endpoints: {
				...defaultConfig.endpoints,
				metrics: {
					...defaultConfig.endpoints.metrics,
					enable: true,
				},
			},
			nodes: {
				...defaultConfig.nodes,
				include: ['n8n-nodes-base.hackerNews'],
			},
			templates: {
				...defaultConfig.templates,
				enabled: false,
			},
			dynamicBanners: {
				endpoint: 'https://localhost:5678/api/banners',
				enabled: false,
			},
		});
		expect(mockFs.readFileSync).not.toHaveBeenCalled();
	});

	it('should read values from files using _FILE env variables', () => {
		const passwordFile = '/path/to/postgres/password';
		process.env = {
			DB_POSTGRESDB_PASSWORD_FILE: passwordFile,
		};
		mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file');

		const config = Container.get(GlobalConfig);
		const expected = {
			...defaultConfig,
			database: {
				...defaultConfig.database,
				postgresdb: {
					...defaultConfig.database.postgresdb,
					password: 'password-from-file',
				},
			},
		};
		// Makes sure the objects are structurally equal while respecting getters,
		// which `toEqual` and `toBe` does not do.
		expect(config).toMatchObject(expected);
		expect(expected).toMatchObject(config);
		expect(mockFs.readFileSync).toHaveBeenCalled();
	});

	it('should handle invalid numbers', () => {
		process.env = {
			DB_LOGGING_MAX_EXECUTION_TIME: 'abcd',
		};
		const config = Container.get(GlobalConfig);
		expect(config.database.logging.maxQueryExecutionTime).toEqual(0);
		expect(consoleWarnMock).toHaveBeenCalledWith(
			'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd',
		);
	});

	describe('string unions', () => {
		it('on invalid value, should warn and fall back to default value', () => {
			process.env = {
				N8N_RUNNERS_MODE: 'non-existing-mode',
				DB_TYPE: 'postgresdb',
			};

			const globalConfig = Container.get(GlobalConfig);
			expect(globalConfig.taskRunners.mode).toEqual('internal');
			expect(consoleWarnMock).toHaveBeenCalledWith(
				expect.stringContaining(
					"Invalid value for N8N_RUNNERS_MODE - Invalid enum value. Expected 'internal' | 'external', received 'non-existing-mode'. Falling back to default value.",
				),
			);

			expect(globalConfig.taskRunners.enabled).toEqual(true);
			expect(globalConfig.database.type).toEqual('postgresdb');
		});
	});
});
