import { expect } from '@playwright/test';
import type { TestInfo } from '@playwright/test';
import autocannon from 'autocannon';
import type { ServiceHelpers } from 'n8n-containers/services/types';

import {
	attachReportMetrics,
	buildAndAttachRunReport,
	renderRunReport,
	reportContainerStats,
	reportDiagnostics,
	reportJaegerTraces,
	reportPgQueryBreakdown,
	reportPgSaturation,
	setupBenchmarkRun,
} from './orchestration';
import type { ApiHelpers } from '../../../../services/api-helper';
import {
	attachThroughputResults,
	resolveMetricQuery,
	waitForThroughput,
} from '../../../../utils/benchmark';
import type { BenchmarkDimensions } from '../../../../utils/benchmark';
import type { WebhookHandle } from '../../../../utils/benchmark/webhook-driver';
import { attachMetric } from '../../../../utils/performance-helper';

const DEFAULT_PIPELINING = 3;

export interface WebhookThroughputOptions {
	handle: WebhookHandle;
	api: ApiHelpers;
	services: ServiceHelpers;
	testInfo: TestInfo;
	baseUrl: string;
	connections: number;
	durationSeconds: number;
	timeoutMs: number;
	/** PromQL metric to track workflow completions. Defaults to resolveMetricQuery(testInfo). */
	metricQuery?: string;
	/**
	 * Seconds of discarded load BEFORE the measurement window. Warms V8 JIT,
	 * PG connection pool, webhook cache, and BullMQ worker hot paths so the
	 * measured run doesn't average cold + hot together. Set to 0 to skip
	 * (keeps the registration probe only). Defaults to 10s.
	 */
	warmupSeconds?: number;
	/**
	 * autocannon pipelining (in-flight requests per connection). Defaults to 3
	 * for throughput tests. Set to 1 for latency-floor measurements where
	 * pure round-trip is the target — pipelining hides per-request latency
	 * by overlapping requests on the same socket.
	 */
	pipelining?: number;
}

/**
 * Runs a webhook throughput test using autocannon for HTTP load generation
 * and VictoriaMetrics for workflow completion tracking.
 *
 * Webhook differs from kafka harnesses: load is generated by autocannon (HTTP)
 * rather than via a TriggerHandle.publishAtRate. The shared `executeLoad`
 * dispatch isn't a fit because autocannon owns its own load model.
 */
