import { beforeAll, describe, expect, it } from "vitest";
import { audio, boot, event, loader } from "../src/index.js";

describe("loader", () => {
	let audioURI;
	let imgURI;

	beforeAll(async () => {
		audioURI =
			"data:audio/mpeg;base64, ";

		imgURI =
			"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";

		boot();
		audio.init("mp3,ogg");
	});

	it("configure the preloader", async () => {
		loader.setOptions({ crossOrigin: "anonymous" });
		expect(loader.crossOrigin === "anonymous").toEqual(true);
	});

	it("should load supported audio assets", async () => {
		await expect(
			new Promise((resolve, reject) => {
				loader.load(
					{
						name: "silence",
						type: "audio",
						src: "/data/sfx/",
					},
					() => {
						resolve(true);
					},
					() => {
						reject(new Error("Failed to load `rect.png`"));
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should load base64 encoded audio assets", async () => {
		await expect(
			new Promise((resolve, reject) => {
				loader.load(
					{
						name: "silence2",
						type: "audio",
						src: audioURI,
					},
					() => {
						resolve(true);
					},
					() => {
						reject(new Error("Failed to load `rect.png`"));
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should load image asset", async () => {
		await expect(
			new Promise((resolve, reject) => {
				loader.load(
					{
						name: "testimage",
						type: "image",
						src: "/data/img/rect.png",
					},
					() => {
						resolve(loader.getImage("testimage") !== null);
					},
					() => {
						reject(new Error("Failed to load `rect.png`"));
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should load base64 encoded image asset", async () => {
		await expect(
			new Promise((resolve, reject) => {
				loader.load(
					{
						name: "testimage2",
						type: "image",
						src: imgURI,
					},
					() => {
						resolve(loader.getImage("testimage2") !== null);
					},
					() => {
						reject(new Error("Failed to load `rect.png`"));
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should unload image asset", async () => {
		loader.unload({ name: "testimage", type: "image" });
		loader.unload({ name: "testimage2", type: "image" });

		expect(
			loader.getImage("testimage") === null &&
				loader.getImage("testimage2") === null,
		).toEqual(true);
	});

	it("should preload multiple assets and call completion callback", async () => {
		await expect(
			new Promise((resolve) => {
				loader.preload(
					[
						{
							name: "preload_img1",
							type: "image",
							src: "/data/img/rect.png",
						},
						{
							name: "preload_img2",
							type: "image",
							src: "/data/img/rect.png",
						},
					],
					() => {
						resolve(true);
					},
					false,
				);
			}),
		).resolves.toBe(true);

		// cleanup
		loader.unload({ name: "preload_img1", type: "image" });
		loader.unload({ name: "preload_img2", type: "image" });
	});

	it("should skip already loaded assets without duplicating", async () => {
		// load an image first
		await new Promise((resolve) => {
			loader.load(
				{ name: "dedup_img", type: "image", src: "/data/img/rect.png" },
				() => {
					resolve();
				},
			);
		});

		const firstRef = loader.getImage("dedup_img");
		expect(firstRef).not.toBeNull();

		// loading the same asset again should return 0 (already loaded)
		const count = loader.load(
			{ name: "dedup_img", type: "image", src: "/data/img/rect.png" },
			() => {},
		);
		expect(count).toBe(0);

		// the reference should be the same object
		expect(loader.getImage("dedup_img")).toBe(firstRef);

		// cleanup
		loader.unload({ name: "dedup_img", type: "image" });
	});

	it("should report progress via LOADER_PROGRESS event", async () => {
		const progressValues = [];

		const listener = (progress) => {
			progressValues.push(progress);
		};

		event.on(event.LOADER_PROGRESS, listener);

		await new Promise((resolve) => {
			loader.preload(
				[
					{
						name: "progress_img1",
						type: "image",
						src: "/data/img/rect.png",
					},
					{
						name: "progress_img2",
						type: "image",
						src: "/data/img/rect.png",
					},
				],
				() => {
					resolve();
				},
				false,
			);
		});

		// should have received progress callbacks
		expect(progressValues.length).toBeGreaterThan(0);

		// last progress value should be 1 (100%)
		expect(progressValues[progressValues.length - 1]).toBe(1);

		// progress should be monotonically increasing
		for (let i = 1; i < progressValues.length; i++) {
			expect(progressValues[i]).toBeGreaterThanOrEqual(progressValues[i - 1]);
		}

		// cleanup
		event.off(event.LOADER_PROGRESS, listener);
		loader.unload({ name: "progress_img1", type: "image" });
		loader.unload({ name: "progress_img2", type: "image" });
	});

	it("should emit LOADER_PROGRESS events", async () => {
		const events = [];

		const listener = (progress, res) => {
			events.push({ progress, name: res.name });
		};

		event.on(event.LOADER_PROGRESS, listener);

		await new Promise((resolve) => {
			loader.preload(
				[
					{
						name: "event_img",
						type: "image",
						src: "/data/img/rect.png",
					},
				],
				() => {
					resolve();
				},
				false,
			);
		});

		expect(events.length).toBeGreaterThan(0);
		expect(events[events.length - 1].name).toBe("event_img");

		// cleanup
		event.off(event.LOADER_PROGRESS, listener);
		loader.unload({ name: "event_img", type: "image" });
	});

	it("should set and get base URL for asset types", () => {
		loader.setBaseURL("image", "assets/img/");
		expect(loader.baseURL["image"]).toBe("assets/img/");

		// reset
		loader.setBaseURL("image", "./");
	});

	it("should set base URL for all types with wildcard", () => {
		loader.setBaseURL("*", "https://cdn.example.com/");

		expect(loader.baseURL["image"]).toBe("https://cdn.example.com/");
		expect(loader.baseURL["audio"]).toBe("https://cdn.example.com/");
		expect(loader.baseURL["json"]).toBe("https://cdn.example.com/");
		expect(loader.baseURL["binary"]).toBe("https://cdn.example.com/");
		expect(loader.baseURL["tmx"]).toBe("https://cdn.example.com/");
		expect(loader.baseURL["fontface"]).toBe("https://cdn.example.com/");

		// reset
		loader.setBaseURL("*", "./");
	});

	it("should strip url() wrapper from fontface src before applying baseURL", () => {
		loader.setBaseURL("fontface", "assets/");
		const receivedSrc = [];

		// stub fontface parser to capture the resolved src
		loader.setParser("fontface", (data, onload) => {
			receivedSrc.push(data.src);
			if (typeof onload === "function") {
				onload();
			}
			return 1;
		});

		// plain path
		loader.load(
			{ name: "font1", type: "fontface", src: "font/test.woff2" },
			() => {},
		);
		// url() wrapped
		loader.load(
			{ name: "font2", type: "fontface", src: "url(font/test.woff2)" },
			() => {},
		);
		// url() with quotes
		loader.load(
			{ name: "font3", type: "fontface", src: "url('font/test.woff2')" },
			() => {},
		);

		// all should resolve to the same baseURL + path
		expect(receivedSrc[0]).toBe("assets/font/test.woff2");
		expect(receivedSrc[1]).toBe("assets/font/test.woff2");
		expect(receivedSrc[2]).toBe("assets/font/test.woff2");

		// reset
		loader.setBaseURL("fontface", "./");
	});

	it("should not strip local() wrapper from fontface src", () => {
		// reset baseURL so it doesn't interfere
		loader.setBaseURL("fontface", "./");
		const receivedSrc = [];

		loader.setParser("fontface", (data, onload) => {
			receivedSrc.push(data.src);
			if (typeof onload === "function") {
				onload();
			}
			return 1;
		});

		loader.load(
			{ name: "font4", type: "fontface", src: "local('My Font')" },
			() => {},
		);

		expect(receivedSrc[0]).toBe("local('My Font')");
	});

	it("should configure loader options", () => {
		loader.setOptions({
			crossOrigin: "use-credentials",
			withCredentials: true,
		});

		expect(loader.crossOrigin).toBe("use-credentials");
		expect(loader.withCredentials).toBe(true);

		// reset
		loader.setOptions({
			crossOrigin: "anonymous",
			withCredentials: false,
		});
	});

	it("should throw on unknown asset type", () => {
		expect(() => {
			loader.load({ name: "test", type: "unknown", src: "test.xyz" });
		}).toThrow();
	});

	it("should throw on invalid parser function", () => {
		expect(() => {
			loader.setParser("custom", "not a function");
		}).toThrow();
	});

	it("should allow registering a custom parser", async () => {
		loader.setParser("custom", (data, onload) => {
			if (typeof onload === "function") {
				onload();
			}
			return 1;
		});

		await expect(
			new Promise((resolve) => {
				loader.load(
					{ name: "custom_asset", type: "custom", src: "test" },
					() => {
						resolve(true);
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should return null for non-existent assets", () => {
		expect(loader.getImage("nonexistent")).toBeNull();
		expect(loader.getJSON("nonexistent")).toBeNull();
		expect(loader.getTMX("nonexistent")).toBeNull();
		expect(loader.getBinary("nonexistent")).toBeNull();
		expect(loader.getVideo("nonexistent")).toBeNull();
		expect(loader.getFont("nonexistent")).toBeNull();
	});

	it("should return false when unloading non-existent assets", () => {
		expect(loader.unload({ name: "nope", type: "image" })).toBe(false);
		expect(loader.unload({ name: "nope", type: "json" })).toBe(false);
		expect(loader.unload({ name: "nope", type: "binary" })).toBe(false);
		expect(loader.unload({ name: "nope", type: "tmx" })).toBe(false);
		expect(loader.unload({ name: "nope", type: "video" })).toBe(false);
	});

	it("should call onerror for invalid image sources", async () => {
		await expect(
			new Promise((resolve, reject) => {
				loader.load(
					{
						name: "bad_image",
						type: "image",
						src: "nonexistent/path/image.png",
					},
					() => {
						reject(new Error("should not succeed"));
					},
					() => {
						resolve(true);
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should call onerror for invalid JSON sources", async () => {
		await expect(
			new Promise((resolve, reject) => {
				loader.load(
					{
						name: "bad_json",
						type: "json",
						src: "nonexistent/path/data.json",
					},
					() => {
						reject(new Error("should not succeed"));
					},
					() => {
						resolve(true);
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should call onerror for invalid binary sources", async () => {
		await expect(
			new Promise((resolve, reject) => {
				loader.load(
					{
						name: "bad_binary",
						type: "binary",
						src: "nonexistent/path/data.bin",
					},
					() => {
						reject(new Error("should not succeed"));
					},
					() => {
						resolve(true);
					},
				);
			}),
		).resolves.toBe(true);
	});

	it("should handle inline TMX data and return 1", () => {
		const result = loader.load(
			{
				name: "inline_tmx",
				type: "tmx",
				data: {
					width: 1,
					height: 1,
					tilewidth: 32,
					tileheight: 32,
					orientation: "orthogonal",
					renderorder: "right-down",
					version: "1.10",
					layers: [],
					tilesets: [],
				},
			},
			() => {},
		);
		expect(result).toBe(1);
	});
});
