import { Test } from '@nestjs/testing';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { PublicService } from './public.service';
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
import { PrismaService } from '../prisma/prisma.service';

describe('PublicService', () => {
  let service: PublicService;
  let prisma: PrismaService;
  const stamp = Date.now();
  let countryId: bigint;
  let categoryId: bigint;
  let childCategoryId: bigint;
  let otherCategoryId: bigint;
  let tagId: bigint;
  let filterGroupIds: bigint[] = [];
  let filterTagIds: bigint[] = [];
  let originGoodId: bigint;
  let familyId: bigint;
  let goodIds: bigint[] = [];

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [PublicService, PrismaService],
    }).compile();
    service = moduleRef.get(PublicService);
    prisma = moduleRef.get(PrismaService);
    await prisma.onModuleInit();

    const country = await prisma.country.create({
      data: { countryName: `Pub Country ${stamp}` },
    });
    countryId = country.id;

    const cat = await prisma.category.create({
      data: { categoryName: `Pub Cat ${stamp}` },
    });
    categoryId = cat.id;
    const child = await prisma.category.create({
      data: { categoryName: `Pub Child ${stamp}`, parentCategoryId: cat.id },
    });
    childCategoryId = child.id;
    const otherCat = await prisma.category.create({
      data: { categoryName: `Pub Other ${stamp}` },
    });
    otherCategoryId = otherCat.id;

    const tag = await prisma.tag.create({
      data: { tagName: `Pub Tag ${stamp}`, tagColor: '#0000FF' },
    });
    tagId = tag.id;

    const og = await prisma.originGood.create({
      data: {
        sdsGoodId: `pub-sds-${stamp}`,
        goodName: `Origin ${stamp}`,
        goodImage: 'http://img',
      },
    });
    originGoodId = og.id;

    // 公开契约族化：商品必须挂族才对外可见
    const family = await prisma.productFamily.create({
      data: { familyName: `Pub Family ${stamp}`, primaryOriginGoodId: og.id },
    });
    familyId = family.id;
    await prisma.originGood.update({
      where: { id: og.id },
      data: { familyId: family.id },
    });

    // Seed 3 goods:
    //   high priority + position.indexVal=1
    //   mid  priority + position.indexVal=5
    //   no  priority + no position (falls back to createdAt)
    const pos1 = await prisma.position.create({
      data: { indexVal: 1, countryId, categoryId },
    });
    const pos2 = await prisma.position.create({
      data: { indexVal: 5, countryId, categoryId },
    });

    const g1 = await prisma.good.create({
      data: {
        goodName: `Pub High ${stamp}`,
        originGoodId,
        familyId: family.id,
        countryId,
        categoryId,
        goodPriority: 10,
        positionId: pos1.id,
      },
    });
    const g2 = await prisma.good.create({
      data: {
        goodName: `Pub Mid ${stamp}`,
        originGoodId,
        familyId: family.id,
        countryId,
        categoryId,
        goodPriority: 5,
        positionId: pos2.id,
      },
    });
    const g3 = await prisma.good.create({
      data: {
        goodName: `Pub NoPos ${stamp}`,
        originGoodId,
        familyId: family.id,
        countryId,
        categoryId,
        tagId,
        goodPriority: 1,
      },
    });
    goodIds = [g1.id, g2.id, g3.id];

    const craftGroup = await prisma.tagGroup.create({
      data: { groupName: `Pub Craft ${stamp}`, sortOrder: 100 },
    });
    const materialGroup = await prisma.tagGroup.create({
      data: { groupName: `Pub Material ${stamp}`, sortOrder: 101 },
    });
    filterGroupIds = [craftGroup.id, materialGroup.id];
    const craftA = await prisma.tag.create({
      data: { tagName: `Pub Craft A ${stamp}`, tagGroupId: craftGroup.id },
    });
    const craftB = await prisma.tag.create({
      data: { tagName: `Pub Craft B ${stamp}`, tagGroupId: craftGroup.id },
    });
    const cotton = await prisma.tag.create({
      data: { tagName: `Pub Cotton ${stamp}`, tagGroupId: materialGroup.id },
    });
    filterTagIds = [craftA.id, craftB.id, cotton.id];
    await prisma.goodTag.createMany({
      data: [
        { goodId: g1.id, tagId: craftA.id },
        { goodId: g1.id, tagId: cotton.id },
        { goodId: g2.id, tagId: craftB.id },
      ],
    });

    await prisma.originGoodDetail.create({
      data: {
        originGoodId,
        productCode: 'OZ10827003',
        productionProcess: '白墨烫画',
        sizeChart: { columns: [], rows: [{ sizeId: 'size_0', sizeName: 'S', measurements: [] }] },
        packageSpecs: { rows: [{ sizeId: 'size_0', sizeName: 'S' }] },
      },
    });
    await prisma.originGoodVariant.create({
      data: {
        originGoodId,
        sdsVariantId: `pub-variant-${stamp}`,
        sku: `OZ${stamp}`,
        sizeName: 'S',
        price: 38,
      },
    });
    // 物化族矩阵（公开详情 family 块依赖 priceMatrix 已重算）
    await new FamilyRecomputeService(prisma).recomputeFamily(family.id);

    // Seed a good in `otherCategory` so the "onlyHaveGoods" filter
    // returns more than one category.
    await prisma.good.create({
      data: {
        goodName: `Pub Other ${stamp}`,
        originGoodId,
        familyId: family.id,
        countryId,
        categoryId: otherCategoryId,
        goodPriority: 1,
      },
    });

    // And seed a good in the *child* category, to verify categoryId
    // recursion.
    await prisma.good.create({
      data: {
        goodName: `Pub ChildGood ${stamp}`,
        originGoodId,
        familyId: family.id,
        countryId,
        categoryId: childCategoryId,
        goodPriority: 0,
      },
    });
  });

  afterAll(async () => {
    if (goodIds.length) {
      await prisma.good.deleteMany({ where: { id: { in: goodIds } } });
    }
    await prisma.good.deleteMany({
      where: { goodName: { contains: `Pub ` } },
    });
    // Good.familyId / OriginGood.familyId 均为 SetNull，先删商品再删族
    await prisma.productFamily.deleteMany({ where: { id: familyId } });
    await prisma.position.deleteMany({
      where: { countryId },
    });
    await prisma.tag.delete({ where: { id: tagId } });
    await prisma.tag.deleteMany({ where: { id: { in: filterTagIds } } });
    await prisma.tagGroup.deleteMany({ where: { id: { in: filterGroupIds } } });
    await prisma.originGood.delete({ where: { id: originGoodId } });
    // Delete children before parent (FK self-relation is RESTRICT).
    await prisma.category.delete({ where: { id: childCategoryId } });
    await prisma.category.delete({ where: { id: otherCategoryId } });
    await prisma.category.delete({ where: { id: categoryId } });
    await prisma.country.delete({ where: { id: countryId } });
    await prisma.onModuleDestroy();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('getCategoriesTree returns only categories that have goods', async () => {
    const tree = await service.getCategoriesTree();
    const allIds = new Set<string>();
    const walk = (list: Array<{ id: string; children: Array<{ id: string }> }>) => {
      for (const n of list) {
        allIds.add(n.id);
        walk(n.children as any);
      }
    };
    walk(tree as any);
    // We seeded goods in `categoryId`, `childCategoryId`, `otherCategoryId`.
    expect(allIds.has(categoryId.toString())).toBe(true);
    expect(allIds.has(childCategoryId.toString())).toBe(true);
    expect(allIds.has(otherCategoryId.toString())).toBe(true);
  });

  it('getCountries returns only countries that have goods', async () => {
    const countries = await service.getCountries();
    expect(countries.find((c) => c.id === countryId.toString())).toBeDefined();
  });

  it('filters by countryId, tagId, keyword and categoryId (recursively)', async () => {
    const filtered = await service.getGoods({
      page: 1,
      pageSize: 50,
      countryId: countryId.toString(),
      categoryId: categoryId.toString(), // includes child
      keyword: `Pub `,
    });
    expect(filtered.total).toBe(1); // 族化后：同族 4 条在售 Good（High/Mid/NoPos/Child）= 1 个款
    expect(filtered.items.every((g) => g.country.id === countryId.toString())).toBe(true);
  });

  it('sorts by priority DESC, position.indexVal ASC, createdAt DESC', async () => {
    const result = await service.getGoods({
      page: 1,
      pageSize: 50,
      countryId: countryId.toString(),
      keyword: `Pub `,
    });
    const priorities = result.items.map((g) => g.goodPriority);
    // First verify primary descending priority.
    const sorted = [...priorities].sort((a, b) => b - a);
    expect(priorities).toEqual(sorted);
  });

  it('uses OR within one tag group and AND across tag groups', async () => {
    const sameGroup = await service.getGoods({
      page: 1,
      pageSize: 50,
      countryId: countryId.toString(),
      keyword: `Pub `,
      tags: [
        {
          tagGroupId: filterGroupIds[0].toString(),
          tagIds: filterTagIds.slice(0, 2).map(String),
        },
      ],
    });
    // 族化后命中族内多条 Good 仍只出代表行（High 优先级最高）
    expect(sameGroup.items.map((item) => item.goodName)).toEqual([`Pub High ${stamp}`]);

    const acrossGroups = await service.getGoods({
      page: 1,
      pageSize: 50,
      countryId: countryId.toString(),
      keyword: `Pub `,
      tags: [
        {
          tagGroupId: filterGroupIds[0].toString(),
          tagIds: filterTagIds.slice(0, 2).map(String),
        },
        {
          tagGroupId: filterGroupIds[1].toString(),
          tagIds: [filterTagIds[2].toString()],
        },
      ],
    });
    expect(acrossGroups.items.map((item) => item.goodName)).toContain(`Pub High ${stamp}`);
    expect(acrossGroups.items.map((item) => item.goodName)).not.toContain(`Pub Mid ${stamp}`);
  });

  it('rejects a tag paired with the wrong tag group', async () => {
    await expect(
      service.getGoods({
        page: 1,
        pageSize: 20,
        tags: [
          {
            tagGroupId: filterGroupIds[1].toString(),
            tagIds: [filterTagIds[0].toString()],
          },
        ],
      }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('returns the family id as the public product id (一族多条 Good 只出一条)', async () => {
    const result = await service.getGoods({
      page: 1,
      pageSize: 50,
      countryId: countryId.toString(),
      keyword: `Pub `,
    });

    // 该族下 5 条 Good（High/Mid/NoPos/Other/Child）→ 列表仅 1 条，goodId=族ID
    expect(result.items).toHaveLength(1);
    expect(result.items[0].goodId).toBe(familyId.toString());
    expect(result.items[0].goodId).not.toBe(goodIds[0].toString());
    expect(result.items[0].goodName).toBe(`Pub High ${stamp}`); // 代表行 = 排序第一条
  });

  it('custom goods (无族) are not visible on public endpoints', async () => {
    const customPublicId = `custom-public-${stamp}`;
    const origin = await prisma.originGood.create({
      data: {
        source: 'CUSTOM',
        sdsGoodId: customPublicId,
        goodName: `Pub Custom ${stamp}`,
        goodPrice: 42,
        detail: { create: { productCode: `CUSTOM-${stamp}` } },
      },
    });
    const good = await prisma.good.create({
      data: {
        originGoodId: origin.id,
        countryId,
        categoryId,
        goodName: `Pub Custom ${stamp}`,
      },
    });
    try {
      const list = await service.getGoods({
        page: 1,
        pageSize: 50,
        countryId: countryId.toString(),
        keyword: `Pub Custom`,
      });
      expect(list.items).toHaveLength(0);
      // sdsGoodId 不再是公开寻址键：非数字直接 404
      await expect(service.getGood(customPublicId)).rejects.toBeInstanceOf(NotFoundException);
    } finally {
      await prisma.good.delete({ where: { id: good.id } });
      await prisma.originGood.delete({ where: { id: origin.id } });
    }
  });

  it('getGood returns family detail by family id and 404 for unknown id', async () => {
    const first = await service.getGoods({
      page: 1,
      pageSize: 1,
      countryId: countryId.toString(),
      keyword: `Pub `,
    });
    expect(first.items.length).toBe(1);
    const detail = await service.getGood(familyId.toString());
    expect(detail.goodId).toBe(first.items[0].goodId);
    expect(detail.productCode).toBe('OZ10827003');
    expect(detail.details.productionProcess).toBe('白墨烫画');
    expect((detail.sizeChart?.rows as unknown[])).toHaveLength(1);
    expect((detail.packageSpecs?.rows as unknown[])).toHaveLength(1);
    expect(detail.variants).toHaveLength(1);
    expect(detail.family?.familyId).toBe(familyId.toString());

    // 旧 sdsGoodId 寻址不再可达（族 ID 是唯一公开键）
    await expect(service.getGood(`pub-sds-${stamp}`)).rejects.toBeInstanceOf(NotFoundException);
    await expect(service.getGood('99999999')).rejects.toBeInstanceOf(
      NotFoundException,
    );
  });

  it('home-goods dedupes by family (keeps the best-positioned good)', async () => {
    const home = await service.getHomeGoods({ limit: 50 });
    // fixture 族的两条带位商品（High index=1 / Mid index=5）→ 只出一条代表行
    const ours = home.filter((h) => h.goodId === familyId.toString());
    expect(ours).toHaveLength(1);
    expect(ours[0].goodName).toBe(`Pub High ${stamp}`);
  });

  it('getTags returns tags with their group info, sorted by group then order', async () => {
    const tags = await service.getTags();
    expect(tags.length).toBeGreaterThan(0);
    // Each tag in our seed (包邮/不包邮/...) should have a group
    const grouped = tags.find((t) => t.tagName === '包邮');
    if (grouped) {
      expect(grouped.group).not.toBeNull();
      expect(grouped.group!.groupName).toBe('物流渠道');
    }
  });

  it('getTagGroups returns only groups that have goods', async () => {
    const groups = await service.getTagGroups();
    expect(groups.length).toBeGreaterThan(0);
    const names = groups.map((g) => g.groupName);
    // 链接级派生标签落地后，商品挂在 物流渠道/印刷工艺/印花数量 三组
    expect(names).toContain('物流渠道');
    expect(names).toContain('印花数量');
    expect(names).toContain('印刷工艺');
    // Sorted by sortOrder
    const sortOrders = groups.map((g) => g.sortOrder);
    expect([...sortOrders].sort((a, b) => a - b)).toEqual(sortOrders);
  });

  describe('merged secondary origin goods', () => {
    it('family members union variants and media (secondary link joins the family)', async () => {
      const secondary = await prisma.originGood.create({
        data: { sdsGoodId: `pub-secondary-${stamp}`, goodName: `Pub Secondary ${stamp}` },
      });
      const secVariant = await prisma.originGoodVariant.create({
        data: {
          originGoodId: secondary.id,
          sdsVariantId: `pub-var-sec-${stamp}`,
          sku: `PUB-SEC-${stamp}`,
          colorId: 'black',
          colorName: '黑色',
          imageUrl: 'http://img/black-sec',
        },
      });
      // 副链归入主 fixture 的族（族机制替代旧 good_origin_goods 关联）
      await prisma.originGood.update({
        where: { id: secondary.id },
        data: { familyId },
      });

      try {
        const detail = await service.getGood(familyId.toString());
        expect(detail.goodId).toBe(familyId.toString()); // 对外 goodId 恒为族ID
        expect(detail.variants.length).toBeGreaterThanOrEqual(2);
        const black = detail.mediaByColor.find((g) => g.colorName === '黑色');
        expect(black).toBeTruthy();
        expect(black!.images).toContain('http://img/black-sec');
        // sdsGoodId 不是公开键：副链 ID 无法寻址
        await expect(service.getGood(`pub-secondary-${stamp}`)).rejects.toBeInstanceOf(
          NotFoundException,
        );
      } finally {
        await prisma.originGoodVariant.delete({ where: { id: secVariant.id } }).catch(() => undefined);
        await prisma.originGood.delete({ where: { id: secondary.id } }).catch(() => undefined);
      }
    });

    it('dedupes variants by color+size (primary wins) and merges specs/options', async () => {
      const stamp2 = `${stamp}-merge2`;
      const primaryOg = await prisma.originGood.create({
        data: { sdsGoodId: `pub-pri-${stamp2}`, goodName: `Pub Pri ${stamp2}` },
      });
      const secondaryOg = await prisma.originGood.create({
        data: { sdsGoodId: `pub-sec-${stamp2}`, goodName: `Pub Sec ${stamp2}` },
      });
      // Primary: Black|S and Black|M, size chart S/M, package S, options S/M.
      await prisma.originGoodVariant.createMany({
        data: [
          { originGoodId: primaryOg.id, sdsVariantId: `v1-${stamp2}`, sku: `SK1-${stamp2}`, colorName: 'Black', sizeName: 'S', price: 16 },
          { originGoodId: primaryOg.id, sdsVariantId: `v2-${stamp2}`, sku: `SK2-${stamp2}`, colorName: 'black', sizeName: 'M', price: 16 },
        ],
      });
      await prisma.originGoodDetail.create({
        data: {
          originGoodId: primaryOg.id,
          sizeChart: { rows: [{ sizeName: 'S', measurements: [{ key: 'chest', cm: '94' }] }, { sizeName: 'M', measurements: [{ key: 'chest', cm: '100' }] }] },
          packageSpecs: { rows: [{ sizeName: 'S' }] },
          options: { sizes: [{ name: 'S' }, { name: 'M' }] },
          media: { images: [{ id: 'i1', url: 'http://img/pri-a', sortOrder: 0 }, { id: 'i2', url: 'http://img/pri-b', sortOrder: 1 }], primaryImageUrl: 'http://img/pri-a' },
        },
      });
      // Secondary: duplicate Black|S with a DIFFERENT price (must be dropped,
      // primary wins), plus a unique color Kelly|S; specs add XXXL rows.
      await prisma.originGoodVariant.createMany({
        data: [
          { originGoodId: secondaryOg.id, sdsVariantId: `v3-${stamp2}`, sku: `SK3-${stamp2}`, colorName: 'Black', sizeName: 'S', price: 20 },
          { originGoodId: secondaryOg.id, sdsVariantId: `v4-${stamp2}`, sku: `SK4-${stamp2}`, colorName: 'Kelly', sizeName: 'S', price: 22 },
        ],
      });
      await prisma.originGoodDetail.create({
        data: {
          originGoodId: secondaryOg.id,
          sizeChart: { rows: [{ sizeName: 'XXXL', measurements: [{ key: 'chest', cm: '120' }] }] },
          packageSpecs: { rows: [{ sizeName: 'M' }, { sizeName: 'XXXL' }] },
          options: { sizes: [{ name: 'XXXL' }] },
          media: { images: [{ id: 'i1', url: 'http://img/pri-a', sortOrder: 0 }, { id: 'i9', url: 'http://img/sec-x', sortOrder: 0 }], primaryImageUrl: 'http://img/pri-a' },
        },
      });
      // 主副链同族 + 一条官网 Good（族化契约：Good 挂族才公开）
      const mergeFamily = await prisma.productFamily.create({
        data: { familyName: `Pub Merge Family ${stamp2}`, primaryOriginGoodId: primaryOg.id },
      });
      await prisma.originGood.updateMany({
        where: { id: { in: [primaryOg.id, secondaryOg.id] } },
        data: { familyId: mergeFamily.id },
      });
      const mergedGood = await prisma.good.create({
        data: {
          goodName: `Pub Merged ${stamp2}`,
          originGoodId: primaryOg.id,
          familyId: mergeFamily.id,
          countryId,
          categoryId,
        },
      });

      try {
        const detail = await service.getGood(mergeFamily.id.toString());
        // Variants: 3 unique color+size combos (case-insensitive); duplicate
        // Black|S from the secondary deduped.
        expect(
          detail.variants.map((v) => `${v.colorName}/${v.sizeName}`.toLowerCase()).sort(),
        ).toEqual(['black/m', 'black/s', 'kelly/s']);
        const blackS = detail.variants.find((v) => v.colorName === 'Black' && v.sizeName === 'S');
        expect(blackS!.price).toBe('16'); // primary price wins over secondary 20
        // Size chart: S/M from primary, XXXL appended from secondary.
        const chartSizes = (detail.sizeChart as any).rows.map((r: any) => r.sizeName).sort();
        expect(chartSizes).toEqual(['M', 'S', 'XXXL']);
        // Package specs: S from primary, M/XXXL appended from secondary.
        const pkgSizes = (detail.packageSpecs as any).rows.map((r: any) => r.sizeName).sort();
        expect(pkgSizes).toEqual(['M', 'S', 'XXXL']);
        // Options: sizes unioned S/M + XXXL.
        const optSizes = (detail.options as any).sizes.map((s: any) => s.name).sort();
        expect(optSizes).toEqual(['M', 'S', 'XXXL']);
        // Media gallery: primary images first, secondary-only URL appended,
        // duplicate URL (pri-a) kept once. Entries keep their object shape.
        const media = detail.media as any;
        expect(media.images.map((i: any) => i.url)).toEqual([
          'http://img/pri-a',
          'http://img/pri-b',
          'http://img/sec-x',
        ]);
        expect(media.primaryImageUrl).toBe('http://img/pri-a');
      } finally {
        await prisma.good.delete({ where: { id: mergedGood.id } });
        await prisma.productFamily.delete({ where: { id: mergeFamily.id } });
        await prisma.originGoodVariant.deleteMany({ where: { originGoodId: { in: [primaryOg.id, secondaryOg.id] } } });
        await prisma.originGoodDetail.deleteMany({ where: { originGoodId: { in: [primaryOg.id, secondaryOg.id] } } });
        await prisma.originGood.delete({ where: { id: primaryOg.id } });
        await prisma.originGood.delete({ where: { id: secondaryOg.id } }).catch(() => undefined);
      }
    });
  });
});
