import type { KyselyPlugin, UnknownRow } from "kysely";
import {
	createQueryId,
	DummyDriver,
	Kysely,
	NoResultError,
	PostgresAdapter,
	PostgresIntrospector,
	PostgresQueryCompiler,
} from "kysely";
import { jsonArrayFrom } from "kysely/helpers/postgres";
import { describe, expect, expectTypeOf, it } from "vitest";
import {
	DateJsonParserPlugin,
	type WithParsedDates,
	withParsedDates,
} from "~/lib/dateParserJsonKysely";

// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------

interface Database {
	box: BoxTable;
	item: ItemTable;
}

interface BoxTable {
	id: number;
}

interface ItemTable {
	id: number;
	box_id: number;
	created_at: Date;
	updated_at: Date | null;
}

// ---------------------------------------------------------------------------
// DummyDriver db — used only for compile-time type tests
// ---------------------------------------------------------------------------

const db = new Kysely<Database>({
	dialect: {
		createAdapter: () => new PostgresAdapter(),
		createDriver: () => new DummyDriver(),
		// biome-ignore lint/suspicious/noExplicitAny: required by Kysely dialect contract
		createIntrospector: (db: Kysely<any>) => new PostgresIntrospector(db),
		createQueryCompiler: () => new PostgresQueryCompiler(),
	},
});

// ---------------------------------------------------------------------------
// Fake query helper — pipes synthetic rows through the plugin at runtime
// ---------------------------------------------------------------------------

class TestNoResultError extends Error {
	constructor() {
		super("No result (test)");
		this.name = "TestNoResultError";
	}
}

function makeFakeQuery<T>(rows: T[]) {
	return {
		withPlugin(plugin: KyselyPlugin) {
			async function run(): Promise<T[]> {
				const result = await plugin.transformResult({
					queryId: createQueryId(),
					result: { rows: rows as UnknownRow[] },
				});
				return result.rows as T[];
			}
			return {
				execute: run,
				async executeTakeFirst() {
					return (await run())[0];
				},
				async executeTakeFirstOrThrow(
					errorConstructor?: (new () => Error) | ((node: unknown) => Error),
				) {
					const row = (await run())[0];
					if (row !== undefined) return row;
					if (typeof errorConstructor === "function") {
						try {
							// try constructor form first
							throw new (errorConstructor as new () => Error)();
						} catch (e) {
							if (e instanceof Error) throw e;
							// it was a factory function, not a constructor
							throw (errorConstructor as (node: unknown) => Error)({});
						}
					}
					throw new NoResultError({} as never);
				},
			};
		},
	};
}

// ---------------------------------------------------------------------------
// DateJsonParserPlugin — direct unit tests
// ---------------------------------------------------------------------------

