import { beforeEach, describe, expect, it } from "bun:test";
import { eq } from "drizzle-orm";
import {
	globalItems,
	globalItemTags,
	items,
	tags,
} from "../../src/db/schema.ts";
import { seedGlobalItems } from "../../src/db/seed-global-items.ts";
import {
	bulkUpsertGlobalItems,
	getGlobalItemWithOwnerCount,
	searchGlobalItems,
	upsertGlobalItem,
} from "../../src/server/services/global-item.service.ts";
import { createTestDb } from "../helpers/db.ts";

type TestDb = Awaited<ReturnType<typeof createTestDb>>;

async function insertGlobalItem(
	db: TestDb["db"],
	data: {
		brand: string;
		model: string;
		category?: string;
		weightGrams?: number;
		priceCents?: number;
	},
) {
	const [row] = await db
		.insert(globalItems)
		.values({
			brand: data.brand,
			model: data.model,
			category: data.category ?? null,
			weightGrams: data.weightGrams ?? null,
			priceCents: data.priceCents ?? null,
		})
		.returning();
	return row;
}

async function insertItem(
	db: TestDb["db"],
	name: string,
	userId: number,
	opts?: { globalItemId?: number },
) {
	const [row] = await db
		.insert(items)
		.values({ name, categoryId: 1, userId, globalItemId: opts?.globalItemId })
		.returning();
	return row;
}

async function insertTag(db: TestDb["db"], name: string) {
	const [row] = await db.insert(tags).values({ name }).returning();
	return row;
}

async function tagGlobalItem(
	db: TestDb["db"],
	globalItemId: number,
	tagId: number,
) {
	await db.insert(globalItemTags).values({ globalItemId, tagId });
}

