// @ts-check

import * as assert from 'node:assert/strict';
import { after, before, beforeEach, describe, it } from 'node:test';
import { parseHTML } from 'linkedom';
import { loadFixture } from './test-utils.js';

function startOfHourISOString() {
	const date = new Date();
	date.setMinutes(0, 0, 0);
	return date.toISOString();
}

/**
 * @template {Record<K, (...args: any[]) => void>} T
 * @template {keyof T} K
 */
class MockFunction {
	/** @type {Parameters<T[K]>[]} */
	calls = [];

	/**
	 * @param {T} object
	 * @param {K} property
	 */
	constructor(object, property) {
		this.object = object;
		this.property = property;
		this.original = object[property];
		object[property] = /** @param {Parameters<T[K]>} args */ (...args) => {
			this.calls.push(args);
		};
	}
	restore() {
		this.object[this.property] = this.original;
	}
	reset() {
		this.calls = [];
	}
}

describe('Web Vitals integration basics', () => {
	/** @type {import('./test-utils').Fixture} */
	let fixture;
	/** @type {import('./test-utils').DevServer} */
	let devServer;
	/** @type {MockFunction<Console, 'error'>} */
	let consoleErrorMock;

	before(async () => {
		consoleErrorMock = new MockFunction(console, 'error');
		fixture = await loadFixture({ root: './fixtures/basics/' });
		devServer = await fixture.startDevServer({});
	});

	after(async () => {
		consoleErrorMock.restore();
		await devServer.stop();
	});

	beforeEach(() => {
		consoleErrorMock.reset();
	});

	it('adds a meta tag to the page', async () => {
		const html = await fixture.fetch('/', {}).then((res) => res.text());
		const { document } = parseHTML(html);
		const meta = document.querySelector('head > meta[name="x-astro-vitals-route"]');
		assert.ok(meta);
		assert.equal(meta.getAttribute('content'), '/');
	});

	it('adds a meta tag using the route pattern to the page', async () => {
		const html = await fixture.fetch('/test', {}).then((res) => res.text());
		const { document } = parseHTML(html);
		const meta = document.querySelector('head > meta[name="x-astro-vitals-route"]');
		assert.ok(meta);
		assert.equal(meta.getAttribute('content'), '/[dynamic]');
	});

	it('returns a 200 response even when bad data is sent to the injected endpoint', async () => {
		{
			// bad data
			const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: 'garbage' });
			assert.equal(res.status, 200);
		}
		{
			// no data
			const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: '[]' });
			assert.equal(res.status, 200);
		}
		assert.equal(consoleErrorMock.calls.length, 2);
	});

	it('validates data sent to the injected endpoint with Zod', async () => {
		const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: '[{}]' });
		assert.equal(res.status, 200);
		const call = consoleErrorMock.calls[0][0];
		assert.ok(call instanceof Error);
		assert.equal(call.name, 'ZodError');
	});

	describe('inserting data via the injected endpoint', () => {
		/** @type {Response} */
		let res;
		before(async () => {
			res = await fixture.fetch('/_web-vitals', {
				method: 'POST',
				body: JSON.stringify([
					{
						pathname: '/',
						route: '/',
						name: 'CLS',
						id: 'v4-1711484350895-3748043125387',
						value: 0,
						rating: 'good',
					},
				]),
			});
		});

		it('inserting data does not error', () => {
			assert.equal(res.status, 200);
			assert.equal(
				consoleErrorMock.calls.length,
				0,
				'Endpoint logged errors:\n' + consoleErrorMock.calls[0]?.join(' '),
			);
		});

		it('inserted data can be retrieved from the database', async () => {
			const dbRows = await fixture.fetch('/rows.json', {}).then((r) => r.json());
			assert.deepEqual(dbRows, [
				{
					pathname: '/',
					route: '/',
					name: 'CLS',
					id: 'v4-17114843-3748043125387',
					value: 0,
					rating: 'good',
					timestamp: startOfHourISOString(),
				},
			]);
		});
	});

	it('inserted data uses a truncated timestamp in the ID', async () => {
		// The IDs generated by the `web-vitals` package include a high resolution timestamp as the second portion,
		// e.g. 'v4-1711484350895-3748043125387'. We reduce this data to an hourly resolution to lessen privacy concerns.
		const dbRows = await fixture.fetch('/rows.json', {}).then((r) => r.json());
		assert.deepEqual(dbRows[0].id, 'v4-17114843-3748043125387');
	});
});