describe("DateJsonParserPlugin", () => {
	it("parses a top-level date string", async () => {
		const plugin = new DateJsonParserPlugin(["created_at"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: { rows: [{ created_at: "2024-01-15T10:00:00.000Z" }] },
		});
		expect(rows[0]?.created_at).toBeInstanceOf(Date);
		expect((rows[0]?.created_at as Date).toISOString()).toBe(
			"2024-01-15T10:00:00.000Z",
		);
	});

	it("parses dates nested inside a JSON array", async () => {
		const plugin = new DateJsonParserPlugin(["items.created_at"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: {
				rows: [
					{
						items: [
							{ id: 1, created_at: "2024-01-01T00:00:00.000Z" },
							{ id: 2, created_at: "2024-06-15T12:30:00.000Z" },
						],
					},
				],
			},
		});
		const items = rows[0]?.items as { id: number; created_at: Date }[];
		expect(items[0]?.created_at).toBeInstanceOf(Date);
		expect(items[0]?.created_at.toISOString()).toBe("2024-01-01T00:00:00.000Z");
		expect(items[1]?.created_at).toBeInstanceOf(Date);
		expect(items[1]?.created_at.toISOString()).toBe("2024-06-15T12:30:00.000Z");
	});

	it("parses a date nested inside a JSON object", async () => {
		const plugin = new DateJsonParserPlugin(["meta.published_at"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: {
				rows: [{ meta: { published_at: "2024-03-01T00:00:00.000Z" } }],
			},
		});
		const meta = rows[0]?.meta as { published_at: Date };
		expect(meta.published_at).toBeInstanceOf(Date);
	});

	it("leaves null values untouched", async () => {
		const plugin = new DateJsonParserPlugin(["items.created_at"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: { rows: [{ items: [{ id: 1, created_at: null }] }] },
		});
		const items = rows[0]?.items as { created_at: null }[];
		expect(items[0]?.created_at).toBeNull();
	});

	it("leaves undefined values untouched", async () => {
		const plugin = new DateJsonParserPlugin(["created_at"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: { rows: [{ created_at: undefined }] },
		});
		expect(rows[0]?.created_at).toBeUndefined();
	});

	it("leaves non-string primitive values untouched", async () => {
		const plugin = new DateJsonParserPlugin(["count"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: { rows: [{ count: 42 }] },
		});
		expect(rows[0]?.count).toBe(42);
	});

	it("applies multiple paths independently", async () => {
		const plugin = new DateJsonParserPlugin(["created_at", "updated_at"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: {
				rows: [
					{
						created_at: "2024-01-01T00:00:00.000Z",
						updated_at: "2024-06-01T00:00:00.000Z",
					},
				],
			},
		});
		expect(rows[0]?.created_at).toBeInstanceOf(Date);
		expect(rows[0]?.updated_at).toBeInstanceOf(Date);
	});

	it("parses the same path across multiple result rows", async () => {
		const plugin = new DateJsonParserPlugin(["created_at"]);
		const { rows } = await plugin.transformResult({
			queryId: createQueryId(),
			result: {
				rows: [
					{ created_at: "2024-01-01T00:00:00.000Z" },
					{ created_at: "2024-02-01T00:00:00.000Z" },
					{ created_at: "2024-03-01T00:00:00.000Z" },
				],
			},
		});
		for (const row of rows) {
			expect(row.created_at).toBeInstanceOf(Date);
		}
	});

	it("does not mutate the original rows array", async () => {
		const original = [{ created_at: "2024-01-01T00:00:00.000Z" }];
		const plugin = new DateJsonParserPlugin(["created_at"]);
		await plugin.transformResult({
			queryId: createQueryId(),
			result: { rows: original },
		});
		// original row object should be untouched (plugin shallow-copies each row)
		expect(original[0]?.created_at).toBe("2024-01-01T00:00:00.000Z");
	});
});

// ---------------------------------------------------------------------------
// withParsedDates — runtime behaviour
// ---------------------------------------------------------------------------

describe("withParsedDates – runtime", () => {
	it("execute() returns all rows with dates parsed", async () => {
		const rows = await withParsedDates(
			makeFakeQuery([
				{ id: 1, items: [{ created_at: "2024-01-01T00:00:00.000Z" }] },
				{ id: 2, items: [{ created_at: "2024-06-01T00:00:00.000Z" }] },
			]),
			["items.created_at"],
		).execute();

		expect(rows).toHaveLength(2);
		expect(rows[0]?.items[0]?.created_at).toBeInstanceOf(Date);
		expect(rows[1]?.items[0]?.created_at).toBeInstanceOf(Date);
	});

	it("executeTakeFirst() returns the first row with date parsed", async () => {
		const row = await withParsedDates(
			makeFakeQuery([
				{ items: [{ created_at: "2024-01-01T00:00:00.000Z" }] },
				{ items: [{ created_at: "2099-01-01T00:00:00.000Z" }] },
			]),
			["items.created_at"],
		).executeTakeFirst();

		expect(row).toBeDefined();
		expect(row?.items[0]?.created_at).toBeInstanceOf(Date);
		expect(row?.items[0]?.created_at.toISOString()).toBe(
			"2024-01-01T00:00:00.000Z",
		);
	});

	it("executeTakeFirst() returns undefined when there are no rows", async () => {
		const row = await withParsedDates(
			makeFakeQuery<{ items: { created_at: string }[] }>([]),
			["items.created_at"],
		).executeTakeFirst();

		expect(row).toBeUndefined();
	});

	it("executeTakeFirstOrThrow() returns the first row with date parsed", async () => {
		const row = await withParsedDates(
			makeFakeQuery([{ items: [{ created_at: "2024-03-23T00:00:00.000Z" }] }]),
			["items.created_at"],
		).executeTakeFirstOrThrow();

		expect(row.items[0]?.created_at).toBeInstanceOf(Date);
		expect(row.items[0]?.created_at.toISOString()).toBe(
			"2024-03-23T00:00:00.000Z",
		);
	});

	it("executeTakeFirstOrThrow() throws NoResultError by default when empty", async () => {
		await expect(
			withParsedDates(makeFakeQuery<{ items: { created_at: string }[] }>([]), [
				"items.created_at",
			]).executeTakeFirstOrThrow(),
		).rejects.toBeInstanceOf(NoResultError);
	});

	it("executeTakeFirstOrThrow() throws a custom error class when provided", async () => {
		await expect(
			withParsedDates(makeFakeQuery<{ items: { created_at: string }[] }>([]), [
				"items.created_at",
			]).executeTakeFirstOrThrow(TestNoResultError),
		).rejects.toBeInstanceOf(TestNoResultError);
	});

	it("executeTakeFirstOrThrow() custom error has the expected message", async () => {
		await expect(
			withParsedDates(makeFakeQuery<{ items: { created_at: string }[] }>([]), [
				"items.created_at",
			]).executeTakeFirstOrThrow(TestNoResultError),
		).rejects.toThrow("No result (test)");
	});
});

