import { convexTest } from "convex-test";
import { describe, it, expect } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";

const modules = import.meta.glob("./**/*.ts");

async function setupAdminUser(t: ReturnType<typeof convexTest>) {
  const asAdmin = t.withIdentity({
    name: "Admin",
    email: "admin@example.com",
    subject: "clerk_admin_123",
  });
  const userId = await asAdmin.mutation(api.users.store, {});
  await t.run(async (ctx) => {
    await ctx.db.patch(userId, { role: "admin" });
  });
  return asAdmin;
}

async function setupCategory(t: ReturnType<typeof convexTest>) {
  let categoryId: any;
  await t.run(async (ctx) => {
    categoryId = await ctx.db.insert("categories", {
      name: "Pet Food",
      slug: "pet-food",
    });
  });
  return categoryId;
}

describe("products", () => {
  it("list returns empty page when no products", async () => {
    const t = convexTest(schema, modules);
    const result = await t.query(api.products.list, {
      paginationOpts: { numItems: 10, cursor: null },
    });
    expect(result.page).toHaveLength(0);
  });

  it("list returns only active products when filtered by status", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    await asAdmin.mutation(api.products.create, {
      name: "Active Product",
      slug: "active-product",
      status: "active",
      categoryId,
      tags: [],
    });
    await asAdmin.mutation(api.products.create, {
      name: "Draft Product",
      slug: "draft-product",
      status: "draft",
      categoryId,
      tags: [],
    });

    const result = await t.query(api.products.list, {
      paginationOpts: { numItems: 10, cursor: null },
      status: "active",
    });
    expect(result.page).toHaveLength(1);
    expect(result.page[0].name).toBe("Active Product");
  });

  it("getBySlug returns product for valid slug", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    await asAdmin.mutation(api.products.create, {
      name: "Dog Treats",
      slug: "dog-treats",
      status: "active",
      categoryId,
      tags: ["dogs"],
    });

    const product = await t.query(api.products.getBySlug, {
      slug: "dog-treats",
    });
    expect(product).not.toBeNull();
    expect(product?.name).toBe("Dog Treats");
  });

  it("getBySlug returns null for non-existent slug", async () => {
    const t = convexTest(schema, modules);
    const product = await t.query(api.products.getBySlug, {
      slug: "does-not-exist",
    });
    expect(product).toBeNull();
  });

  it("create throws for non-admin users", async () => {
    const t = convexTest(schema, modules);
    const categoryId = await setupCategory(t);

    const asCustomer = t.withIdentity({
      name: "Customer",
      email: "customer@example.com",
      subject: "clerk_customer_123",
    });
    await asCustomer.mutation(api.users.store, {});

    await expect(
      asCustomer.mutation(api.products.create, {
        name: "Illegal Product",
        slug: "illegal-product",
        status: "active",
        categoryId,
        tags: [],
      }),
    ).rejects.toThrow("Unauthorized: admin access required");
  });

  it("create succeeds for admin users", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    const productId = await asAdmin.mutation(api.products.create, {
      name: "Cat Toy",
      slug: "cat-toy",
      status: "draft",
      categoryId,
      tags: ["cats"],
    });
    expect(productId).toBeTruthy();
  });

  it("archive changes product status to archived", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    const productId = await asAdmin.mutation(api.products.create, {
      name: "Old Product",
      slug: "old-product",
      status: "active",
      categoryId,
      tags: [],
    });

    await asAdmin.mutation(api.products.archive, { id: productId });

    const product = await t.query(api.products.getBySlug, {
      slug: "old-product",
    });
    expect(product).toBeNull();
  });

  it("addVariant creates variant linked to product", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    const productId = await asAdmin.mutation(api.products.create, {
      name: "Variant Product",
      slug: "variant-product",
      status: "active",
      categoryId,
      tags: [],
    });

    const variantId = await asAdmin.mutation(api.products.addVariant, {
      productId,
      name: "Small",
      sku: "VAR-SM-001",
      price: 999,
      stockQuantity: 10,
      isActive: true,
    });
    expect(variantId).toBeTruthy();

    const product = await t.query(api.products.getBySlug, {
      slug: "variant-product",
    });
    expect(product).not.toBeNull();
    expect(product?.variants).toHaveLength(1);
    expect(product?.variants[0].name).toBe("Small");
    expect(product?.variants[0].sku).toBe("VAR-SM-001");
    expect(product?.variants[0].price).toBe(999);
  });

  it("updateVariant changes variant price", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    const productId = await asAdmin.mutation(api.products.create, {
      name: "Update Variant Product",
      slug: "update-variant-product",
      status: "active",
      categoryId,
      tags: [],
    });
    const variantId = await asAdmin.mutation(api.products.addVariant, {
      productId,
      name: "Medium",
      sku: "VAR-MD-001",
      price: 1999,
      stockQuantity: 5,
      isActive: true,
    });

    await asAdmin.mutation(api.products.updateVariant, {
      id: variantId,
      price: 1499,
    });

    const product = await t.query(api.products.getBySlug, {
      slug: "update-variant-product",
    });
    expect(product?.variants[0].price).toBe(1499);
  });

  it("addImage inserts image with correct position", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    const productId = await asAdmin.mutation(api.products.create, {
      name: "Image Product",
      slug: "image-product",
      status: "active",
      categoryId,
      tags: [],
    });

    await asAdmin.mutation(api.products.addImage, {
      productId,
      url: "https://example.com/first.jpg",
      alt: "First image",
      position: 1,
    });
    await asAdmin.mutation(api.products.addImage, {
      productId,
      url: "https://example.com/second.jpg",
      alt: "Second image",
      position: 0,
    });

    const product = await t.query(api.products.getBySlug, {
      slug: "image-product",
    });
    expect(product?.images).toHaveLength(2);
    expect(product?.images[0].position).toBe(0);
    expect(product?.images[0].url).toBe("https://example.com/second.jpg");
    expect(product?.images[1].position).toBe(1);
    expect(product?.images[1].url).toBe("https://example.com/first.jpg");
  });

  it("reorderImages updates position values", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    const productId = await asAdmin.mutation(api.products.create, {
      name: "Reorder Product",
      slug: "reorder-product",
      status: "active",
      categoryId,
      tags: [],
    });

    const id1 = await asAdmin.mutation(api.products.addImage, {
      productId,
      url: "https://example.com/a.jpg",
      position: 0,
    });
    const id2 = await asAdmin.mutation(api.products.addImage, {
      productId,
      url: "https://example.com/b.jpg",
      position: 1,
    });

    await asAdmin.mutation(api.products.reorderImages, {
      updates: [
        { id: id1, position: 1 },
        { id: id2, position: 0 },
      ],
    });

    const product = await t.query(api.products.getBySlug, {
      slug: "reorder-product",
    });
    expect(product?.images).toHaveLength(2);
    expect(product?.images[0].position).toBe(0);
    expect(product?.images[0].url).toBe("https://example.com/b.jpg");
    expect(product?.images[1].position).toBe(1);
    expect(product?.images[1].url).toBe("https://example.com/a.jpg");
  });

  it("search returns products matching query string", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    await asAdmin.mutation(api.products.create, {
      name: "Dog Treats",
      slug: "dog-treats",
      status: "active",
      categoryId,
      tags: [],
    });
    await asAdmin.mutation(api.products.create, {
      name: "Cat Food",
      slug: "cat-food",
      status: "active",
      categoryId,
      tags: [],
    });

    const result = await t.query(api.products.search, { query: "Dog" });
    expect(result).toHaveLength(1);
    expect(result[0].name).toContain("Dog");
  });

  it("search respects status filter", async () => {
    const t = convexTest(schema, modules);
    const asAdmin = await setupAdminUser(t);
    const categoryId = await setupCategory(t);

    await asAdmin.mutation(api.products.create, {
      name: "Active Treats",
      slug: "active-treats",
      status: "active",
      categoryId,
      tags: [],
    });
    await asAdmin.mutation(api.products.create, {
      name: "Draft Treats",
      slug: "draft-treats",
      status: "draft",
      categoryId,
      tags: [],
    });

    const result = await t.query(api.products.search, {
      query: "Treats",
      status: "active",
    });
    expect(result).toHaveLength(1);
    expect(result[0].name).toBe("Active Treats");
  });

  it("search with no matches returns empty array", async () => {
    const t = convexTest(schema, modules);
    const result = await t.query(api.products.search, {
      query: "xyznonexistent",
    });
    expect(result).toEqual([]);
  });

  describe("searchTypeahead", () => {
    async function setupHierarchicalCategories(t: ReturnType<typeof convexTest>) {
      let dogsChildCategoryId: any;
      let catsChildCategoryId: any;
      await t.run(async (ctx) => {
        const dogsId = await ctx.db.insert("categories", {
          name: "Dogs",
          slug: "dogs",
        });
        dogsChildCategoryId = await ctx.db.insert("categories", {
          name: "Dog Food",
          slug: "dog-food",
          parentId: dogsId,
        });
        const catsId = await ctx.db.insert("categories", {
          name: "Cats",
          slug: "cats",
        });
        catsChildCategoryId = await ctx.db.insert("categories", {
          name: "Cat Food",
          slug: "cat-food",
          parentId: catsId,
        });
      });
      return { dogsChildCategoryId, catsChildCategoryId };
    }

    it("returns empty array for query shorter than 3 characters", async () => {
      const t = convexTest(schema, modules);
      const result = await t.query(api.products.searchTypeahead, { query: "do" });
      expect(result).toEqual([]);
    });

    it("returns empty array for query of exactly 2 characters", async () => {
      const t = convexTest(schema, modules);
      const result = await t.query(api.products.searchTypeahead, { query: "ab" });
      expect(result).toEqual([]);
    });

    it("returns empty array when no products match the query", async () => {
      const t = convexTest(schema, modules);
      const result = await t.query(api.products.searchTypeahead, {
        query: "xyznonexistent",
      });
      expect(result).toEqual([]);
    });

    it("returns active products matching the query", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId } = await setupHierarchicalCategories(t);

      await asAdmin.mutation(api.products.create, {
        name: "Premium Dog Kibble",
        slug: "premium-dog-kibble",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Cat Litter Deluxe",
        slug: "cat-litter-deluxe",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Kibble",
      });
      expect(result).toHaveLength(1);
      expect(result[0].name).toBe("Premium Dog Kibble");
    });

    it("does not return draft or archived products", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId } = await setupHierarchicalCategories(t);

      await asAdmin.mutation(api.products.create, {
        name: "Active Bone Treat",
        slug: "active-bone-treat",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Draft Bone Treat",
        slug: "draft-bone-treat",
        status: "draft",
        categoryId: dogsChildCategoryId,
        tags: [],
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Bone",
      });
      expect(result).toHaveLength(1);
      expect(result[0].name).toBe("Active Bone Treat");
    });

    it("filters by parentCategorySlug when provided", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId, catsChildCategoryId } =
        await setupHierarchicalCategories(t);

      await asAdmin.mutation(api.products.create, {
        name: "Royal Canin Dog Food",
        slug: "royal-canin-dog-food",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Royal Canin Cat Food",
        slug: "royal-canin-cat-food",
        status: "active",
        categoryId: catsChildCategoryId,
        tags: [],
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Royal Canin",
        parentCategorySlug: "cats",
      });
      expect(result).toHaveLength(1);
      expect(result[0].name).toBe("Royal Canin Cat Food");
      expect(result[0].parentCategorySlug).toBe("cats");
    });

    it("returns all matching products when no category filter is provided", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId, catsChildCategoryId } =
        await setupHierarchicalCategories(t);

      await asAdmin.mutation(api.products.create, {
        name: "Pro Plan Dog Food",
        slug: "pro-plan-dog-food",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Pro Plan Cat Food",
        slug: "pro-plan-cat-food",
        status: "active",
        categoryId: catsChildCategoryId,
        tags: [],
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Pro Plan",
      });
      expect(result).toHaveLength(2);
    });

    it("returns enriched data: imageUrl, minPrice, slug, and category slugs", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId } = await setupHierarchicalCategories(t);

      const productId = await asAdmin.mutation(api.products.create, {
        name: "Enriched Dog Treat",
        slug: "enriched-dog-treat",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.addImage, {
        productId,
        url: "https://example.com/treat.jpg",
        alt: "Dog Treat",
        position: 0,
      });
      await asAdmin.mutation(api.products.addVariant, {
        productId,
        name: "100g",
        sku: "TREAT-100G",
        price: 599,
        stockQuantity: 50,
        isActive: true,
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Enriched",
      });
      expect(result).toHaveLength(1);
      const item = result[0];
      expect(item.slug).toBe("enriched-dog-treat");
      expect(item.parentCategorySlug).toBe("dogs");
      expect(item.childCategorySlug).toBe("dog-food");
      expect(item.imageUrl).toBe("https://example.com/treat.jpg");
      expect(item.imageAlt).toBe("Dog Treat");
      expect(item.minPrice).toBe(599);
    });

    it("returns minPrice=0 when product has no active variants", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId } = await setupHierarchicalCategories(t);

      await asAdmin.mutation(api.products.create, {
        name: "Variantless Collar",
        slug: "variantless-collar",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Variantless",
      });
      expect(result).toHaveLength(1);
      expect(result[0].minPrice).toBe(0);
    });

    it("returns imageUrl as undefined when product has no images", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId } = await setupHierarchicalCategories(t);

      await asAdmin.mutation(api.products.create, {
        name: "Imageless Dog Toy",
        slug: "imageless-dog-toy",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Imageless",
      });
      expect(result).toHaveLength(1);
      expect(result[0].imageUrl).toBeUndefined();
    });

    it("picks the lowest priced active variant as minPrice", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId } = await setupHierarchicalCategories(t);

      const productId = await asAdmin.mutation(api.products.create, {
        name: "Multi-Variant Dog Food",
        slug: "multi-variant-dog-food",
        status: "active",
        categoryId: dogsChildCategoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.addVariant, {
        productId,
        name: "1kg",
        sku: "MV-1KG",
        price: 1200,
        stockQuantity: 10,
        isActive: true,
      });
      await asAdmin.mutation(api.products.addVariant, {
        productId,
        name: "500g",
        sku: "MV-500G",
        price: 799,
        stockQuantity: 20,
        isActive: true,
      });
      // Inactive variant with a lower price — should be ignored
      await asAdmin.mutation(api.products.addVariant, {
        productId,
        name: "2kg",
        sku: "MV-2KG",
        price: 199,
        stockQuantity: 5,
        isActive: false,
      });

      const result = await t.query(api.products.searchTypeahead, {
        query: "Multi-Variant",
      });
      expect(result).toHaveLength(1);
      expect(result[0].minPrice).toBe(799);
    });

    it("respects the limit parameter", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const { dogsChildCategoryId } = await setupHierarchicalCategories(t);

      for (let i = 0; i < 5; i++) {
        await asAdmin.mutation(api.products.create, {
          name: `Dog Snack ${i}`,
          slug: `dog-snack-${i}`,
          status: "active",
          categoryId: dogsChildCategoryId,
          tags: [],
        });
      }

      const result = await t.query(api.products.searchTypeahead, {
        query: "Dog Snack",
        limit: 3,
      });
      expect(result).toHaveLength(3);
    });
  });

  describe("listByTag", () => {
    it("returns only active products with the given tag", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const categoryId = await setupCategory(t);

      await asAdmin.mutation(api.products.create, {
        name: "Sale Product",
        slug: "sale-product",
        status: "active",
        categoryId,
        tags: ["sale", "dogs"],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Top Pick",
        slug: "top-pick",
        status: "active",
        categoryId,
        tags: ["top-picks"],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Regular Product",
        slug: "regular-product",
        status: "active",
        categoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Draft Sale",
        slug: "draft-sale",
        status: "draft",
        categoryId,
        tags: ["sale"],
      });

      const result = await t.query(api.products.listByTag, { tag: "sale" });
      expect(result).toHaveLength(1);
      expect(result[0].name).toBe("Sale Product");
    });

    it("returns empty array when no products match the tag", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const categoryId = await setupCategory(t);

      await asAdmin.mutation(api.products.create, {
        name: "Regular Product",
        slug: "regular-product",
        status: "active",
        categoryId,
        tags: ["dogs"],
      });

      const result = await t.query(api.products.listByTag, { tag: "sale" });
      expect(result).toHaveLength(0);
    });

    it("respects the limit arg", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const categoryId = await setupCategory(t);

      for (let i = 0; i < 5; i++) {
        await asAdmin.mutation(api.products.create, {
          name: `Sale Product ${i}`,
          slug: `sale-product-${i}`,
          status: "active",
          categoryId,
          tags: ["sale"],
        });
      }

      const result = await t.query(api.products.listByTag, {
        tag: "sale",
        limit: 3,
      });
      expect(result).toHaveLength(3);
    });

    it("returns products matching top-picks tag", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const categoryId = await setupCategory(t);

      await asAdmin.mutation(api.products.create, {
        name: "Top Pick A",
        slug: "top-pick-a",
        status: "active",
        categoryId,
        tags: ["top-picks"],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Top Pick B",
        slug: "top-pick-b",
        status: "active",
        categoryId,
        tags: ["top-picks", "sale"],
      });
      await asAdmin.mutation(api.products.create, {
        name: "No Pick",
        slug: "no-pick",
        status: "active",
        categoryId,
        tags: ["sale"],
      });

      const result = await t.query(api.products.listByTag, { tag: "top-picks" });
      expect(result).toHaveLength(2);
      const names = result.map((p: any) => p.name).sort();
      expect(names).toEqual(["Top Pick A", "Top Pick B"]);
    });
  });

  describe("listRecentlyAdded", () => {
    it("returns active products created within the last 30 days", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const categoryId = await setupCategory(t);

      // These will have _creationTime = now (within 30 days)
      await asAdmin.mutation(api.products.create, {
        name: "New Product A",
        slug: "new-product-a",
        status: "active",
        categoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.create, {
        name: "New Product B",
        slug: "new-product-b",
        status: "active",
        categoryId,
        tags: [],
      });
      await asAdmin.mutation(api.products.create, {
        name: "Draft New",
        slug: "draft-new",
        status: "draft",
        categoryId,
        tags: [],
      });

      const result = await t.query(api.products.listRecentlyAdded, {});
      // Only the two active products should appear; draft is excluded
      expect(result.length).toBeGreaterThanOrEqual(2);
      const names = result.map((p: any) => p.name);
      expect(names).toContain("New Product A");
      expect(names).toContain("New Product B");
      expect(names).not.toContain("Draft New");
    });

    it("respects the limit arg", async () => {
      const t = convexTest(schema, modules);
      const asAdmin = await setupAdminUser(t);
      const categoryId = await setupCategory(t);

      for (let i = 0; i < 5; i++) {
        await asAdmin.mutation(api.products.create, {
          name: `Product ${i}`,
          slug: `product-${i}`,
          status: "active",
          categoryId,
          tags: [],
        });
      }

      const result = await t.query(api.products.listRecentlyAdded, { limit: 3 });
      expect(result).toHaveLength(3);
    });

    it("returns empty array when no active products exist", async () => {
      const t = convexTest(schema, modules);
      const result = await t.query(api.products.listRecentlyAdded, {});
      expect(result).toHaveLength(0);
    });
  });
});
