import assert from 'node:assert/strict';
import { describe, before, after, it } from 'node:test';
import * as cheerio from 'cheerio';
import { basename } from 'node:path';
import { Writable } from 'node:stream';
import parseSrcset from 'parse-srcset';
import { removeDir } from '../dist/core/fs/index.js';
import { Logger } from '../dist/core/logger/core.js';
import testAdapter from './test-adapter.js';
import { testImageService } from './test-image-service.js';
import { loadFixture } from './test-utils.js';

describe('astro:image', () => {
	/** @type {import('./test-utils').Fixture} */
	let fixture;

	describe('dev', () => {
		/** @type {import('./test-utils').DevServer} */
		let devServer;
		/** @type {Array<{ type: any, level: 'error', message: string; }>} */
		let logs = [];

		before(async () => {
			fixture = await loadFixture({
				root: './fixtures/core-image/',
				image: {
					service: testImageService({ foo: 'bar' }),
					domains: ['avatars.githubusercontent.com'],
				},
			});

			devServer = await fixture.startDevServer({
				logger: new Logger({
					level: 'error',
					dest: new Writable({
						objectMode: true,
						write(event, _, callback) {
							logs.push(event);
							callback();
						},
					}),
				}),
			});
		});

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

		describe('basics', () => {
			let $;
			before(async () => {
				let res = await fixture.fetch('/');
				let html = await res.text();
				$ = cheerio.load(html);
			});

			it('Adds the <img> tag', () => {
				let $img = $('#local img');
				assert.equal($img.length, 1);
				assert.equal($img.attr('src').startsWith('/_image'), true);
			});

			it('includes loading and decoding attributes', () => {
				let $img = $('#local img');
				assert.equal(!!$img.attr('loading'), true);
				assert.equal(!!$img.attr('decoding'), true);
			});

			it('has width and height - no dimensions set', () => {
				let $img = $('#local img');
				assert.equal($img.attr('width'), '207');
				assert.equal($img.attr('height'), '243');
			});

			it('has proper width and height - only width', () => {
				let $img = $('#local-width img');
				assert.equal($img.attr('width'), '350');
				assert.equal($img.attr('height'), '411');
			});

			it('has proper width and height - only height', () => {
				let $img = $('#local-height img');
				assert.equal($img.attr('width'), '170');
				assert.equal($img.attr('height'), '200');
			});

			it('has proper width and height - has both width and height', () => {
				let $img = $('#local-both img');
				assert.equal($img.attr('width'), '300');
				assert.equal($img.attr('height'), '400');
			});

			it('includes the provided alt', () => {
				let $img = $('#local img');
				assert.equal($img.attr('alt'), 'a penguin');
			});

			it('middleware loads the file', async () => {
				let $img = $('#local img');
				let src = $img.attr('src');
				let res = await fixture.fetch(src);
				assert.equal(res.status, 200);
			});

			it('returns proper content-type', async () => {
				let $img = $('#local img');
				let src = $img.attr('src');
				let res = await fixture.fetch(src);
				assert.equal(res.headers.get('content-type'), 'image/webp');
			});

			it('properly skip processing SVGs, but does not error', async () => {
				let res = await fixture.fetch('/svgSupport');
				let html = await res.text();

				$ = cheerio.load(html);
				let $img = $('img');
				assert.equal($img.length, 1);

				let src = $img.attr('src');
				res = await fixture.fetch(src);
				assert.equal(res.status, 200);
			});

			it("errors when an ESM imported image's src is passed to Image/getImage instead of the full import", async () => {
				logs.length = 0;
				let res = await fixture.fetch('/error-image-src-passed');
				await res.text();

				assert.equal(logs.length, 1);
				assert.equal(logs[0].message.includes('must be an imported image or an URL'), true);
			});

			it('supports images from outside the project', async () => {
				let res = await fixture.fetch('/outsideProject');
				let html = await res.text();
				$ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.length, 2);
				assert.equal(
					$img.toArray().every((img) => {
						return (
							img.attribs['src'].startsWith('/@fs/') ||
							img.attribs['src'].startsWith('/_image?href=%2F%40fs%2F')
						);
					}),
					true
				);
			});

			it('supports inlined imports', async () => {
				let res = await fixture.fetch('/inlineImport');
				let html = await res.text();
				$ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.length, 1);

				let src = $img.attr('src');
				res = await fixture.fetch(src);
				assert.equal(res.status, 200);
			});

			it('supports uppercased imports', async () => {
				let res = await fixture.fetch('/uppercase');
				let html = await res.text();
				$ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.length, 1);

				let src = $img.attr('src');
				let loading = $img.attr('loading');
				res = await fixture.fetch(src);
				assert.equal(res.status, 200);
				assert.notEqual(loading, undefined);
			});

			it('supports avif', async () => {
				let res = await fixture.fetch('/avif');
				let html = await res.text();
				$ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.length, 1);

				let src = $img.attr('src');
				res = await fixture.fetch(src);
				assert.equal(res.status, 200);
				assert.equal(res.headers.get('content-type'), 'image/avif');
			});

			it('has a working Picture component', async () => {
				let res = await fixture.fetch('/picturecomponent');
				let html = await res.text();
				$ = cheerio.load(html);

				// Fallback format
				let $img = $('#picture-fallback img');
				assert.equal($img.length, 1);

				const imageURL = new URL($img.attr('src'), 'http://localhost');
				assert.equal(imageURL.searchParams.get('f'), 'jpeg');
				assert.equal($img.attr('fallbackformat'), undefined);

				// Densities
				$img = $('#picture-density-2-format img');
				let $picture = $('#picture-density-2-format picture');
				let $source = $('#picture-density-2-format source');
				assert.equal($img.length, 1);
				assert.equal($picture.length, 1);
				assert.equal($source.length, 2);

				const srcset = parseSrcset($source.attr('srcset'));
				assert.equal(
					srcset.every((src) => src.url.startsWith('/_image')),
					true
				);
				assert.deepEqual(
					srcset.map((src) => src.d),
					[undefined, 2]
				);

				// Widths
				$img = $('#picture-widths img');
				$picture = $('#picture-widths picture');
				$source = $('#picture-widths source');
				assert.equal($img.length, 1);
				assert.equal($picture.length, 1);
				assert.equal($source.length, 1);
				assert.equal(
					$source.attr('sizes'),
					'(max-width: 448px) 400px, (max-width: 810px) 750px, 1050px'
				);

				const srcset2 = parseSrcset($source.attr('srcset'));
				assert.equal(
					srcset2.every((src) => src.url.startsWith('/_image')),
					true
				);
				assert.deepEqual(
					srcset2.map((src) => src.w),
					[207]
				);
			});

			it('properly deduplicate srcset images', async () => {
				let res = await fixture.fetch('/srcset');
				let html = await res.text();
				$ = cheerio.load(html);

				let localImage = $('#local-3-images img');
				assert.equal(
					new Set([
						...parseSrcset(localImage.attr('srcset')).map((src) => src.url),
						localImage.attr('src'),
					]).size,
					3
				);

				let remoteImage = $('#remote-3-images img');
				assert.equal(
					new Set([
						...parseSrcset(remoteImage.attr('srcset')).map((src) => src.url),
						remoteImage.attr('src'),
					]).size,
					3
				);

				let local1x = $('#local-1x img');
				assert.equal(
					new Set([
						...parseSrcset(local1x.attr('srcset')).map((src) => src.url),
						local1x.attr('src'),
					]).size,
					1
				);

				let remote1x = $('#remote-1x img');
				assert.equal(
					new Set([
						...parseSrcset(remote1x.attr('srcset')).map((src) => src.url),
						remote1x.attr('src'),
					]).size,
					1
				);

				let local2Widths = $('#local-2-widths img');
				assert.equal(
					new Set([
						...parseSrcset(local2Widths.attr('srcset')).map((src) => src.url),
						local2Widths.attr('src'),
					]).size,
					2
				);

				let remote2Widths = $('#remote-2-widths img');
				assert.equal(
					new Set([
						...parseSrcset(remote2Widths.attr('srcset')).map((src) => src.url),
						remote2Widths.attr('src'),
					]).size,
					2
				);
			});
		});

		describe('vite-isms', () => {
			/**
			 * @type {cheerio.CheerioAPI}
			 */
			let $;
			before(async () => {
				let res = await fixture.fetch('/vite');
				let html = await res.text();
				$ = cheerio.load(html);
			});

			it('support ?url imports', () => {
				let $url = $('#url');
				assert.equal($url.text(), 'string');
			});

			it('support ?raw imports', () => {
				let $raw = $('#raw');
				assert.equal($raw.text(), 'string');
			});

			it('support glob import as raw', () => {
				let $raw = $('#glob-import');
				assert.equal($raw.text(), 'string');
			});
		});

		describe('remote', () => {
			describe('working', () => {
				let $;
				before(async () => {
					let res = await fixture.fetch('/');
					let html = await res.text();
					$ = cheerio.load(html);
				});

				it('has proper link and works', async () => {
					let $img = $('#remote img');

					let src = $img.attr('src');
					assert.ok(src.startsWith('/_image?'));
					const imageRequest = await fixture.fetch(src);
					assert.equal(imageRequest.status, 200);
				});

				it('includes the provided alt', async () => {
					let $img = $('#remote img');
					assert.equal($img.attr('alt'), 'fred');
				});

				it('includes loading and decoding attributes', () => {
					let $img = $('#remote img');
					assert.equal(!!$img.attr('loading'), true);
					assert.equal(!!$img.attr('decoding'), true);
				});

				it('includes width and height attributes', () => {
					let $img = $('#remote img');
					assert.equal(!!$img.attr('width'), true);
					assert.equal(!!$img.attr('height'), true);
				});

				it('support data: URI', () => {
					let $img = $('#data-uri img');
					assert.equal(
						$img.attr('src'),
						'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAA2gAwAEAAAAAQAAAA0AAAAAWvB1rQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KGV7hBwAAAWJJREFUKBVtUDEsQ1EUve+1/SItKYMIkYpF06GJdGAwNFFGkxBEYupssRm6EpvJbpVoYhRd6FBikDSxYECsBpG25D/nvP/+p+Ik551z73v33feuyA/izq5CL8ET8ALcBolYIP+vd0ibX/yAT7uj2qkVzwWzUBa0nbacbkKJHi5dlYhXmARYeAS+MwCWA5FPqKIP/9IH/wiygMru5y5mcRYkPHYKP7gAPw4SDbCjRXMgRBJctM4t4ROriM2QSpmkeOtub6YfMYrZvelykbD1sxJVg+6AfKqURRKQLfA4JvoVWgIjDMNlGLVKZxNRFsZsoHGAgREZHKPlJEi2t7if3r2KKS9nVOo0rtNZ3yR7M/VGTqTy5Y4o/scWHBbKfIq0/eZ+x3850OZpaTTxlu/4D3ssuA72uxrYS2rFYjh+aRbmb24LpTVu1IqVKG8P/lmUEaNMxeh6fmquOhkMBE8JJ2yPfwPjdVhiDbiX6AAAAABJRU5ErkJggg=='
					);
					assert.equal(!!$img.attr('width'), true);
					assert.equal(!!$img.attr('height'), true);
				});

				it('support images from public', () => {
					let $img = $('#public img');
					assert.equal($img.attr('src'), '/penguin3.jpg');
					assert.equal(!!$img.attr('width'), true);
					assert.equal(!!$img.attr('height'), true);
				});
			});

			it('error if no width and height', async () => {
				logs.length = 0;
				let res = await fixture.fetch('/remote-error-no-dimensions');
				await res.text();

				assert.equal(logs.length, 1);
				assert.equal(logs[0].message.includes('Missing width and height attributes'), true);
			});

			it('error if no height', async () => {
				logs.length = 0;
				let res = await fixture.fetch('/remote-error-no-height');
				await res.text();

				assert.equal(logs.length, 1);
				assert.equal(logs[0].message.includes('Missing height attribute'), true);
			});

			it('supports aliases', async () => {
				let res = await fixture.fetch('/alias');
				let html = await res.text();
				let $ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.length, 1);
				assert.equal($img.attr('src').includes('penguin1.jpg'), true);
			});
		});

		describe('markdown', () => {
			let $;
			before(async () => {
				let res = await fixture.fetch('/post');
				let html = await res.text();
				$ = cheerio.load(html);
			});

			it('Adds the <img> tag', () => {
				let $img = $('img');
				assert.equal($img.length, 2);

				// Verbose test for the full URL to make sure the image went through the full pipeline
				assert.equal(
					$img.attr('src').startsWith('/_image') && $img.attr('src').endsWith('f=webp'),
					true
				);
			});

			it('has width and height attributes', () => {
				let $img = $('img');
				assert.equal(!!$img.attr('width'), true);
				assert.equal(!!$img.attr('height'), true);
			});

			it('Supports aliased paths', async () => {
				let res = await fixture.fetch('/aliasMarkdown');
				let html = await res.text();
				$ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.attr('src').startsWith('/_image'), true);
			});

			it('Supports special characters in file name', async () => {
				let res = await fixture.fetch('/specialChars');
				let html = await res.text();
				$ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.attr('src').startsWith('/_image'), true);
			});

			it('properly handles remote images', async () => {
				let res = await fixture.fetch('/httpImage');
				let html = await res.text();
				$ = cheerio.load(html);

				let $img = $('img');
				assert.equal($img.length, 2);
				const remoteUrls = ['https://example.com/image.png', '/image.png'];
				$img.each((index, element) => {
					assert.equal(element.attribs['src'], remoteUrls[index]);
				});
			});
		});

		describe('getImage', () => {
			let $;
			before(async () => {
				let res = await fixture.fetch('/get-image');
				let html = await res.text();
				$ = cheerio.load(html);
			});

			it('Adds the <img> tag', () => {
				let $img = $('img');
				assert.equal($img.length, 1);
				assert.equal($img.attr('src').startsWith('/_image'), true);
			});

			it('includes the provided alt', () => {
				let $img = $('img');
				assert.equal($img.attr('alt'), 'a penguin');
			});
		});

		describe('content collections', () => {
			let $;
			before(async () => {
				let res = await fixture.fetch('/blog/one');
				let html = await res.text();
				$ = cheerio.load(html);
			});

			it('Adds the <img> tags', () => {
				let $img = $('img');
				assert.equal($img.length, 8);
			});

			it('image in cc folder is processed', () => {
				let $imgs = $('img');
				let $blogfolderimg = $($imgs[7]);
				assert.equal($blogfolderimg.attr('src').includes('blogfolder.jpg'), true);
				assert.equal($blogfolderimg.attr('src').endsWith('f=webp'), true);
			});

			it('has proper source for directly used image', () => {
				let $img = $('#direct-image img');
				assert.equal($img.attr('src').startsWith('/'), true);
			});

			it('has proper source for refined image', () => {
				let $img = $('#refined-image img');
				assert.equal($img.attr('src').startsWith('/'), true);
			});

			it('has proper sources for array of images', () => {
				let $img = $('#array-of-images img');
				const imgsSrcs = [];
				$img.each((i, img) => imgsSrcs.push(img.attribs['src']));
				assert.equal($img.length, 2);
				assert.equal(
					imgsSrcs.every((img) => img.startsWith('/')),
					true
				);
			});

			it('has proper attributes for optimized image through getImage', () => {
				let $img = $('#optimized-image-get-image img');
				assert.equal($img.attr('src').startsWith('/_image'), true);
				assert.equal($img.attr('width'), '207');
				assert.equal($img.attr('height'), '243');
			});

			it('has proper attributes for optimized image through Image component', () => {
				let $img = $('#optimized-image-component img');
				assert.equal($img.attr('src').startsWith('/_image'), true);
				assert.equal($img.attr('width'), '207');
				assert.equal($img.attr('height'), '243');
				assert.equal($img.attr('alt'), 'A penguin!');
			});

			it('properly handles nested images', () => {
				let $img = $('#nested-image img');
				assert.equal($img.attr('src').startsWith('/'), true);
			});
		});

		describe('regular img tag', () => {
			/** @type {ReturnType<import('cheerio')['load']>} */
			let $;
			before(async () => {
				let res = await fixture.fetch('/regular-img');
				let html = await res.text();
				$ = cheerio.load(html);
			});

			it('does not have a file url', async () => {
				assert.equal($('img').attr('src').startsWith('file://'), false);
			});

			it('includes /src in the path', async () => {
				assert.equal($('img').attr('src').includes('/src'), true);
			});
		});

		describe('custom service', () => {
			it('custom service implements getHTMLAttributes', async () => {
				const response = await fixture.fetch('/');
				const html = await response.text();

				const $ = cheerio.load(html);
				assert.equal($('#local img').attr('data-service'), 'my-custom-service');
			});

			it('custom service works in Markdown', async () => {
				const response = await fixture.fetch('/post');
				const html = await response.text();

				const $ = cheerio.load(html);
				assert.equal($('img').attr('data-service'), 'my-custom-service');
			});

			it('gets service config', async () => {
				const response = await fixture.fetch('/');
				const html = await response.text();

				const $ = cheerio.load(html);
				assert.equal($('#local img').attr('data-service-config'), 'bar');
			});
		});

		describe('custom endpoint', async () => {
			/** @type {import('./test-utils').DevServer} */
			let customEndpointDevServer;

			/** @type {import('./test-utils.js').Fixture} */
			let customEndpointFixture;

			before(async () => {
				customEndpointFixture = await loadFixture({
					root: './fixtures/core-image/',
					image: {
						endpoint: './src/custom-endpoint.ts',
						service: testImageService({ foo: 'bar' }),
						domains: ['avatars.githubusercontent.com'],
					},
				});

				customEndpointDevServer = await customEndpointFixture.startDevServer({
					server: { port: 4324 },
				});
			});

			it('custom endpoint works', async () => {
				const response = await customEndpointFixture.fetch('/');
				const html = await response.text();

				const $ = cheerio.load(html);
				const src = $('#local img').attr('src');

				let res = await customEndpointFixture.fetch(src);
				assert.equal(res.status, 200);
				assert.equal(
					await res.text(),
					"You fool! I'm not a image endpoint at all, I just return this!"
				);
			});

			after(async () => {
				await customEndpointDevServer.stop();
			});
		});
	});

	describe('proper errors', () => {
		/** @type {import('./test-utils').DevServer} */
		let devServer;
		/** @type {Array<{ type: any, level: 'error', message: string; }>} */
		let logs = [];

		before(async () => {
			fixture = await loadFixture({
				root: './fixtures/core-image-errors/',
				image: {
					service: testImageService(),
				},
			});

			devServer = await fixture.startDevServer({
				logger: new Logger({
					level: 'error',
					dest: new Writable({
						objectMode: true,
						write(event, _, callback) {
							logs.push(event);
							callback();
						},
					}),
				}),
			});
		});

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

		it("properly error when getImage's first parameter isn't filled", async () => {
			logs.length = 0;
			let res = await fixture.fetch('/get-image-empty');
			await res.text();

			assert.equal(logs.length, 1);
			assert.equal(logs[0].message.includes('Expected getImage() parameter'), true);
		});

		it('properly error when src is undefined', async () => {
			logs.length = 0;
			let res = await fixture.fetch('/get-image-undefined');
			await res.text();

			assert.equal(logs.length, 1);
			assert.equal(logs[0].message.includes('Expected `src` property'), true);
		});

		it('properly error image in Markdown frontmatter is not found', async () => {
			logs.length = 0;
			let res = await fixture.fetch('/blog/one');
			await res.text();

			assert.equal(logs.length, 1);
			assert.equal(logs[0].message.includes('does not exist. Is the path correct?'), true);
		});

		it('properly error image in Markdown content is not found', async () => {
			logs.length = 0;
			let res = await fixture.fetch('/post');
			await res.text();
			assert.equal(logs.length, 1);
			assert.equal(logs[0].message.includes('Could not find requested image'), true);
		});
	});

	describe('support base option correctly', () => {
		before(async () => {
			fixture = await loadFixture({
				root: './fixtures/core-image-base/',
				image: {
					service: testImageService(),
				},
				base: '/blog',
			});
			await fixture.build();
		});

		it('has base path prefix when using the Image component', async () => {
			const html = await fixture.readFile('/index.html');
			const $ = cheerio.load(html);
			const src = $('#local img').attr('src');
			assert.equal(src.length > 0, true);
			assert.equal(src.startsWith('/blog'), true);
		});

		it('has base path prefix when using getImage', async () => {
			const html = await fixture.readFile('/get-image/index.html');
			const $ = cheerio.load(html);
			const src = $('img').attr('src');
			assert.equal(src.length > 0, true);
			assert.equal(src.startsWith('/blog'), true);
		});

		it('has base path prefix when using image directly', async () => {
			const html = await fixture.readFile('/direct/index.html');
			const $ = cheerio.load(html);
			const src = $('img').attr('src');
			assert.equal(src.length > 0, true);
			assert.equal(src.startsWith('/blog'), true);
		});

		it('has base path prefix in Markdown', async () => {
			const html = await fixture.readFile('/post/index.html');
			const $ = cheerio.load(html);
			const src = $('img').attr('src');
			assert.equal(src.length > 0, true);
			assert.equal(src.startsWith('/blog'), true);
		});

		it('has base path prefix in Content Collection frontmatter', async () => {
			const html = await fixture.readFile('/blog/one/index.html');
			const $ = cheerio.load(html);
			const src = $('img').attr('src');
			assert.equal(src.length > 0, true);
			assert.equal(src.startsWith('/blog'), true);
		});

		it('has base path prefix in SSR', async () => {
			const fixtureWithBase = await loadFixture({
				root: './fixtures/core-image-ssr/',
				output: 'server',
				adapter: testAdapter(),
				image: {
					service: testImageService(),
				},
				base: '/blog',
			});
			await fixtureWithBase.build();
			const app = await fixtureWithBase.loadTestAdapterApp();
			const request = new Request('http://example.com/blog/');
			const response = await app.render(request);
			assert.equal(response.status, 200);
			const html = await response.text();
			const $ = cheerio.load(html);
			const src = $('#local img').attr('src');
			assert.equal(src.startsWith('/blog'), true);
		});
	});

	describe('build ssg', () => {
		before(async () => {
			fixture = await loadFixture({
				root: './fixtures/core-image-ssg/',
				image: {
					service: testImageService(),
					domains: ['astro.build', 'avatars.githubusercontent.com'],
				},
			});
			// Remove cache directory
			removeDir(new URL('./fixtures/core-image-ssg/node_modules/.astro', import.meta.url));

			await fixture.build();
		});

		it('writes out images to dist folder', async () => {
			const html = await fixture.readFile('/index.html');
			const $ = cheerio.load(html);
			const src = $('#local img').attr('src');
			assert.equal(src.length > 0, true);
			const data = await fixture.readFile(src, null);
			assert.equal(data instanceof Buffer, true);
		});

		it('writes out allowed remote images', async () => {
			const html = await fixture.readFile('/remote/index.html');
			const $ = cheerio.load(html);
			const src = $('#remote img').attr('src');
			assert.equal(src.length > 0, true);
			const data = await fixture.readFile(src, null);
			assert.equal(data instanceof Buffer, true);
		});

		it('writes out images to dist folder with proper extension if no format was passed', async () => {
			const html = await fixture.readFile('/index.html');
			const $ = cheerio.load(html);
			const src = $('#local img').attr('src');
			assert.equal(src.endsWith('.webp'), true);
		});

		it('getImage() usage also written', async () => {
			const html = await fixture.readFile('/get-image/index.html');
			const $ = cheerio.load(html);
			let $img = $('img');

			// <img> tag
			assert.equal($img.length, 1);
			assert.equal($img.attr('alt'), 'a penguin');

			// image itself
			const src = $img.attr('src');
			const data = await fixture.readFile(src, null);
			assert.equal(data instanceof Buffer, true);
		});

		it('Picture component images are written', async () => {
			const html = await fixture.readFile('/picturecomponent/index.html');
			const $ = cheerio.load(html);
			let $img = $('img');
			let $source = $('source');

			assert.equal($img.length, 1);
			assert.equal($source.length, 2);

			const srcset = parseSrcset($source.attr('srcset'));
			let hasExistingSrc = await Promise.all(
				srcset.map(async (src) => {
					const data = await fixture.readFile(src.url, null);
					return data instanceof Buffer;
				})
			);

			assert.deepEqual(
				hasExistingSrc.every((src) => src === true),
				true
			);
		});

		it('markdown images are written', async () => {
			const html = await fixture.readFile('/post/index.html');
			const $ = cheerio.load(html);
			let $img = $('img');

			// <img> tag
			assert.equal($img.length, 1);
			assert.equal($img.attr('alt'), 'My article cover');

			// image itself
			const src = $img.attr('src');
			const data = await fixture.readFile(src, null);
			assert.equal(data instanceof Buffer, true);
		});

		it('aliased images are written', async () => {
			const html = await fixture.readFile('/alias/index.html');

			const $ = cheerio.load(html);
			let $img = $('img');

			// <img> tag
			assert.equal($img.length, 1);
			assert.equal($img.attr('alt'), 'A penguin!');

			// image itself
			const src = $img.attr('src');
			const data = await fixture.readFile(src, null);
			assert.equal(data instanceof Buffer, true);
		});

		it('aliased images in Markdown are written', async () => {
			const html = await fixture.readFile('/aliasMarkdown/index.html');

			const $ = cheerio.load(html);
			let $img = $('img');

			// <img> tag
			assert.equal($img.length, 1);
			assert.equal($img.attr('alt'), 'A penguin');

			// image itself
			const src = $img.attr('src');
			const data = await fixture.readFile(src, null);
			assert.equal(data instanceof Buffer, true);
		});

		it('output files for content collections images', async () => {
			const html = await fixture.readFile('/blog/one/index.html');

			const $ = cheerio.load(html);
			let $img = $('img');
			assert.equal($img.length, 2);

			const srcdirect = $('#direct-image img').attr('src');
			const datadirect = await fixture.readFile(srcdirect, null);
			assert.equal(datadirect instanceof Buffer, true);

			const srcnested = $('#nested-image img').attr('src');
			const datanested = await fixture.readFile(srcnested, null);
			assert.equal(datanested instanceof Buffer, true);
		});

		it('quality attribute produces a different file', async () => {
			const html = await fixture.readFile('/quality/index.html');
			const $ = cheerio.load(html);
			assert.notEqual($('#no-quality img').attr('src'), $('#quality-low img').attr('src'));
		});

		it('quality can be a number between 0-100', async () => {
			const html = await fixture.readFile('/quality/index.html');
			const $ = cheerio.load(html);
			assert.notEqual($('#no-quality img').attr('src'), $('#quality-num img').attr('src'));
		});

		it('format attribute produces a different file', async () => {
			const html = await fixture.readFile('/format/index.html');
			const $ = cheerio.load(html);
			assert.notEqual($('#no-format img').attr('src'), $('#format-avif img').attr('src'));
		});

		it('has cache entries', async () => {
			const generatedImages = (await fixture.glob('_astro/**/*.webp'))
				.map((path) => basename(path))
				.sort();
			const cachedImages = [
				...(await fixture.glob('../node_modules/.astro/assets/**/*.webp')),
				...(await fixture.glob('../node_modules/.astro/assets/**/*.json')),
			]
				.map((path) => basename(path).replace('.webp.json', '.webp'))
				.sort();

			assert.deepEqual(generatedImages, cachedImages);
		});

		it('uses cache entries', async () => {
			const logs = [];
			const logging = {
				dest: {
					write(chunk) {
						logs.push(chunk);
					},
				},
			};

			await fixture.build({ logging });
			const generatingImageIndex = logs.findIndex((logLine) =>
				logLine.message.includes('generating optimized images')
			);
			const relevantLogs = logs.slice(generatingImageIndex + 1, -1);
			const isReusingCache = relevantLogs.every((logLine) =>
				logLine.message.includes('(reused cache entry)')
			);

			assert.equal(isReusingCache, true);
		});

		it('client images are written to build', async () => {
			const html = await fixture.readFile('/client/index.html');
			const $ = cheerio.load(html);
			let $script = $('script');

			// Find image
			const regex = /src:"([^"]*)/;
			const imageSrc = regex.exec($script.html())[1];
			const data = await fixture.readFile(imageSrc, null);
			assert.equal(data instanceof Buffer, true);
		});

		it('client images srcset parsed correctly', async () => {
			const html = await fixture.readFile('/srcset/index.html');
			const $ = cheerio.load(html);
			const srcset = $('#local-2-widths-with-spaces img').attr('srcset');

			// Find image
			const regex = /^(.+?) \d+[wx]$/m;
			const imageSrcset = regex.exec(srcset)[1];
			assert.notEqual(imageSrcset.includes(' '), true);
		});

		it('supports images with encoded characters in url', async () => {
			const html = await fixture.readFile('/index.html');
			const $ = cheerio.load(html);
			const img = $('#encoded-chars img');
			const src = img.attr('src');
			const data = await fixture.readFile(src);
			assert.notEqual(data, undefined);
		});

		describe('custom service in build', () => {
			it('uses configured hashes properties', async () => {
				await fixture.build();
				const html = await fixture.readFile('/imageDeduplication/index.html');

				const $ = cheerio.load(html);

				const allTheSamePath = $('#all-the-same img')
					.map((_, el) => $(el).attr('src'))
					.get();

				assert.equal(
					allTheSamePath.every((path) => path === allTheSamePath[0]),
					true
				);

				const useCustomHashProperty = $('#use-data img')
					.map((_, el) => $(el).attr('src'))
					.get();
				assert.equal(
					useCustomHashProperty.every((path) => path === useCustomHashProperty[0]),
					false
				);

				assert.notEqual(useCustomHashProperty[1], useCustomHashProperty[0]);
			});
		});
	});

	describe('dev ssr', () => {
		let devServer;
		before(async () => {
			fixture = await loadFixture({
				root: './fixtures/core-image-ssr/',
				output: 'server',
				adapter: testAdapter(),
				image: {
					service: testImageService(),
				},
			});
			devServer = await fixture.startDevServer();
		});

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

		it('does not interfere with query params', async () => {
			let res = await fixture.fetch('/api?src=image.png');
			const html = await res.text();
			assert.equal(html, 'An image: "image.png"');
		});
	});

	describe('prod ssr', () => {
		before(async () => {
			fixture = await loadFixture({
				root: './fixtures/core-image-ssr/',
				output: 'server',
				adapter: testAdapter(),
				image: {
					endpoint: 'astro/assets/endpoint/node',
					service: testImageService(),
				},
			});
			await fixture.build();
		});

		it('dynamic route images are built at response time', async () => {
			const app = await fixture.loadTestAdapterApp();
			let request = new Request('http://example.com/');
			let response = await app.render(request);
			assert.equal(response.status, 200);
			const html = await response.text();
			const $ = cheerio.load(html);
			const src = $('#local img').attr('src');
			request = new Request('http://example.com' + src);
			response = await app.render(request);
			assert.equal(response.status, 200);
		});

		it('prerendered routes images are built', async () => {
			const html = await fixture.readFile('/client/prerender/index.html');
			const $ = cheerio.load(html);
			const src = $('img').attr('src');
			const imgData = await fixture.readFile('/client' + src, null);
			assert.equal(imgData instanceof Buffer, true);
		});
	});
});
