import type { CurrentsFixtures, CurrentsWorkerFixtures } from '@currents/playwright';
import { fixtures as currentsFixtures } from '@currents/playwright';
import { test as base, expect, request } from '@playwright/test';
import type { ServiceHelpers } from 'n8n-containers/services/types';
import type { N8NConfig, N8NStack } from 'n8n-containers/stack';
import { createN8NStack } from 'n8n-containers/stack';

import { CAPABILITIES, type Capability } from './capabilities';
import { consoleErrorFixtures } from './console-error-monitor';
import { N8N_AUTH_COOKIE } from '../config/constants';
import { setupDefaultInterceptors } from '../config/intercepts';
import { observabilityFixtures, type ObservabilityTestFixtures } from '../fixtures/observability';
import {
	quarantineFixtures,
	type QuarantineTestFixtures,
	type QuarantineWorkerFixtures,
} from '../fixtures/quarantine';
import { n8nPage } from '../pages/n8nPage';
import { ApiHelpers } from '../services/api-helper';
import { TestError, type TestRequirements } from '../Types';
import { setupTestRequirements } from '../utils/requirements';
import { getBackendUrl, getFrontendUrl } from '../utils/url-helper';

type TestFixtures = {
	n8n: n8nPage;
	api: ApiHelpers;
	baseURL: string;
	setupRequirements: (requirements: TestRequirements) => Promise<void>;
	/** Type-safe service helpers (mailpit, gitea, proxy, observability, etc.) */
	services: ServiceHelpers;
	/**
	 * Direct URLs to each main instance (bypasses load balancer).
	 * Only available in container mode with multi-main setup.
	 * Index 0 = main-1, Index 1 = main-2, etc.
	 */
	mainUrls: string[];
	/**
	 * Create an API helper for a specific main instance (bypasses load balancer).
	 * Useful for multi-main testing scenarios.
	 * @param mainIndex - 0-based index of the main (0 = main-1, 1 = main-2, etc.)
	 */
	createApiForMain: (mainIndex: number) => Promise<ApiHelpers>;
};

type WorkerFixtures = {
	n8nUrl: string;
	backendUrl: string;
	frontendUrl: string;
	dbSetup: undefined;
	n8nStackConfig: N8NConfig;
	n8nContainer: N8NStack;
	capability?: CapabilityOption;
};

type CapabilityOption = Capability | N8NConfig;
type ProjectUse = { containerConfig?: N8NConfig };

function parseGlobalTestEnv(): Record<string, string> {
	const raw = process.env.N8N_TEST_ENV;
	if (!raw) return {};
	try {
		return JSON.parse(raw) as Record<string, string>;
	} catch {
		console.warn('[base.ts] Failed to parse N8N_TEST_ENV');
		return {};
	}
}

function logKeepalive(container: N8NStack): void {
	console.log('\n=== KEEPALIVE: Containers left running for debugging ===');
	console.log(`    URL: ${container.baseUrl}`);
	console.log(`    Project: ${container.projectName}`);
	console.log('    Cleanup: pnpm --filter n8n-containers stack:clean:all');
	console.log('=========================================================\n');
}

export const test = base.extend<
	TestFixtures & CurrentsFixtures & ObservabilityTestFixtures & QuarantineTestFixtures,
	WorkerFixtures & CurrentsWorkerFixtures & QuarantineWorkerFixtures
