import { building, dev } from '$app/environment';
import { error, isHttpError, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import fs from 'node:fs';
import { COOKIE_NAME } from './routes/cookies/shared';
import { _set_from_init } from './routes/init-hooks/+page.server';
import { getRequestEvent } from '$app/server';
import { resolve } from '$app/paths';

// TODO: remove in SvelteKit 3.0
// @ts-ignore this doesn't exist in old Node
Promise.withResolvers ??= () => {
	/** @type {{ promise: Promise<any>, resolve: (value: any) => void, reject: (reason?: any) => void }} */
	const d = {};
	d.promise = new Promise((resolve, reject) => {
		d.resolve = resolve;
		d.reject = reject;
	});
	return d;
};

// check that this doesn't throw when called outside an event context
resolve('/');

/**
 * Transform an error into a POJO, by copying its `name`, `message`
 * and (in dev) `stack`, plus any custom properties, plus recursively
 * serialized `cause` properties.
 *
 * @param {Error} error
 */
export function error_to_pojo(error) {
	if (isHttpError(error)) {
		return {
			status: error.status,
			...error.body
		};
	}

	const { name, message, stack, ...custom } = error;
	return { name, message, stack, ...custom };
}

/** @type {import('@sveltejs/kit').HandleServerError} */
export const handleError = ({ event, error: e, status, message }) => {
	const error = /** @type {Error} */ (e);
	// TODO we do this because there's no other way (that i'm aware of)
	// to communicate errors back to the test suite. even if we could
	// capture stderr, attributing an error to a specific request
	// is trickier when things run concurrently
	const errors = fs.existsSync('test/errors.json')
		? JSON.parse(fs.readFileSync('test/errors.json', 'utf8'))
		: {};
	errors[event.url.pathname] = error_to_pojo(error);
	fs.writeFileSync('test/errors.json', JSON.stringify(errors));

	if (event.url.pathname.startsWith('/get-request-event/')) {
		const ev = getRequestEvent();
		message = /** @type {string} */ (ev.locals.message);
	}

	return event.url.pathname.endsWith('404-fallback')
		? undefined
		: { message: `${error.message} (${status} ${message})` };
};

export const handle = sequence(
	// eslint-disable-next-line prefer-arrow-callback -- this needs a name for tests
	function set_tracing_test_id({ event, resolve }) {
		const test_id = !building && event.url.searchParams.get('test_id');
		if (test_id) {
			event.tracing.root.setAttribute('test_id', test_id);
		}
		return resolve(event);
	},
	({ event, resolve }) => {
		event.locals.key = event.route.id;
		event.locals.params = event.params;
		event.locals.answer = 42;
		return resolve(event);
	},
	({ event, resolve }) => {
		if (
			event.request.url.includes('__data.json') &&
			(event.url.pathname.endsWith('__data.json') || !event.isDataRequest)
		) {
			throw new Error(
				'__data.json requests should have the suffix stripped from the URL and isDataRequest set to true'
			);
		}
		return resolve(event);
	},
	({ event, resolve }) => {
		if (
			event.request.headers.has('host') &&
			!event.request.headers.has('user-agent') !== event.isSubRequest
		) {
			throw new Error('SSR API sub-requests should have isSubRequest set to true');
		}
		return resolve(event);
	},
	({ event, resolve }) => {
		if (event.url.pathname.includes('fetch-credentialed')) {
			// Only get the cookie at the test where we know it's set to avoid polluting our logs with (correct) warnings
			event.locals.name = /** @type {string} */ (event.cookies.get('name'));
		}
		return resolve(event);
	},
	async ({ event, resolve }) => {
		if (event.url.pathname === '/cookies/serialize') {
			event.cookies.set('before', 'before', { path: '' });
			const response = await resolve(event);
			response.headers.append(
				'set-cookie',
				event.cookies.serialize('after', 'after', { path: '' })
			);
			return response;
		}
		return resolve(event);
	},
	async ({ event, resolve }) => {
		if (event.url.pathname === '/errors/error-in-handle') {
			throw new Error('Error in handle');
		} else if (event.url.pathname === '/errors/expected-error-in-handle') {
			error(500, 'Expected error in handle');
		}

		const response = await resolve(event, {
			transformPageChunk: event.url.pathname.startsWith('/transform-page-chunk')
				? ({ html }) => html.replace('__REPLACEME__', 'Worked!')
				: undefined
		});

		try {
			// in some tests we fetch stuff with undici, and the headers are immutable.
			// we can safely ignore it in those cases
			response.headers.append('set-cookie', 'name=SvelteKit; path=/; HttpOnly');
		} catch {}

		return response;
	},
	async ({ event, resolve }) => {
		if (event.url.pathname.includes('/redirect/in-handle')) {
			const location = event.url.searchParams.get('location');
			if (location) {
				redirect(307, location);
			}

			if (event.url.search === '?throw') {
				redirect(307, event.url.origin + '/redirect/c');
			} else if (event.url.search.includes('cookies')) {
				event.cookies.delete(COOKIE_NAME, { path: '/cookies' });
				redirect(307, event.url.origin + '/cookies');
			} else {
				return new Response(undefined, { status: 307, headers: { location: '/redirect/c' } });
			}
		}

		return resolve(event);
	},
	async ({ event, resolve }) => {
		if (event.url.pathname === '/prerendering/prerendered-endpoint/from-handle-hook') {
			return event.fetch('/prerendering/prerendered-endpoint/api');
		}

		return resolve(event);
	},
	async ({ event, resolve }) => {
		if (event.url.pathname === '/actions/redirect-in-handle' && event.request.method === 'POST') {
			redirect(303, '/actions/enhance');
		}

		return resolve(event);
	},
	async ({ event, resolve }) => {
		if (!dev && !building && event.url.pathname === '/prerendering/prerendered-endpoint/api') {
			error(
				500,
				`Server hooks should not be called for prerendered endpoints: isSubRequest=${event.isSubRequest}`
			);
		}
		return resolve(event);
	},
	async ({ event, resolve }) => {
		if (['/non-existent-route', '/non-existent-route-loop'].includes(event.url.pathname)) {
			event.locals.url = new URL(event.request.url);
		}
		return resolve(event);
	},
	async ({ event, resolve }) => {
		if (event.url.pathname.startsWith('/get-request-event/')) {
			const e = getRequestEvent();

			if (event !== e) {
				throw new Error('event !== e');
			}

			e.locals.message = 'hello from hooks.server.js';
		}

		return resolve(event, {
			// needed for asset-preload tests
			preload: () => true
		});
	}
);

/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ request, fetch }) {
	if (request.url.endsWith('/server-fetch-request.json')) {
		request = new Request(
			request.url.replace('/server-fetch-request.json', '/server-fetch-request-modified.json'),
			request
		);
	}

	return fetch(request);
}

export function init() {
	_set_from_init();
}
