import { beforeAll, describe, expect, it } from "bun:test";
import { getJwtHeaders } from "tests/helpers/jwt";
import { expectStatus } from "tests/utils";
import { db } from "~/db";
import { shows } from "~/db/schema";
import { bubble } from "~/models/examples";
import { dune1984 } from "~/models/examples/dune-1984";
import { dune } from "~/models/examples/dune-2021";
import type { Movie } from "~/models/movie";
import { createMovie, getMovies, handlers } from "../helpers";

beforeAll(async () => {
	await db.delete(shows);
	for (const movie of [bubble, dune1984, dune]) {
		const [ret, _] = await createMovie(movie);
		expect(ret.status).toBe(201);
	}
});

describe("Get all movies", () => {
	it("Invalid filter params", async () => {
		const [resp, body] = await getMovies({
			filter: `slug eq ${bubble.slug}`,
			langs: "en",
		});

		expectStatus(resp, body).toBe(422);
		expect(body).toMatchObject({
			status: 422,
			message: expect.any(String),
			details: {
				in: "slug eq bubble",
			},
		});
	});
	it("Invalid filter syntax", async () => {
		const [resp, body] = await getMovies({
			filter: `slug eq gt ${bubble.slug}`,
			langs: "en",
		});

		expectStatus(resp, body).toBe(422);
		expect(body).toMatchObject({
			details: expect.anything(),
			message: "Invalid filter: slug eq gt bubble.",
			status: 422,
		});
	});
	it("Limit 2, default sort", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			langs: "en",
		});

		expectStatus(resp, body).toBe(200);
		expect(body).toMatchObject({
			items: [
				expect.objectContaining({ slug: bubble.slug }),
				expect.objectContaining({ slug: dune.slug }),
			],
			this: "http://localhost/api/movies?limit=2",
			// we can't have the exact after since it contains the pk that changes with every tests.
			next: expect.stringContaining(
				"http://localhost/api/movies?limit=2&after=WyJkdW5lIiw",
			),
		});
	});
	it("Limit 2, default sort, page 2", async () => {
		let [resp, body] = await getMovies({
			limit: 2,
			langs: "en",
		});
		expectStatus(resp, body).toBe(200);

		resp = await handlers.handle(
			new Request(body.next, { headers: await getJwtHeaders() }),
		);
		body = await resp.json();

		expectStatus(resp, body).toBe(200);
		expect(body).toMatchObject({
			items: [expect.objectContaining({ slug: dune1984.slug })],
			this: expect.stringContaining(
				"http://localhost/api/movies?limit=2&after=WyJkdW5lIiw",
			),
			next: null,
		});
	});
	it("Limit 2, sort by dates desc, page 2", async () => {
		let [resp, body] = await getMovies({
			limit: 2,
			sort: "-airDate",
			langs: "en",
		});
		expectStatus(resp, body).toBe(200);

		// we copy this due to https://github.com/oven-sh/bun/issues/3521
		const next = body.next;
		expect(body).toMatchObject({
			items: [
				expect.objectContaining({ slug: bubble.slug, airDate: bubble.airDate }),
				expect.objectContaining({ slug: dune.slug, airDate: dune.airDate }),
			],
			this: "http://localhost/api/movies?limit=2&sort=-airDate",
			next: expect.stringContaining(
				"http://localhost/api/movies?limit=2&sort=-airDate&after=WyIyMDIxLTEwLTIyIiw",
			),
		});

		resp = await handlers.handle(
			new Request(next, { headers: await getJwtHeaders() }),
		);
		body = await resp.json();

		expectStatus(resp, body).toBe(200);
		expect(body).toMatchObject({
			items: [
				expect.objectContaining({
					slug: dune1984.slug,
					airDate: dune1984.airDate,
				}),
			],
			this: next,
			next: null,
		});
	});

	describe("Random sort", () => {
		it("No limit, compare order with same seeds", async () => {
			// First query
			const [resp1, body1] = await getMovies({
				sort: "random:100",
			});
			expectStatus(resp1, body1).toBe(200);
			const items1: Movie[] = body1.items;
			const items1Ids = items1.map(({ id }) => id);

			// Second query
			const [resp2, body2] = await getMovies({
				sort: "random:100",
			});
			expectStatus(resp2, body2).toBe(200);
			const items2: Movie[] = body2.items;
			const items2Ids = items2.map(({ id }) => id);

			expect(items1Ids).toEqual(items2Ids);
		});
		it("Limit 1, pages 1 and 2 ", async () => {
			// First query fetches all
			// use the result to know what is expected
			let [resp, body] = await getMovies({
				sort: "random:1234",
			});
			expectStatus(resp, body).toBe(200);
			let items: Movie[] = body.items;
			const expectedIds = items.map(({ id }) => id);

			// Get First Page
			[resp, body] = await getMovies({
				sort: "random:1234",
				limit: 1,
			});
			expectStatus(resp, body).toBe(200);
			items = body.items;
			expect(items.length).toBe(1);
			expect(items[0].id).toBe(expectedIds[0]);
			// Get Second Page
			resp = await handlers.handle(
				new Request(body.next, { headers: await getJwtHeaders() }),
			);
			body = await resp.json();

			expectStatus(resp, body).toBe(200);
			items = body.items;
			expect(items.length).toBe(1);
			expect(items[0].id).toBe(expectedIds[1]);
		});
		it("Limit 1, pages 1 and 2, no seed ", async () => {
			const [resp, body] = await getMovies({
				sort: "random",
				limit: 2,
			});
			expectStatus(resp, body).toBe(200);

			const resp2 = await handlers.handle(
				new Request(body.next, { headers: await getJwtHeaders() }),
			);
			const body2 = await resp2.json();
			expectStatus(resp2, body).toBe(200);

			expect(body2.items.length).toBe(1);
			expect(body.items.map((x: Movie) => x.slug)).not.toContain(
				body2.items[0].slug,
			);
		});

		it("Get /random", async () => {
			const resp = await handlers.handle(
				new Request("http://localhost/api/movies/random", {
					headers: await getJwtHeaders(),
				}),
			);
			expect(resp.status).toBe(302);
			const location = resp.headers.get("location")!;
			expect(location).toStartWith("/api/movies/");
		});
	});
	it("Limit 2, fallback lang, prefer original", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			langs: "en-au",
			preferOriginal: true,
		});

		expectStatus(resp, body).toBe(200);
		expect(body.items[0]).toMatchObject({
			slug: bubble.slug,
			name: bubble.translations.en.name,
			poster: {
				source: bubble.translations.ja.poster,
			},
			thumbnail: {
				source: bubble.translations.ja.thumbnail,
			},
			banner: null,
			// we fallback to the translated value when the original is null.
			logo: { source: bubble.translations.en.logo },
		});
		expect(body.items[1]).toMatchObject({
			slug: dune.slug,
			name: dune.translations.en.name,
		});
	});
	it("Limit 2, * lang, prefer original", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			langs: "*",
			preferOriginal: true,
		});

		expectStatus(resp, body).toBe(200);
		expect(body.items[0]).toMatchObject({
			slug: bubble.slug,
			name: expect.any(String),
			poster: {
				source: expect.any(String),
			},
			thumbnail: {
				source: expect.any(String),
			},
			banner: null,
		});
		expect(body.items[1]).toMatchObject({
			slug: dune.slug,
			name: dune.translations.en.name,
		});
	});
	it("Limit 2, unknown lang, prefer original", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			langs: "toto",
			preferOriginal: true,
		});

		expectStatus(resp, body).toBe(200);
		expect(body.items[0]).toMatchObject({
			slug: bubble.slug,
			name: expect.any(String),
			poster: {
				source: expect.any(String),
			},
			thumbnail: {
				source: expect.any(String),
			},
			banner: null,
		});
		expect(body.items[1]).toMatchObject({
			slug: dune.slug,
			name: dune.translations.en.name,
		});
	});
	it("Filter with tags", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			filter: "tags eq gravity",
			langs: "en",
		});

		expectStatus(resp, body).toBe(200);
		expect(body.items).toBeArrayOfSize(1);
		expect(body.items[0].slug).toBe(bubble.slug);
	});
});

describe("search", () => {
	it("Partial match", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			query: "bub",
			langs: "en",
		});

		expectStatus(resp, body).toBe(200);
		expect(body.items).toBeArrayOfSize(1);
		expect(body.items[0].slug).toBe(bubble.slug);
	});
	it("Invalid search don't match", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			query: "buboeuoeunhoeu",
			langs: "en",
		});

		expectStatus(resp, body).toBe(200);
		expect(body.items).toBeArrayOfSize(0);
	});
	it("Typo match", async () => {
		const [resp, body] = await getMovies({
			limit: 2,
			query: "bobble",
			langs: "en",
		});

		expectStatus(resp, body).toBe(200);
		expect(body.items).toBeArrayOfSize(1);
		expect(body.items[0].slug).toBe(bubble.slug);
	});
});
