/**
 * Integration tests for EmDashClient.
 *
 * Tests full CRUD lifecycles against a mock HTTP backend that simulates
 * the real API behavior including _rev tokens, schema caching, and
 * content state transitions.
 */

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

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

// ---------------------------------------------------------------------------
// Simulated backend
// ---------------------------------------------------------------------------

const COLLECTION_MATCH_REGEX = /^\/schema\/collections\/([^/]+)$/;
const CONTENT_LIST_REGEX = /^\/content\/([^/]+)$/;
const CONTENT_ITEM_REGEX = /^\/content\/([^/]+)\/([^/]+)$/;
const CONTENT_ACTION_REGEX = /^\/content\/([^/]+)\/([^/]+)\/(publish|unpublish|schedule|restore)$/;

interface StoredItem {
	id: string;
	type: string;
	slug: string | null;
	status: string;
	data: Record<string, unknown>;
	authorId: string | null;
	createdAt: string;
	updatedAt: string;
	publishedAt: string | null;
	scheduledAt: string | null;
	liveRevisionId: string | null;
	draftRevisionId: string | null;
	version: number;
}

function encodeRev(item: StoredItem): string {
	return btoa(`${item.version}:${item.updatedAt}`);
}

/** Wraps body in `{ data: body }` to match the standard API response envelope. */
function jsonRes(body: unknown, status = 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" },
	});
}

/**
 * A stateful mock backend that simulates EmDash's REST API.
 * Supports schema, content CRUD, _rev tokens, and conflict detection.
 */
