import { Test } from '@nestjs/testing';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { PublicService } from './public.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 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;

    // 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,
        countryId,
        categoryId,
        goodPriority: 10,
        positionId: pos1.id,
      },
    });
    const g2 = await prisma.good.create({
      data: {
        goodName: `Pub Mid ${stamp}`,
        originGoodId,
        countryId,
        categoryId,
        goodPriority: 5,
        positionId: pos2.id,
      },
    });
    const g3 = await prisma.good.create({
      data: {
        goodName: `Pub NoPos ${stamp}`,
        originGoodId,
        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,
      },
    });

    // Seed a good in `otherCategory` so the "onlyHaveGoods" filter
    // returns more than one category.
    await prisma.good.create({
      data: {
        goodName: `Pub Other ${stamp}`,
        originGoodId,
        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,
        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 ` } },
    });
    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).toBeGreaterThanOrEqual(4); // High, Mid, NoPos, ChildGood
    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),
        },
      ],
    });
    expect(sameGroup.items.map((item) => item.goodName)).toEqual(
      expect.arrayContaining([`Pub High ${stamp}`, `Pub Mid ${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 SDS product id as the public product id', async () => {
    const result = await service.getGoods({
      page: 1,
      pageSize: 1,
      countryId: countryId.toString(),
      keyword: `Pub High ${stamp}`,
    });

    expect(result.items).toHaveLength(1);
    expect(result.items[0].goodId).toBe(`pub-sds-${stamp}`);
    expect(result.items[0].goodId).not.toBe(goodIds[0].toString());
  });

  it('returns custom goods through the same public product contract', 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 detail = await service.getGood(customPublicId);
      expect(detail.goodId).toBe(customPublicId);
      expect(detail.goodName).toBe(`Pub Custom ${stamp}`);
      expect(detail.productCode).toBe(`CUSTOM-${stamp}`);
    } finally {
      await prisma.good.delete({ where: { id: good.id } });
      await prisma.originGood.delete({ where: { id: origin.id } });
    }
  });

  it('getGood returns detail 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(`pub-sds-${stamp}`);
    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);

    await expect(service.getGood('99999999')).rejects.toBeInstanceOf(
      NotFoundException,
    );
  });

  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('resolves a good by secondary sdsGoodId with merged variants', 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',
        },
      });
      // Attach as secondary source of the highest-priority fixture good.
      await prisma.goodOriginGood.create({
        data: { goodId: goodIds[0], originGoodId: secondary.id },
      });

      try {
        const detail = await service.getGood(`pub-secondary-${stamp}`);
        expect(detail.goodId).toBe(`pub-sds-${stamp}`); // 对外 goodId 仍是主源
        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');
      } finally {
        await prisma.goodOriginGood.deleteMany({ where: { originGoodId: secondary.id } });
        await prisma.originGoodVariant.delete({ where: { id: secVariant.id } }).catch(() => undefined);
        await prisma.originGood.delete({ where: { id: secondary.id } }).catch(() => undefined);
      }
    });
  });
});