>({
	...currentsFixtures.baseFixtures,
	...currentsFixtures.coverageFixtures,
	...currentsFixtures.actionFixtures,
	...observabilityFixtures,
	...consoleErrorFixtures,
	...quarantineFixtures,

	// Option for test.use({ capability: 'proxy' }) - transformed into N8NStack by n8nContainer
	capability: [undefined, { scope: 'worker', option: true }],

	// Resolves the effective N8NConfig from project.containerConfig (base) +
	// capability (override) + N8N_TEST_ENV (global). Topology-neutral: it
	// always produces a config, even when a container will not be provisioned.
	n8nStackConfig: [
		async ({ capability }, use, workerInfo) => {
			const { containerConfig: base = {} } = workerInfo.project.use as ProjectUse;
			const override: N8NConfig = !capability
				? {}
				: typeof capability === 'string'
					? CAPABILITIES[capability]
					: capability;

			const globalEnv = parseGlobalTestEnv();

			const config: N8NConfig = {
				...base,
				...override,
				services: [...new Set([...(base.services ?? []), ...(override.services ?? [])])],
				env: {
					...globalEnv,
					...base.env,
					...override.env,
					E2E_TESTS: 'true',
					N8N_RESTRICT_FILE_ACCESS_TO: '',
				},
			};

			await use(config);
		},
		{ scope: 'worker', box: true },
	],

	// Creates container from n8nStackConfig.
	// When N8N_BASE_URL is set, skips container creation for local testing.
	n8nContainer: [
		async ({ n8nStackConfig }, use) => {
			if (getBackendUrl()) {
				await use(null!);
				return;
			}

			const container = await createN8NStack(n8nStackConfig);
			await use(container);

			if (process.env.N8N_CONTAINERS_KEEPALIVE === 'true') {
				logKeepalive(container);
				return;
			}

			await container.stop();
		},
		{ scope: 'worker', box: true },
	],

	n8nUrl: [
		async ({ n8nContainer }, use) => {
			const envBaseURL = process.env.N8N_BASE_URL ?? n8nContainer?.baseUrl;
			await use(envBaseURL);
		},
		{ scope: 'worker' },
	],

	backendUrl: [
		async ({ n8nContainer }, use) => {
			const envBackendURL = getBackendUrl() ?? n8nContainer?.baseUrl;
			await use(envBackendURL);
		},
		{ scope: 'worker' },
	],

	frontendUrl: [
		async ({ n8nContainer }, use) => {
			const envFrontendURL = getFrontendUrl() ?? n8nContainer?.baseUrl;
			await use(envFrontendURL);
		},
		{ scope: 'worker' },
	],

	dbSetup: [
		async ({ n8nContainer }, use) => {
			if (n8nContainer) {
				console.log('Resetting database for new container');
				const apiContext = await request.newContext({ baseURL: n8nContainer.baseUrl });
				const api = new ApiHelpers(apiContext);
				await api.resetDatabase();
				await apiContext.dispose();
			}
			await use(undefined);
		},
		{ scope: 'worker' },
	],

	baseURL: async ({ frontendUrl, dbSetup }, use) => {
		void dbSetup; // Ensure dbSetup runs first
		await use(frontendUrl);
	},

	n8n: async ({ context, backendUrl, frontendUrl }, use, testInfo) => {
		await setupDefaultInterceptors(context);
		const page = await context.newPage();

		// Set debounce multiplier for E2E tests - 1 means normal timing (no change)
		// Can be lowered (e.g. 0.5) to speed up tests, but avoid 0 as it causes race conditions
		await page.addInitScript(() => {
			sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '1');
		});

		const useSeparateApiContext = backendUrl !== frontendUrl;

		if (useSeparateApiContext) {
			const apiContext = await request.newContext({ baseURL: backendUrl });
			const api = new ApiHelpers(apiContext);

			const n8nInstance = new n8nPage(page, api);
			await n8nInstance.api.setupFromTags(testInfo.tags);

			// Auth: no tag = owner, @auth:none = unauthenticated, @auth:member etc = specific role
			const hasAuthTag = testInfo.tags.some((tag) => tag.startsWith('@auth:'));
			let apiCookies = await apiContext.storageState();
			let authCookie = apiCookies.cookies.find((cookie) => cookie.name === N8N_AUTH_COOKIE);

			if (!hasAuthTag && !authCookie) {
				await api.signin('owner');
				apiCookies = await apiContext.storageState();
				authCookie = apiCookies.cookies.find((cookie) => cookie.name === N8N_AUTH_COOKIE);
			}

			// Transfer auth cookie from API context (backend) to browser context (frontend)
			if (authCookie) {
				const backendUrlParsed = new URL(backendUrl);
				const frontendUrlParsed = new URL(frontendUrl);

				if (backendUrlParsed.hostname === frontendUrlParsed.hostname) {
					await context.addCookies([
						{
							...authCookie,
							domain: frontendUrlParsed.hostname,
							path: '/',
							sameSite: 'Lax',
						},
					]);
				} else {
					await context.addCookies([
						{
							name: authCookie.name,
							value: authCookie.value,
							url: frontendUrl,
							path: '/',
							httpOnly: authCookie.httpOnly,
							secure: authCookie.secure,
							sameSite: 'Lax',
						},
					]);
				}
			}
			await n8nInstance.start.withProjectFeatures();
			await use(n8nInstance);
			await apiContext.dispose();
		} else {
			const n8nInstance = new n8nPage(page);
			await n8nInstance.api.setupFromTags(testInfo.tags);
			await n8nInstance.start.withProjectFeatures();
			await use(n8nInstance);
		}
	},

	api: async ({ backendUrl }, use, testInfo) => {
		const context = await request.newContext({ baseURL: backendUrl });
		const api = new ApiHelpers(context);
		await api.setupFromTags(testInfo.tags);

		const hasAuthTag = testInfo.tags.some((tag) => tag.startsWith('@auth:'));
		const apiCookies = await context.storageState();
		const authCookie = apiCookies.cookies.find((cookie) => cookie.name === N8N_AUTH_COOKIE);

		if (!hasAuthTag && !authCookie) {
			await api.signin('owner');
		}

		await use(api);
		await context.dispose();
	},

	mainUrls: async ({ n8nContainer }, use) => {
		const urls = n8nContainer?.mainUrls ?? [];
		await use(urls);
	},

	createApiForMain: async ({ n8nContainer }, use, testInfo) => {
		const contexts: Array<{ dispose: () => Promise<void> }> = [];

		const createApi = async (mainIndex: number): Promise<ApiHelpers> => {
			const mainUrls = n8nContainer?.mainUrls ?? [];
			if (mainIndex < 0 || mainIndex >= mainUrls.length) {
				throw new TestError(
					`Invalid main index ${mainIndex}. Available mains: ${mainUrls.length}. ` +
						'Ensure you are running in multi-main container mode.',
				);
			}

			const context = await request.newContext({ baseURL: mainUrls[mainIndex] });
			contexts.push(context);

			const api = new ApiHelpers(context);
			await api.setupFromTags(testInfo.tags.filter((tag) => tag.toLowerCase() !== '@db:reset'));

			const hasAuthTag = testInfo.tags.some((tag) => tag.startsWith('@auth:'));
			const apiCookies = await context.storageState();
			const authCookie = apiCookies.cookies.find((cookie) => cookie.name === N8N_AUTH_COOKIE);

			if (!hasAuthTag && !authCookie) {
				await api.signin('owner');
			}

			return api;
		};

		await use(createApi);

		// Cleanup all created contexts
		for (const ctx of contexts) {
			await ctx.dispose();
		}
	},

	setupRequirements: async ({ n8n, context }, use) => {
		const setupFunction = async (requirements: TestRequirements): Promise<void> => {
			await setupTestRequirements(n8n, context, requirements);
		};

		await use(setupFunction);
	},

	services: async ({ n8nContainer }, use) => {
		await use(n8nContainer.services);
	},
});

export { expect };

/*
Fixture Dependency Graph:
Worker: capability + project.containerConfig → n8nStackConfig → n8nContainer → [backendUrl, frontendUrl, dbSetup]
Test:   frontendUrl + dbSetup → baseURL → n8n (uses backendUrl for API calls)
        backendUrl → api
        n8nContainer → services

n8nStackConfig: Resolved N8NConfig (topology-neutral, always produced)
n8nContainer:   Container lifecycle (stop, containers, mainUrls, etc.)
services:       Type-safe helpers (mailpit, gitea, proxy, observability, etc.)
*/
