import { describe, it, expect } from "vitest";

import { EmDashClient, EmDashApiError } from "../../../src/client/index.js";
import type { Interceptor } from "../../../src/client/transport.js";

// Regex patterns for route matching
const CONTENT_POSTS_ABC_REGEX = /\/content\/posts\/abc/;

// ---------------------------------------------------------------------------
// Mock backend
// ---------------------------------------------------------------------------

interface MockRoute {
	method: string;
	path: RegExp | string;
	handler: (req: Request) => Response | Promise<Response>;
}

/**
 * Creates a mock HTTP backend as an interceptor.
 * Routes are matched in order. Unmatched requests return 404.
 */
function createMockBackend(routes: MockRoute[]): Interceptor {
	return async (req) => {
		const url = new URL(req.url);
		const path = url.pathname + url.search;

		for (const route of routes) {
			if (req.method !== route.method) continue;
			if (typeof route.path === "string") {
				if (!path.includes(route.path)) continue;
			} else {
				if (!route.path.test(path)) continue;
			}
			return route.handler(req);
		}

		return new Response(
			JSON.stringify({ error: { code: "NOT_FOUND", message: "No matching route" } }),
			{ status: 404, headers: { "Content-Type": "application/json" } },
		);
	};
}

/** Wraps body in `{ data: body }` to match the standard API response envelope. */
function jsonResponse(body: unknown, status: number = 200): Response {
	// Error responses (4xx/5xx) are NOT wrapped in { data }
	const payload = status >= 400 ? body : { data: body };
	return new Response(JSON.stringify(payload), {
		status,
		headers: { "Content-Type": "application/json" },
	});
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("EmDashClient", () => {
	describe("_rev token flow", () => {
		it("blind update (no _rev) succeeds", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections/posts",
					handler: () =>
						jsonResponse({
							item: {
								slug: "posts",
								label: "Posts",
								fields: [{ slug: "title", type: "string", label: "Title" }],
							},
						}),
				},
				{
					method: "PUT",
					path: CONTENT_POSTS_ABC_REGEX,
					handler: async (req) => {
						const body = (await req.json()) as Record<string, unknown>;
						// No _rev should be sent
						expect(body._rev).toBeUndefined();
						return jsonResponse({
							item: { id: "abc", data: { title: "Blind" } },
							_rev: "newrev",
						});
					},
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const updated = await client.update("posts", "abc", {
				data: { title: "Blind" },
			});
			expect(updated.data.title).toBe("Blind");
		});

		it("get() returns _rev on the item", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections/posts",
					handler: () =>
						jsonResponse({
							item: {
								slug: "posts",
								label: "Posts",
								fields: [{ slug: "title", type: "string", label: "Title" }],
							},
						}),
				},
				{
					method: "GET",
					path: CONTENT_POSTS_ABC_REGEX,
					handler: () =>
						jsonResponse({
							item: {
								id: "abc",
								type: "posts",
								slug: "hello",
								status: "draft",
								data: { title: "Hello" },
								authorId: null,
								createdAt: "2026-01-01",
								updatedAt: "2026-01-01",
								publishedAt: null,
								scheduledAt: null,
								liveRevisionId: null,
								draftRevisionId: null,
							},
							_rev: "dGVzdHJldg",
						}),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const post = await client.get("posts", "abc");
			expect(post.id).toBe("abc");
			expect(post._rev).toBe("dGVzdHJldg");
		});

		it("update() sends _rev when provided", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections/posts",
					handler: () =>
						jsonResponse({
							item: {
								slug: "posts",
								label: "Posts",
								fields: [{ slug: "title", type: "string", label: "Title" }],
							},
						}),
				},
				{
					method: "PUT",
					path: CONTENT_POSTS_ABC_REGEX,
					handler: async (req) => {
						const body = await req.json();
						expect((body as Record<string, unknown>)._rev).toBe("dGVzdHJldg");
						return jsonResponse({
							item: { id: "abc", data: { title: "Updated" } },
							_rev: "bmV3cmV2",
						});
					},
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const updated = await client.update("posts", "abc", {
				data: { title: "Updated" },
				_rev: "dGVzdHJldg",
			});
			expect(updated.data.title).toBe("Updated");
			expect(updated._rev).toBe("bmV3cmV2");
		});
	});

	describe("create()", () => {
		it("does not require a prior get()", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections/posts",
					handler: () =>
						jsonResponse({
							item: {
								slug: "posts",
								label: "Posts",
								fields: [{ slug: "title", type: "string", label: "Title" }],
							},
						}),
				},
				{
					method: "POST",
					path: "/content/posts",
					handler: () =>
						jsonResponse({
							item: {
								id: "new1",
								type: "posts",
								slug: "hello",
								status: "draft",
								data: { title: "Hello" },
								authorId: null,
								createdAt: "2026-01-01",
								updatedAt: "2026-01-01",
								publishedAt: null,
								scheduledAt: null,
								liveRevisionId: null,
								draftRevisionId: null,
							},
						}),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const item = await client.create("posts", {
				data: { title: "Hello" },
				slug: "hello",
			});
			expect(item.id).toBe("new1");
		});
	});

	describe("API error handling", () => {
		it("throws EmDashApiError on 4xx responses", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections",
					handler: () => jsonResponse({ error: { code: "FORBIDDEN", message: "No access" } }, 403),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			try {
				await client.collections();
				expect.fail("Should have thrown");
			} catch (error) {
				expect(error).toBeInstanceOf(EmDashApiError);
				const apiErr = error as EmDashApiError;
				expect(apiErr.status).toBe(403);
				expect(apiErr.code).toBe("FORBIDDEN");
				expect(apiErr.message).toBe("No access");
			}
		});

		it("throws EmDashApiError on 500 responses", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/manifest",
					handler: () =>
						jsonResponse(
							{
								error: {
									code: "INTERNAL_ERROR",
									message: "Something broke",
								},
							},
							500,
						),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			try {
				await client.manifest();
				expect.fail("Should have thrown");
			} catch (error) {
				expect(error).toBeInstanceOf(EmDashApiError);
				expect((error as EmDashApiError).status).toBe(500);
			}
		});
	});

	describe("list()", () => {
		it("returns items and nextCursor", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/content/posts",
					handler: () =>
						jsonResponse({
							items: [
								{
									id: "1",
									type: "posts",
									slug: "a",
									status: "published",
									data: {},
								},
								{
									id: "2",
									type: "posts",
									slug: "b",
									status: "published",
									data: {},
								},
							],
							nextCursor: "cursor123",
						}),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const result = await client.list("posts", { status: "published" });
			expect(result.items).toHaveLength(2);
			expect(result.nextCursor).toBe("cursor123");
		});
	});

	describe("listAll()", () => {
		it("follows cursors until exhaustion", async () => {
			let page = 0;
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/content/posts",
					handler: () => {
						page++;
						if (page === 1) {
							return jsonResponse({
								items: [{ id: "1", data: {} }],
								nextCursor: "page2",
							});
						}
						return jsonResponse({
							items: [{ id: "2", data: {} }],
						});
					},
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const all = [];
			for await (const item of client.listAll("posts")) {
				all.push(item);
			}
			expect(all).toHaveLength(2);
			expect(all[0]?.id).toBe("1");
			expect(all[1]?.id).toBe("2");
		});
	});

	describe("delete/publish/unpublish/schedule/restore", () => {
		it("calls the correct endpoints", async () => {
			const calledPaths: string[] = [];

			const backend: Interceptor = async (req) => {
				calledPaths.push(`${req.method} ${new URL(req.url).pathname}`);
				return jsonResponse({});
			};

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			await client.delete("posts", "abc");
			await client.publish("posts", "abc");
			await client.unpublish("posts", "abc");
			await client.schedule("posts", "abc", { at: "2026-03-01T00:00:00Z" });
			await client.restore("posts", "abc");

			expect(calledPaths).toEqual([
				"DELETE /_emdash/api/content/posts/abc",
				"POST /_emdash/api/content/posts/abc/publish",
				"POST /_emdash/api/content/posts/abc/unpublish",
				"POST /_emdash/api/content/posts/abc/schedule",
				"POST /_emdash/api/content/posts/abc/restore",
			]);
		});
	});

	describe("schema methods", () => {
		it("collections() returns list", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections",
					handler: () =>
						jsonResponse({
							items: [
								{ slug: "posts", label: "Posts", supports: [] },
								{ slug: "pages", label: "Pages", supports: [] },
							],
						}),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const cols = await client.collections();
			expect(cols).toHaveLength(2);
			expect(cols[0]?.slug).toBe("posts");
		});

		it("createCollection() sends correct payload", async () => {
			let capturedBody: unknown;
			const backend = createMockBackend([
				{
					method: "POST",
					path: "/schema/collections",
					handler: async (req) => {
						capturedBody = await req.json();
						return jsonResponse({
							item: {
								slug: "events",
								label: "Events",
								labelSingular: "Event",
							},
						});
					},
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			await client.createCollection({
				slug: "events",
				label: "Events",
				labelSingular: "Event",
			});

			expect(capturedBody).toEqual({
				slug: "events",
				label: "Events",
				labelSingular: "Event",
			});
		});
	});

	describe("PT <-> Markdown auto-conversion", () => {
		it("converts PT fields to markdown on get()", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections/posts",
					handler: () =>
						jsonResponse({
							item: {
								slug: "posts",
								label: "Posts",
								fields: [
									{ slug: "title", type: "string", label: "Title" },
									{ slug: "body", type: "portableText", label: "Body" },
								],
							},
						}),
				},
				{
					method: "GET",
					path: CONTENT_POSTS_ABC_REGEX,
					handler: () =>
						jsonResponse({
							item: {
								id: "abc",
								type: "posts",
								data: {
									title: "Hello",
									body: [
										{
											_type: "block",
											style: "normal",
											markDefs: [],
											children: [
												{
													_type: "span",
													text: "World",
													marks: [],
												},
											],
										},
									],
								},
							},
							_rev: "rev1",
						}),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const item = await client.get("posts", "abc");
			expect(item.data.title).toBe("Hello");
			expect(typeof item.data.body).toBe("string");
			expect(item.data.body).toContain("World");
		});

		it("returns raw PT when raw: true", async () => {
			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections/posts",
					handler: () =>
						jsonResponse({
							item: {
								slug: "posts",
								fields: [{ slug: "body", type: "portableText", label: "Body" }],
							},
						}),
				},
				{
					method: "GET",
					path: CONTENT_POSTS_ABC_REGEX,
					handler: () =>
						jsonResponse({
							item: {
								id: "abc",
								data: {
									body: [
										{
											_type: "block",
											children: [{ _type: "span", text: "Raw" }],
										},
									],
								},
							},
							_rev: "rev1",
						}),
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			const item = await client.get("posts", "abc", { raw: true });
			expect(Array.isArray(item.data.body)).toBe(true);
		});

		it("converts markdown to PT on create()", async () => {
			let capturedData: Record<string, unknown> | undefined;

			const backend = createMockBackend([
				{
					method: "GET",
					path: "/schema/collections/posts",
					handler: () =>
						jsonResponse({
							item: {
								slug: "posts",
								fields: [
									{ slug: "title", type: "string", label: "Title" },
									{ slug: "body", type: "portableText", label: "Body" },
								],
							},
						}),
				},
				{
					method: "POST",
					path: "/content/posts",
					handler: async (req) => {
						const body = (await req.json()) as Record<string, unknown>;
						capturedData = body.data as Record<string, unknown>;
						return jsonResponse({
							item: {
								id: "new1",
								data: capturedData,
							},
						});
					},
				},
			]);

			const client = new EmDashClient({
				baseUrl: "http://localhost:4321",
				token: "test",
				interceptors: [backend],
			});

			await client.create("posts", {
				data: {
					title: "Hello",
					body: "Some **bold** text",
				},
			});

			expect(capturedData).toBeDefined();
			expect(capturedData!.title).toBe("Hello");
			expect(Array.isArray(capturedData!.body)).toBe(true);
		});
	});
});