// ---------------------------------------------------------------------------
// withParsedDates — type-level tests (compile-time only, no runtime assertions)
// ---------------------------------------------------------------------------

describe("withParsedDates – types", () => {
	it("transforms string → Date for a nested array field", () => {
		const fakeRows: { items: { created_at: string }[] }[] = [];
		const query = withParsedDates(makeFakeQuery(fakeRows), [
			"items.created_at",
		]);
		type Result = Awaited<ReturnType<typeof query.execute>>;
		expectTypeOf<Result>().toEqualTypeOf<{ items: { created_at: Date }[] }[]>();
	});

	it("preserves unrelated fields unchanged", () => {
		const fakeRows: {
			id: number;
			items: { created_at: string; name: string }[];
		}[] = [];
		const query = withParsedDates(makeFakeQuery(fakeRows), [
			"items.created_at",
		]);
		type Result = Awaited<ReturnType<typeof query.execute>>;
		expectTypeOf<Result>().toEqualTypeOf<
			{ id: number; items: { created_at: Date; name: string }[] }[]
		>();
	});

	it("executeTakeFirst() returns Row | undefined", () => {
		const fakeRows: { items: { created_at: string }[] }[] = [];
		const query = withParsedDates(makeFakeQuery(fakeRows), [
			"items.created_at",
		]);
		type Result = Awaited<ReturnType<typeof query.executeTakeFirst>>;
		expectTypeOf<Result>().toEqualTypeOf<
			{ items: { created_at: Date }[] } | undefined
		>();
	});

	it("executeTakeFirstOrThrow() returns Row (never undefined)", () => {
		const fakeRows: { items: { created_at: string }[] }[] = [];
		const query = withParsedDates(makeFakeQuery(fakeRows), [
			"items.created_at",
		]);
		type Result = Awaited<ReturnType<typeof query.executeTakeFirstOrThrow>>;
		expectTypeOf<Result>().toEqualTypeOf<{ items: { created_at: Date }[] }>();
	});

	it("WithParsedDates utility type transforms correctly", () => {
		type Input = { items: { created_at: string; id: number }[] };
		type Output = WithParsedDates<Input, ["items.created_at"]>;
		expectTypeOf<Output>().toEqualTypeOf<{
			items: { created_at: Date; id: number }[];
		}>();
	});

	it("WithParsedDates preserves null unions on the array field", () => {
		type Input = { items: { created_at: string }[] | null };
		type Output = WithParsedDates<Input, ["items.created_at"]>;
		expectTypeOf<Output>().toEqualTypeOf<{
			items: { created_at: Date }[] | null;
		}>();
	});

	it("type-checks against real DummyDriver Kysely instance", () => {
		const query = withParsedDates(
			db
				.selectFrom("box")
				.select((eb) => [
					jsonArrayFrom(
						eb
							.selectFrom("item")
							.selectAll()
							.whereRef("item.box_id", "=", "box.id"),
					).as("items"),
				]),
			["items.created_at"],
		);
		// created_at inside items must be Date, not string — compile-time check only
		type Result = Awaited<ReturnType<typeof query.execute>>;
		expectTypeOf<Result>().toExtend<{ items: { created_at: Date }[] }[]>();
	});
});