function createStatefulBackend() {
	const collections = new Map<
		string,
		{
			slug: string;
			label: string;
			labelSingular: string;
			fields: Array<{ slug: string; type: string; label: string; required?: boolean }>;
		}
	>();

	const content = new Map<string, StoredItem>();
	let idCounter = 0;

	// Seed a collection
	collections.set("posts", {
		slug: "posts",
		label: "Posts",
		labelSingular: "Post",
		fields: [
			{ slug: "title", type: "string", label: "Title", required: true },
			{ slug: "body", type: "portableText", label: "Body" },
			{ slug: "excerpt", type: "text", label: "Excerpt" },
		],
	});

	const interceptor: Interceptor = async (req) => {
		const url = new URL(req.url);
		const path = url.pathname.replace("/_emdash/api", "");

		// --- Schema routes ---

		if (req.method === "GET" && path === "/schema/collections") {
			return jsonRes({
				items: Array.from(collections.values(), ({ slug, label, labelSingular }) => ({
					slug,
					label,
					labelSingular,
					supports: [],
				})),
			});
		}

		const colMatch = path.match(COLLECTION_MATCH_REGEX);
		if (req.method === "GET" && colMatch) {
			const col = collections.get(colMatch[1]);
			if (!col) return jsonRes({ error: { code: "NOT_FOUND", message: "Not found" } }, 404);
			return jsonRes({ item: { ...col, supports: [] } });
		}

		// --- Manifest ---

		if (req.method === "GET" && path === "/manifest") {
			const cols: Record<string, unknown> = {};
			for (const [slug, col] of collections) {
				const fields: Record<string, unknown> = {};
				for (const f of col.fields) {
					fields[f.slug] = { kind: f.type, label: f.label, required: f.required };
				}
				cols[slug] = {
					label: col.label,
					labelSingular: col.labelSingular,
					supports: [],
					fields,
				};
			}
			return jsonRes({ version: "0.1.0", hash: "abc", collections: cols, plugins: {} });
		}

		// --- Content list ---

		const listMatch = path.match(CONTENT_LIST_REGEX);
		if (req.method === "GET" && listMatch) {
			const collectionSlug = listMatch[1];
			const status = url.searchParams.get("status");
			const items = [...content.values()]
				.filter((i) => i.type === collectionSlug)
				.filter((i) => !status || i.status === status);
			return jsonRes({ items, nextCursor: undefined });
		}

		// --- Content create ---

		if (req.method === "POST" && listMatch) {
			const collectionSlug = listMatch[1];
			const body = (await req.json()) as {
				data: Record<string, unknown>;
				slug?: string;
				status?: string;
			};
			const id = `item_${++idCounter}`;
			const now = new Date().toISOString();
			const item: StoredItem = {
				id,
				type: collectionSlug,
				slug: body.slug ?? null,
				status: body.status ?? "draft",
				data: body.data,
				authorId: null,
				createdAt: now,
				updatedAt: now,
				publishedAt: null,
				scheduledAt: null,
				liveRevisionId: null,
				draftRevisionId: null,
				version: 1,
			};
			content.set(id, item);
			return jsonRes({ item, _rev: encodeRev(item) });
		}

		// --- Content get/update/delete ---

		const itemMatch = path.match(CONTENT_ITEM_REGEX);
		if (itemMatch) {
			const itemId = itemMatch[2];
			const item = content.get(itemId);

			if (req.method === "GET") {
				if (!item) return jsonRes({ error: { code: "NOT_FOUND", message: "Not found" } }, 404);
				return jsonRes({ item, _rev: encodeRev(item) });
			}

			if (req.method === "PUT") {
				if (!item) return jsonRes({ error: { code: "NOT_FOUND", message: "Not found" } }, 404);

				const body = (await req.json()) as {
					data?: Record<string, unknown>;
					slug?: string;
					status?: string;
					_rev?: string;
				};

				// Check _rev for conflict
				if (body._rev) {
					const expected = encodeRev(item);
					if (body._rev !== expected) {
						return jsonRes(
							{
								error: {
									code: "CONFLICT",
									message: "Entry has been modified since last read",
								},
							},
							409,
						);
					}
				}

				// Apply updates
				if (body.data) item.data = { ...item.data, ...body.data };
				if (body.slug !== undefined) item.slug = body.slug;
				if (body.status) item.status = body.status;
				item.updatedAt = new Date().toISOString();
				item.version++;

				return jsonRes({ item, _rev: encodeRev(item) });
			}

			if (req.method === "DELETE") {
				if (!item) return jsonRes({ error: { code: "NOT_FOUND", message: "Not found" } }, 404);
				item.status = "trashed";
				item.updatedAt = new Date().toISOString();
				return jsonRes({});
			}
		}

		// --- Content actions ---

		const actionMatch = path.match(CONTENT_ACTION_REGEX);
		if (req.method === "POST" && actionMatch) {
			const itemId = actionMatch[2];
			const action = actionMatch[3];
			const item = content.get(itemId);

			if (!item) return jsonRes({ error: { code: "NOT_FOUND", message: "Not found" } }, 404);

			switch (action) {
				case "publish":
					item.status = "published";
					item.publishedAt = new Date().toISOString();
					break;
				case "unpublish":
					item.status = "draft";
					item.publishedAt = null;
					break;
				case "schedule": {
					const body = (await req.json()) as { scheduledAt: string };
					item.scheduledAt = body.scheduledAt;
					break;
				}
				case "restore":
					item.status = "draft";
					break;
			}

			item.updatedAt = new Date().toISOString();
			return jsonRes({});
		}

		// --- Search ---

		if (req.method === "GET" && path === "/search") {
			const q = url.searchParams.get("q") ?? "";
			const items = [...content.values()]
				.filter((i) => JSON.stringify(i.data).toLowerCase().includes(q.toLowerCase()))
				.map((i) => ({
					id: i.id,
					collection: i.type,
					title: typeof i.data.title === "string" ? i.data.title : "",
					score: 1,
				}));
			return jsonRes({ items });
		}

		return jsonRes(
			{ error: { code: "NOT_FOUND", message: `No route: ${req.method} ${path}` } },
			404,
		);
	};

	return { interceptor, collections, content };
}

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