export async function runWebhookThroughputTest(options: WebhookThroughputOptions): Promise<void> {
	const {
		handle,
		api,
		services,
		testInfo,
		baseUrl,
		connections,
		durationSeconds,
		timeoutMs,
		warmupSeconds = 10,
		pipelining = DEFAULT_PIPELINING,
	} = options;
	const { nodeCount, nodeOutputSize } = handle.scenario;
	const metricQuery = options.metricQuery ?? resolveMetricQuery(testInfo);
	testInfo.setTimeout(timeoutMs + 120_000);

	const dimensions: BenchmarkDimensions = {
		trigger: 'webhook',
		nodes: nodeCount,
		connections,
		duration_s: durationSeconds,
	};
	if (nodeOutputSize !== undefined) dimensions.output = nodeOutputSize;

	// Warm-up runs inside setupBenchmarkRun so the baseline counter is captured
	// AFTER the probe POST — otherwise warm-up traffic would offset the measurement.
	const setup = await setupBenchmarkRun({
		api,
		services,
		testInfo,
		handle,
		metricQuery,
		createOptions: { webhookPrefix: 'bench' },
		warmUp: async ({ webhookPath }) => {
			if (!webhookPath) throw new Error('Webhook path missing — workflow has no webhook node?');

			// Registration probe: webhook activation is async, this confirms the
			// route is live before we hammer it with autocannon.
			await api.webhooks.trigger(`/webhook/${webhookPath}`, {
				method: 'POST',
				data: handle.payload,
				maxNotFoundRetries: 10,
				notFoundRetryDelayMs: 500,
			});
			console.log(`[WEBHOOK] Registered at /webhook/${webhookPath}`);

			if (warmupSeconds > 0) {
				console.log(
					`[WEBHOOK] Warm-up: ${connections} conns × ${pipelining} pipelining × ${warmupSeconds}s (discarded)`,
				);
				await autocannon({
					url: `${baseUrl}/webhook/${webhookPath}`,
					connections,
					pipelining,
					duration: warmupSeconds,
					method: 'POST',
					body: JSON.stringify(handle.payload),
					headers: { 'Content-Type': 'application/json' },
				});
			}
		},
	});

	const webhookUrl = `${baseUrl}/webhook/${setup.webhookPath}`;
	console.log(
		`[WEBHOOK] Starting ${connections} connections × ${pipelining} pipelining for ${durationSeconds}s → ${webhookUrl}\n` +
			`  Workflow: ${nodeCount} nodes (${nodeOutputSize})`,
	);

	const [cannonResult, throughputResult] = await Promise.all([
		autocannon({
			url: webhookUrl,
			connections,
			// Pipelining N puts N in-flight requests per connection — multiplies effective
			// concurrency (Little's Law: throughput = in_flight / latency) without
			// multiplying sockets. Hides whether server saturation is real or just
			// connection-starved on the load-gen side.
			pipelining,
			duration: durationSeconds,
			method: 'POST',
			body: JSON.stringify(handle.payload),
			headers: { 'Content-Type': 'application/json' },
		}),
		waitForThroughput(services.observability.metrics, {
			expectedCount: Infinity,
			nodeCount,
			timeoutMs: (durationSeconds + 30) * 1000,
			baselineValue: setup.baselineCounter,
			metricQuery,
		}),
	]);

	// Backlog growth rate: ingestion exceeding processing accumulates queue depth.
	// For async webhooks at saturation, this is THE key indicator of sustainability.
	const httpReqPerSec = cannonResult.requests.average;
	const n8nExecPerSec = throughputResult.tailExecPerSec || throughputResult.avgExecPerSec;
	const backlogGrowthPerSec = httpReqPerSec - n8nExecPerSec;
	const ingestionVsExecutionRatio = n8nExecPerSec > 0 ? httpReqPerSec / n8nExecPerSec : 0;
	const totalErrors = cannonResult.errors + cannonResult.non2xx;
	// Denominator must be `sent` (initiated requests), not `total` (completed
	// responses): a fully failing run completes zero responses and would
	// otherwise report 0% error rate.
	const requestsSent = cannonResult.requests.sent;
	const errorRatePct = requestsSent > 0 ? (totalErrors / requestsSent) * 100 : 0;

	await attachThroughputResults(testInfo, dimensions, throughputResult);
	await attachMetric(testInfo, 'http-latency-p50', cannonResult.latency.p50, 'ms', dimensions);
	// autocannon doesn't expose p95; p97.5 is the closest standard percentile
	// and errs on the conservative side for SLO budgets.
	await attachMetric(testInfo, 'http-latency-p97_5', cannonResult.latency.p97_5, 'ms', dimensions);
	await attachMetric(testInfo, 'http-latency-p99', cannonResult.latency.p99, 'ms', dimensions);
	await attachMetric(testInfo, 'http-requests-sent', requestsSent, 'count', dimensions);
	await attachMetric(testInfo, 'http-requests-avg', httpReqPerSec, 'req/s', dimensions);
	await attachMetric(testInfo, 'http-errors', totalErrors, 'count', dimensions);
	await attachMetric(testInfo, 'http-error-rate-pct', errorRatePct, '%', dimensions);
	await attachMetric(testInfo, 'backlog-growth-per-sec', backlogGrowthPerSec, 'msg/s', dimensions);
	await attachMetric(
		testInfo,
		'ingestion-vs-execution-ratio',
		ingestionVsExecutionRatio,
		'ratio',
		dimensions,
	);

	const diagnostics = await reportDiagnostics({
		testInfo,
		services,
		durationMs: throughputResult.durationMs,
		dimensions,
	});
	const { containers, source: containersSource } = await reportContainerStats(
		diagnostics,
		setup.dockerStatsSampler,
	);
	const pgQueries = await reportPgQueryBreakdown({
		services,
		durationMs: throughputResult.durationMs,
	});
	const pgSaturation = await reportPgSaturation({
		services,
		durationMs: throughputResult.durationMs,
	});
	await reportJaegerTraces({ testInfo, services, since: setup.activationStart });

	const verdict =
		errorRatePct > 1
			? `OVERLOADED — ${errorRatePct.toFixed(2)}% error rate`
			: backlogGrowthPerSec > httpReqPerSec * 0.05
				? `INGESTION > EXECUTION — backlog growing at ${backlogGrowthPerSec.toFixed(1)}/sec`
				: 'BALANCED — ingestion and execution keeping pace';

	// totalMs is the active measurement window (publish+drain); wallClockMs
	// includes activation/warm-up so cross-run comparisons can spot a slow
	// startup that wouldn't show in totalMs.
	const wallClockMs = Date.now() - setup.activationStart;
	const report = await buildAndAttachRunReport({
		testInfo,
		scenario: { spec: testInfo.title, dimensions },
		duration: { totalMs: throughputResult.durationMs, wallClockMs },
		throughput: {
			reqPerSec: httpReqPerSec,
			execPerSec: throughputResult.avgExecPerSec,
			tailExecPerSec: throughputResult.tailExecPerSec,
			p50Ms: cannonResult.latency.p50,
			p97_5Ms: cannonResult.latency.p97_5,
			p99Ms: cannonResult.latency.p99,
			totalRequests: requestsSent,
			totalCompleted: throughputResult.totalCompleted,
			errors: totalErrors,
			errorBreakdown: { transportErrors: cannonResult.errors, non2xx: cannonResult.non2xx },
			errorRatePct,
			backlogGrowthPerSec,
			ingestionVsExecutionRatio,
			verdict,
		},
		containers,
		containersSource,
		diagnostics,
		pgQueries,
		pgSaturation,
		walBaseline: setup.walBaseline,
	});
	await attachReportMetrics(testInfo, report, dimensions);
	renderRunReport(report);

	expect(throughputResult.totalCompleted).toBeGreaterThan(0);
}