describe("Global Item Service", () => {
	let db: TestDb["db"];
	let userId: number;

	beforeEach(async () => {
		const testDb = await createTestDb();
		db = testDb.db;
		userId = testDb.userId;
	});

	describe("searchGlobalItems", () => {
		it("returns all global items when no query provided", async () => {
			await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			await insertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
			});

			const results = await searchGlobalItems(db);
			expect(results).toHaveLength(2);
		});

		it("returns items matching brand (case-insensitive)", async () => {
			await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			await insertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
			});

			const results = await searchGlobalItems(db, "revelate");
			expect(results).toHaveLength(1);
			expect(results[0].brand).toBe("Revelate Designs");
		});

		it("returns items matching model (case-insensitive)", async () => {
			await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			await insertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
			});

			const results = await searchGlobalItems(db, "HANDLEBAR");
			expect(results).toHaveLength(1);
			expect(results[0].model).toBe("Handlebar Pack");
		});

		it("does not match everything with wildcard chars", async () => {
			await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			await insertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
			});

			const results = await searchGlobalItems(db, "100%");
			expect(results).toHaveLength(0);
		});

		it("returns all items when no tags provided", async () => {
			await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			await insertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
			});

			const results = await searchGlobalItems(db, undefined, undefined);
			expect(results).toHaveLength(2);
		});

		it("filters by single tag", async () => {
			const gi1 = await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			const _gi2 = await insertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
			});

			const tag = await insertTag(db, "ultralight");
			await tagGlobalItem(db, gi1.id, tag.id);

			const results = await searchGlobalItems(db, undefined, ["ultralight"]);
			expect(results).toHaveLength(1);
			expect(results[0].brand).toBe("Revelate Designs");
		});

		it("filters by multiple tags with AND logic", async () => {
			const gi1 = await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			const gi2 = await insertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
			});

			const tagUL = await insertTag(db, "ultralight");
			const tagBP = await insertTag(db, "bikepacking");
			// gi1 has both tags
			await tagGlobalItem(db, gi1.id, tagUL.id);
			await tagGlobalItem(db, gi1.id, tagBP.id);
			// gi2 has only bikepacking
			await tagGlobalItem(db, gi2.id, tagBP.id);

			const results = await searchGlobalItems(db, undefined, [
				"ultralight",
				"bikepacking",
			]);
			expect(results).toHaveLength(1);
			expect(results[0].brand).toBe("Revelate Designs");
		});

		it("combines text search and tag filtering", async () => {
			const gi1 = await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
			});
			const gi2 = await insertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Spinelock",
			});

			const tag = await insertTag(db, "bikepacking");
			await tagGlobalItem(db, gi1.id, tag.id);
			await tagGlobalItem(db, gi2.id, tag.id);

			// Both tagged bikepacking, but only one matches "terrapin"
			const results = await searchGlobalItems(db, "terrapin", ["bikepacking"]);
			expect(results).toHaveLength(1);
			expect(results[0].model).toBe("Terrapin System");
		});
	});

	describe("getGlobalItemWithOwnerCount", () => {
		it("returns item with ownerCount 0 when no items reference it", async () => {
			const gi = await insertGlobalItem(db, {
				brand: "MSR",
				model: "PocketRocket 2",
			});

			const result = await getGlobalItemWithOwnerCount(db, gi.id);
			expect(result).not.toBeNull();
			expect(result!.ownerCount).toBe(0);
			expect(result!.brand).toBe("MSR");
		});

		it("returns ownerCount matching number of items with globalItemId", async () => {
			const gi = await insertGlobalItem(db, {
				brand: "MSR",
				model: "PocketRocket 2",
			});

			await insertItem(db, "My Stove", userId, { globalItemId: gi.id });
			await insertItem(db, "Another Stove", userId, {
				globalItemId: gi.id,
			});

			const result = await getGlobalItemWithOwnerCount(db, gi.id);
			expect(result).not.toBeNull();
			expect(result!.ownerCount).toBe(2);
		});

		it("returns null for non-existent id", async () => {
			const result = await getGlobalItemWithOwnerCount(db, 9999);
			expect(result).toBeNull();
		});
	});

	describe("seedGlobalItems", () => {
		it("inserts seed data on first call", async () => {
			await seedGlobalItems(db);
			const all = await db.select().from(globalItems);
			expect(all.length).toBeGreaterThan(0);
		});

		it("is idempotent on second call", async () => {
			await seedGlobalItems(db);
			const countAfterFirst = (await db.select().from(globalItems)).length;

			await seedGlobalItems(db);
			const countAfterSecond = (await db.select().from(globalItems)).length;

			expect(countAfterSecond).toBe(countAfterFirst);
		});
	});

	describe("upsert operations", () => {
		it("upsertGlobalItem creates new item and returns { item, created: true }", async () => {
			const result = await upsertGlobalItem(db, {
				brand: "Revelate Designs",
				model: "Terrapin System",
				category: "Bags",
				weightGrams: 210,
			});

			expect(result.created).toBe(true);
			expect(result.item.id).toBeDefined();
			expect(result.item.brand).toBe("Revelate Designs");
			expect(result.item.model).toBe("Terrapin System");
		});

		it("upsertGlobalItem updates existing item on (brand, model) conflict and returns { item, created: false }", async () => {
			await upsertGlobalItem(db, {
				brand: "MSR",
				model: "PocketRocket 2",
				weightGrams: 83,
			});

			const second = await upsertGlobalItem(db, {
				brand: "MSR",
				model: "PocketRocket 2",
				weightGrams: 90,
			});

			expect(second.created).toBe(false);
			expect(second.item.weightGrams).toBe(90);

			// Only one row should exist
			const all = await db.select().from(globalItems);
			expect(all).toHaveLength(1);
		});

		it("upsertGlobalItem persists sourceUrl, imageCredit, imageSourceUrl", async () => {
			const result = await upsertGlobalItem(db, {
				brand: "Apidura",
				model: "Handlebar Pack",
				sourceUrl: "https://apidura.com/shop/handlebar-pack/",
				imageCredit: "Apidura Ltd",
				imageSourceUrl: "https://apidura.com/images/handlebar-pack.jpg",
			});

			expect(result.item.sourceUrl).toBe(
				"https://apidura.com/shop/handlebar-pack/",
			);
			expect(result.item.imageCredit).toBe("Apidura Ltd");
			expect(result.item.imageSourceUrl).toBe(
				"https://apidura.com/images/handlebar-pack.jpg",
			);
		});

		it("upsertGlobalItem with tags creates tags and links them", async () => {
			const result = await upsertGlobalItem(db, {
				brand: "Therm-a-Rest",
				model: "NeoAir XLite",
				tags: ["sleeping-pad", "ultralight"],
			});

			expect(result.created).toBe(true);

			const linkedTags = await db
				.select({ name: tags.name })
				.from(globalItemTags)
				.innerJoin(tags, eq(globalItemTags.tagId, tags.id))
				.where(eq(globalItemTags.globalItemId, result.item.id));

			expect(linkedTags).toHaveLength(2);
			const tagNames = linkedTags.map((t) => t.name).sort();
			expect(tagNames).toEqual(["sleeping-pad", "ultralight"]);
		});

		it("upsertGlobalItem without tags leaves existing tags untouched", async () => {
			// Create item with tags
			const first = await upsertGlobalItem(db, {
				brand: "Sea to Summit",
				model: "Spark III",
				tags: ["sleeping-bag"],
			});

			// Upsert without tags
			await upsertGlobalItem(db, {
				brand: "Sea to Summit",
				model: "Spark III",
				weightGrams: 450,
			});

			// Tags should remain
			const linkedTags = await db
				.select()
				.from(globalItemTags)
				.where(eq(globalItemTags.globalItemId, first.item.id));

			expect(linkedTags).toHaveLength(1);
		});

		it("upsertGlobalItem with empty tags array clears existing tags", async () => {
			// Create item with tags
			const first = await upsertGlobalItem(db, {
				brand: "Big Agnes",
				model: "Copper Spur HV UL2",
				tags: ["tent", "ultralight"],
			});

			// Upsert with empty tags
			await upsertGlobalItem(db, {
				brand: "Big Agnes",
				model: "Copper Spur HV UL2",
				tags: [],
			});

			// Tags should be cleared
			const linkedTags = await db
				.select()
				.from(globalItemTags)
				.where(eq(globalItemTags.globalItemId, first.item.id));

			expect(linkedTags).toHaveLength(0);
		});

		it("bulkUpsertGlobalItems processes array and returns correct created/updated counts", async () => {
			const result = await bulkUpsertGlobalItems(db, [
				{ brand: "Petzl", model: "Actik Core", weightGrams: 87 },
				{ brand: "Black Diamond", model: "Spot 400", weightGrams: 95 },
				{ brand: "Black Diamond", model: "Spot 350", weightGrams: 90 },
			]);

			expect(result.created).toBe(3);
			expect(result.updated).toBe(0);
			expect(result.items).toHaveLength(3);
		});

		it("bulkUpsertGlobalItems handles mix of new and existing items", async () => {
			// Pre-insert one item
			await upsertGlobalItem(db, {
				brand: "Petzl",
				model: "Actik Core",
				weightGrams: 87,
			});

			const result = await bulkUpsertGlobalItems(db, [
				{ brand: "Petzl", model: "Actik Core", weightGrams: 90 }, // existing
				{ brand: "Black Diamond", model: "Spot 400", weightGrams: 95 }, // new
			]);

			expect(result.created).toBe(1);
			expect(result.updated).toBe(1);
			expect(result.items).toHaveLength(2);
		});
	});
});