describe("EmDashClient lifecycle (integration)", () => {
	function createClient() {
		const { interceptor, content } = createStatefulBackend();
		const client = new EmDashClient({
			baseUrl: "http://localhost:4321",
			token: "test",
			interceptors: [interceptor],
		});
		return { client, content };
	}

	it("full content CRUD lifecycle", async () => {
		const { client } = createClient();

		// Create
		const created = await client.create("posts", {
			data: { title: "My Post", body: "Hello **world**" },
			slug: "my-post",
			status: "draft",
		});
		expect(created.id).toBeDefined();
		expect(created.slug).toBe("my-post");
		expect(created.status).toBe("draft");
		// body was converted from markdown to PT
		expect(Array.isArray(created.data.body)).toBe(true);

		// List
		const list = await client.list("posts");
		expect(list.items).toHaveLength(1);
		expect(list.items[0].id).toBe(created.id);

		// Get — returns _rev for optimistic concurrency
		const fetched = await client.get("posts", created.id);
		expect(fetched.id).toBe(created.id);
		expect(typeof fetched.data.body).toBe("string"); // PT -> markdown
		expect(fetched.data.body).toContain("world");
		expect(fetched._rev).toBeDefined();

		// Update with explicit _rev
		const updated = await client.update("posts", created.id, {
			data: { title: "Updated Title" },
			_rev: fetched._rev,
		});
		expect(updated.data.title).toBe("Updated Title");

		// Publish
		await client.publish("posts", created.id);

		// List published
		const published = await client.list("posts", { status: "published" });
		expect(published.items).toHaveLength(1);

		// Unpublish
		await client.unpublish("posts", created.id);

		// Delete (soft)
		await client.delete("posts", created.id);
	});

	it("blind update succeeds without _rev", async () => {
		const { client } = createClient();

		const item = await client.create("posts", {
			data: { title: "Test" },
		});

		// Update without reading — blind write (no _rev) should succeed
		const updated = await client.update("posts", item.id, {
			data: { title: "Blind Write OK" },
		});
		expect(updated.data.title).toBe("Blind Write OK");
	});

	it("get() returns _rev and update() accepts it for conflict detection", async () => {
		const { client } = createClient();

		const item = await client.create("posts", {
			data: { title: "Test" },
		});

		// Read — should return _rev on the item
		const fetched = await client.get("posts", item.id);
		expect(fetched._rev).toBeDefined();

		// Update with explicit _rev
		const updated = await client.update("posts", item.id, {
			data: { title: "Safe Update" },
			_rev: fetched._rev,
		});
		expect(updated.data.title).toBe("Safe Update");
	});

	it("multiple sequential updates work with explicit _rev", async () => {
		const { client } = createClient();

		const item = await client.create("posts", {
			data: { title: "V1" },
		});

		// First read
		const v1 = await client.get("posts", item.id);

		// First update with _rev
		await client.update("posts", item.id, {
			data: { title: "V2" },
			_rev: v1._rev,
		});

		// Re-read for fresh _rev (previous rev is now stale)
		const v2 = await client.get("posts", item.id);

		// Second update with new _rev
		const v3 = await client.update("posts", item.id, {
			data: { title: "V3" },
			_rev: v2._rev,
		});
		expect(v3.data.title).toBe("V3");
	});

	it("listAll() iterates through all items", async () => {
		const { client } = createClient();

		// Create multiple items
		await client.create("posts", { data: { title: "A" } });
		await client.create("posts", { data: { title: "B" } });
		await client.create("posts", { data: { title: "C" } });

		const all = [];
		for await (const item of client.listAll("posts")) {
			all.push(item);
		}
		expect(all).toHaveLength(3);
	});

	it("schedule() sets scheduling metadata", async () => {
		const { client } = createClient();

		const item = await client.create("posts", { data: { title: "Scheduled" } });
		await client.schedule("posts", item.id, { at: "2026-06-01T09:00:00Z" });

		// Verify via get
		const fetched = await client.get("posts", item.id);
		expect(fetched.scheduledAt).toBe("2026-06-01T09:00:00Z");
	});

	it("search() finds matching content", async () => {
		const { client } = createClient();

		await client.create("posts", { data: { title: "Deployment Guide" } });
		await client.create("posts", { data: { title: "Getting Started" } });

		const results = await client.search("deployment");
		expect(results).toHaveLength(1);
		expect(results[0].title).toBe("Deployment Guide");
	});

	it("schema operations work", async () => {
		const { client } = createClient();

		const cols = await client.collections();
		expect(cols.length).toBeGreaterThan(0);
		expect(cols[0].slug).toBe("posts");

		const col = await client.collection("posts");
		expect(col.fields).toHaveLength(3);
		expect(col.fields[0].slug).toBe("title");
	});

	it("manifest() returns full schema", async () => {
		const { client } = createClient();

		const manifest = await client.manifest();
		expect(manifest.version).toBe("0.1.0");
		expect(manifest.collections.posts).toBeDefined();
		expect(manifest.collections.posts.fields.title).toBeDefined();
	});

	it("API errors are typed correctly", async () => {
		const { client } = createClient();

		try {
			await client.get("posts", "nonexistent");
			expect.fail("Should have thrown");
		} catch (error) {
			expect(error).toBeInstanceOf(EmDashApiError);
			const apiErr = error as EmDashApiError;
			expect(apiErr.status).toBe(404);
			expect(apiErr.code).toBe("NOT_FOUND");
		}
	});

	it("PT conversion round-trips through create and get", async () => {
		const { client } = createClient();

		// Create with markdown
		const item = await client.create("posts", {
			data: {
				title: "Markdown Post",
				body: "# Hello\n\nSome **bold** text\n\n- Item 1\n- Item 2",
			},
		});

		// Data stored as PT
		expect(Array.isArray(item.data.body)).toBe(true);

		// Get returns markdown
		const fetched = await client.get("posts", item.id);
		expect(typeof fetched.data.body).toBe("string");
		const body = fetched.data.body as string;
		expect(body).toContain("# Hello");
		expect(body).toContain("**bold**");
		expect(body).toContain("- Item 1");
	});
});
