import { describe, expect, it, vi } from 'vitest';
import { ClientResponseError } from './client';
import { createRequestCache, httpStatusFor, parallel, toHttpError } from './load';

const cre = (status: number, error: string, message?: string): ClientResponseError =>
	new ClientResponseError({ status, data: { error, message } });

describe('httpStatusFor', () => {
	it('passes through an in-band XRPC status', () => {
		expect(httpStatusFor(cre(404, 'RecordNotFound'))).toBe(404);
	});

	it('prefers an in-band status over the error-name mapping', () => {
		// status 503 is in [400,599] so it wins even though the name maps to 404.
		expect(httpStatusFor(cre(503, 'RecordNotFound'))).toBe(503);
	});

	it('maps the error name when the status is out of the 400-599 band', () => {
		expect(httpStatusFor(cre(200, 'RecordNotFound'))).toBe(404);
		expect(httpStatusFor(cre(200, 'InvalidRequest'))).toBe(400);
		expect(httpStatusFor(cre(200, 'UpstreamFailed'))).toBe(502);
		expect(httpStatusFor(cre(200, 'UpstreamGone'))).toBe(502);
		expect(httpStatusFor(cre(200, 'InvalidRecord'))).toBe(502);
		expect(httpStatusFor(cre(200, 'Overloaded'))).toBe(503);
		expect(httpStatusFor(cre(200, 'SomethingElse'))).toBe(500);
	});

	it('maps any non-XRPC cause to 500', () => {
		expect(httpStatusFor(new Error('boom'))).toBe(500);
		expect(httpStatusFor('a string')).toBe(500);
		expect(httpStatusFor(undefined)).toBe(500);
		expect(httpStatusFor({ status: 404 })).toBe(500);
	});
});

describe('toHttpError', () => {
	it('throws a SvelteKit error with the mapped status and the XRPC description', () => {
		let thrown: unknown;
		try {
			toHttpError(cre(404, 'RecordNotFound', 'no such repo'));
		} catch (e) {
			thrown = e;
		}
		expect(thrown).toMatchObject({ status: 404, body: { message: 'no such repo' } });
	});

	it('falls back to the error name when the description is absent', () => {
		let thrown: unknown;
		try {
			toHttpError(cre(200, 'Overloaded'));
		} catch (e) {
			thrown = e;
		}
		expect(thrown).toMatchObject({ status: 503, body: { message: 'Overloaded' } });
	});

	it('uses the fallback message + 500 for a non-XRPC cause', () => {
		let thrown: unknown;
		try {
			toHttpError(new Error('boom'), 'could not load');
		} catch (e) {
			thrown = e;
		}
		expect(thrown).toMatchObject({ status: 500, body: { message: 'could not load' } });
	});
});

describe('parallel', () => {
	it('preserves key/value pairing regardless of settle order', async () => {
		// deferred across a few microtasks (no real timer) so it settles after `fast`.
		const slow = Promise.resolve()
			.then(() => Promise.resolve())
			.then(() => 'slow');
		const result = await parallel({ fast: Promise.resolve('fast'), slow });
		expect(result).toEqual({ fast: 'fast', slow: 'slow' });
	});

	it('resolves to an empty object for no tasks', async () => {
		await expect(parallel({})).resolves.toEqual({});
	});

	it('rejects if any input promise rejects', async () => {
		await expect(
			parallel({ a: Promise.resolve(1), b: Promise.reject(new Error('boom')) })
		).rejects.toThrow('boom');
	});
});

describe('createRequestCache', () => {
	it('invokes the loader once per key and shares the value', async () => {
		const cache = createRequestCache();
		const load = vi.fn(async () => 42);

		const first = await cache.run('k', load);
		const second = await cache.run('k', load);

		expect(first).toBe(42);
		expect(second).toBe(42);
		expect(load).toHaveBeenCalledTimes(1);
	});

	it('shares one in-flight promise for concurrent same-key calls', () => {
		const cache = createRequestCache();
		const load = vi.fn(async () => 'v');

		const a = cache.run('k', load);
		const b = cache.run('k', load);

		expect(a).toBe(b);
		expect(load).toHaveBeenCalledTimes(1);
	});

	it('runs distinct keys independently', async () => {
		const cache = createRequestCache();
		const load = vi.fn(async (key: string) => key.toUpperCase());

		await cache.run('a', () => load('a'));
		await cache.run('b', () => load('b'));

		expect(load).toHaveBeenCalledTimes(2);
	});
});
