import { Cache } from '~/models/cache.model';
import faker from '@faker-js/faker';

jest.useFakeTimers();

type RandomRecord = Readonly<{
	key: string;
	value: Readonly<unknown>;
}>;

describe('cache.class', () => {
	/* eslint-disable @typescript-eslint/init-declarations */
	let cache: Cache<unknown>;
	/* eslint-enable @typescript-eslint/init-declarations */

	afterEach(() => {
		cache.clear();
	});

	describe('Default constructor options', () => {
		beforeEach(() => {
			cache = new Cache();
		});

		test('Setting and getting a record', () => {
			const key = faker.lorem.word();
			const record = JSON.parse(faker.datatype.json()) as Readonly<unknown>;

			cache.set(key, record);

			const stored = cache.get(key);

			expect(stored).toStrictEqual(record);
		});

		test('Deleting a record', () => {
			const key = faker.lorem.word();
			const record = JSON.parse(faker.datatype.json()) as Readonly<unknown>;

			cache.set(key, record);
			cache.delete(key);

			const stored = cache.get(key);

			expect(stored).toBeNull();
		});

		test('Clearing the cache', () => {
			const numRecords = 10;

			const keyValuePairs = new Array(numRecords)
				.fill(null)
				.map((): [string, Readonly<unknown>] => [
					faker.lorem.word(),
					JSON.parse(faker.datatype.json()) as Readonly<unknown>
				]);

			keyValuePairs
				.forEach(([key, value]: readonly [string, Readonly<unknown>]) => cache.set(key, value));

			cache.clear();

			keyValuePairs
				.map(([key]: readonly [string, Readonly<unknown>]) => key)
				.forEach(key => {
					const stored = cache.get(key);

					expect(stored).toBeNull();
				});
		});
	});

	describe('Custom maxSize parameter', () => {
		const maxSize = 10;

		beforeEach(() => {
			cache = new Cache({ maxSize });
		});

		test('maxSize respected, with LRU default', () => {
			// Create maxSize + 1 records
			const records: RandomRecord[] = new Array(maxSize + 1)
				.fill(null)
				.map((unused, index) => ({
					key: `${index}`,
					value: JSON.parse(faker.datatype.json()) as Readonly<unknown>
				}));

			const [firstRecord] = records;

			const { [records.length - 1]: lastRecord } = records;

			const allButLastRecord = records.slice(0, records.length - 1);

			// Insert all but the last into the cache
			allButLastRecord.forEach(({ key, value }) => {
				cache.set(key, value);
			});

			// Verify that all but the last are in the cache
			allButLastRecord.forEach(({ key, value }) => {
				expect(cache.get(key)).toStrictEqual(value);
			});

			// Verify that the last is not in the cache
			expect(cache.get(lastRecord.key)).toBeNull();

			// Insert the last
			cache.set(lastRecord.key, lastRecord.value);

			// Verify that the first is no longer in the cache (LRU default)
			expect(cache.get(firstRecord.key)).toStrictEqual(null);

			// Verify that the last is in the cache
			expect(cache.get(lastRecord.key)).toStrictEqual(lastRecord.value);
		});
	});

	describe('Custom ttl parameter', () => {
		const ttl = 5;

		beforeEach(() => {
			cache = new Cache<RandomRecord>({ ttl });
		});

		test('ttl respected', () => {
			const key = faker.lorem.word();
			const record = JSON.parse(faker.datatype.json()) as RandomRecord;

			cache.set(key, record);

			let storedRecord = cache.get(key);

			expect(storedRecord).toStrictEqual(record);

			jest.advanceTimersByTime(ttl + 1);

			storedRecord = cache.get(key);

			expect(storedRecord).toBeNull();
		});
	});
});
