import { describe, it, expect, vi } from 'vitest';

process.env.API_INTERNAL_URL = 'http://backend:8080';

const { GET, PUT, POST, PATCH } = await import('./+server');

function makeEvent(
	path: string,
	method = 'GET',
	mockFetch = vi
		.fn()
		.mockResolvedValue(
			new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } })
		)
) {
	return {
		params: { path },
		request: new Request(`http://localhost/api/${path}`, { method }),
		url: new URL(`http://localhost/api/${path}`),
		fetch: mockFetch
	};
}

describe('catch-all API proxy — security guards', () => {
	it('returns 400 for path traversal with double-dot', async () => {
		const event = makeEvent('documents/../../../etc/passwd');
		const response = await GET(event as never);
		expect(response.status).toBe(400);
	});

	it('returns 400 for URL-encoded dot traversal (%2e)', async () => {
		const event = makeEvent('documents/%2e%2e/sensitive');
		const response = await GET(event as never);
		expect(response.status).toBe(400);
	});

	it('returns 400 for mixed-case encoded traversal (%2E)', async () => {
		const event = makeEvent('documents/%2E%2E/sensitive');
		const response = await GET(event as never);
		expect(response.status).toBe(400);
	});

	it('returns 404 for actuator paths', async () => {
		const event = makeEvent('actuator/heapdump');
		const response = await GET(event as never);
		expect(response.status).toBe(404);
	});

	it('returns 404 for actuator/env', async () => {
		const event = makeEvent('actuator/env');
		const response = await GET(event as never);
		expect(response.status).toBe(404);
	});
});

describe('catch-all API proxy — body size limit', () => {
	it('returns 413 when Content-Length exceeds 1 MB', async () => {
		const mockFetch = vi.fn();
		const event = makeEvent('documents', 'POST', mockFetch);
		(event.request as Request) = new Request('http://localhost/api/documents', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json', 'Content-Length': '1048577' },
			body: JSON.stringify({})
		});

		const response = await POST(event as never);

		expect(response.status).toBe(413);
		expect(mockFetch).not.toHaveBeenCalled();
	});

	it('returns 413 when actual body exceeds 1 MB even if Content-Length header is absent or lying', async () => {
		const mockFetch = vi.fn();
		const event = makeEvent('documents', 'POST', mockFetch);
		(event.request as Request) = new Request('http://localhost/api/documents', {
			method: 'POST',
			headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': '0' },
			body: new Uint8Array(1_048_577) // 1 MB + 1 byte, but Content-Length says 0
		});

		const response = await POST(event as never);

		expect(response.status).toBe(413);
		expect(mockFetch).not.toHaveBeenCalled();
	});

	it('forwards request when Content-Length header is absent', async () => {
		const mockFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }));
		const event = makeEvent('documents', 'POST', mockFetch);
		(event.request as Request) = new Request('http://localhost/api/documents', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ title: 'hello' })
		});

		const response = await POST(event as never);

		expect(response.status).toBe(200);
		expect(mockFetch).toHaveBeenCalled();
	});

	it('does not reject request with Content-Length exactly at 1 MB', async () => {
		const mockFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }));
		const event = makeEvent('documents', 'POST', mockFetch);
		(event.request as Request) = new Request('http://localhost/api/documents', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json', 'Content-Length': '1048576' },
			body: JSON.stringify({})
		});

		const response = await POST(event as never);

		expect(response.status).toBe(200);
	});
});

describe('catch-all API proxy — forwarding', () => {
	it('forwards PUT request to backend with correct URL', async () => {
		const mockFetch = vi.fn().mockResolvedValue(
			new Response('{"id":"block-1"}', {
				status: 200,
				headers: { 'Content-Type': 'application/json' }
			})
		);
		const event = makeEvent('documents/doc-1/transcription-blocks/block-1', 'PUT', mockFetch);
		(event.request as Request) = new Request(
			'http://localhost/api/documents/doc-1/transcription-blocks/block-1',
			{
				method: 'PUT',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ text: 'hello' })
			}
		);

		const response = await PUT(event as never);

		expect(mockFetch).toHaveBeenCalledWith(
			'http://backend:8080/api/documents/doc-1/transcription-blocks/block-1',
			expect.objectContaining({ method: 'PUT' })
		);
		expect(response.status).toBe(200);
	});

	it('passes through the backend response status code', async () => {
		const mockFetch = vi.fn().mockResolvedValue(new Response(null, { status: 404 }));
		const event = makeEvent('documents/nonexistent', 'GET', mockFetch);

		const response = await GET(event as never);

		expect(response.status).toBe(404);
	});

	it('forwards Content-Disposition response header', async () => {
		const mockFetch = vi.fn().mockResolvedValue(
			new Response('data', {
				status: 200,
				headers: {
					'Content-Type': 'application/pdf',
					'Content-Disposition': 'attachment; filename="document.pdf"'
				}
			})
		);
		const event = makeEvent('documents/doc-1/export', 'GET', mockFetch);

		const response = await GET(event as never);

		expect(response.headers.get('Content-Disposition')).toBe('attachment; filename="document.pdf"');
	});

	it('does not forward proxy-connection hop-by-hop header', async () => {
		const mockFetch = vi.fn().mockResolvedValue(
			new Response('data', {
				status: 200,
				headers: {
					'Content-Type': 'application/json',
					'Proxy-Connection': 'keep-alive'
				}
			})
		);
		const event = makeEvent('documents', 'GET', mockFetch);

		const response = await GET(event as never);

		expect(response.headers.get('Proxy-Connection')).toBeNull();
	});

	it('does not forward hop-by-hop headers like transfer-encoding', async () => {
		const mockFetch = vi.fn().mockResolvedValue(
			new Response('data', {
				status: 200,
				headers: {
					'Content-Type': 'application/json',
					'Transfer-Encoding': 'chunked'
				}
			})
		);
		const event = makeEvent('documents', 'GET', mockFetch);

		const response = await GET(event as never);

		expect(response.headers.get('Transfer-Encoding')).toBeNull();
	});

	it('forwards PATCH request to backend with correct URL', async () => {
		const mockFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }));
		const event = makeEvent('documents/doc-1', 'PATCH', mockFetch);
		(event.request as Request) = new Request('http://localhost/api/documents/doc-1', {
			method: 'PATCH',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ title: 'updated' })
		});

		const response = await PATCH(event as never);

		expect(mockFetch).toHaveBeenCalledWith(
			'http://backend:8080/api/documents/doc-1',
			expect.objectContaining({ method: 'PATCH' })
		);
		expect(response.status).toBe(200);
	});
});
