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

describe('GoodsService', () => {
  let service: GoodsService;
  let prisma: PrismaService;
  const stamp = Date.now();

  // Fixtures
  let countryId: bigint;
  let country2Id: bigint;
  let categoryId: bigint;
  let childCategoryId: bigint;
  let tagId: bigint;
  let positionId: bigint;
  let originGoodIds: bigint[] = [];

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [
        GoodsService,
        PrismaService,
        {
          provide: SyncService,
          useValue: { queueProductDetailSync: jest.fn() },
        },
        {
          provide: FamilyRecomputeService,
          // 全量并行跑时其他套件的扫名归族可能把本套件夹具链接收进族，
          // create/update 会调用 syncFamilyTags —— mock 必须覆盖全部被调方法
          useValue: {
            enqueue: jest.fn(),
            recomputeFamily: jest.fn().mockResolvedValue(undefined),
            syncFamilyTags: jest.fn().mockResolvedValue({ goodsUpdated: 0, linksUpdated: 0 }),
            refreshLinkTags: jest.fn().mockResolvedValue(undefined),
            mirrorLinkTagsToGoods: jest.fn().mockResolvedValue(undefined),
          },
        },
      ],
    }).compile();
    service = moduleRef.get(GoodsService);
    prisma = moduleRef.get(PrismaService);
    await prisma.onModuleInit();

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

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

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

    const pos = await prisma.position.create({
      data: { indexVal: 1, countryId, categoryId },
    });
    positionId = pos.id;

    const originGoods = await Promise.all(
      Array.from({ length: 5 }).map((_, i) =>
        prisma.originGood.create({
          data: {
            sdsGoodId: `sds-goods-${stamp}-${i}`,
            goodName: `Goods Origin ${stamp} ${i}`,
          },
        }),
      ),
    );
    originGoodIds = originGoods.map((og) => og.id);
  });

  afterAll(async () => {
    // Wipe all goods first so origin_goods/category can be removed.
    await prisma.good.deleteMany({
      where: { goodName: { contains: `Goods Test ${stamp}` } },
    });
    await prisma.good.deleteMany({
      where: { goodName: { contains: `Origin ${stamp}` } },
    });
    await prisma.originGood.deleteMany({
      where: { id: { in: originGoodIds } },
    });
    await prisma.position.delete({ where: { id: positionId } });
    await prisma.tag.delete({ where: { id: tagId } });
    await prisma.category.delete({ where: { id: childCategoryId } });
    await prisma.category.delete({ where: { id: categoryId } });
    await prisma.country.delete({ where: { id: countryId } });
    await prisma.country.delete({ where: { id: country2Id } });
    await prisma.onModuleDestroy();
  });

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

  it('creates and reads back a good', async () => {
    const created = await service.create({
      goodName: `Goods Test ${stamp} basic`,
      originGoodId: Number(originGoodIds[0]),
      countryId: Number(countryId),
      categoryId: Number(categoryId),
      tagIds: [Number(tagId)],
      positionId: Number(positionId),
      goodPriority: 3,
    });
    expect(created.id).toBeTruthy();
    expect(created.country?.countryName).toBeTruthy();
    expect(created.tags.some((t) => t.tagColor === '#00FF00')).toBe(true);

    const fetched = await service.findOne(BigInt(created.id));
    expect(fetched.goodName).toBe(`Goods Test ${stamp} basic`);
  });

  it('creates, edits, and removes a fully editable custom good', async () => {
    const created = await service.createCustom({
      goodName: `Goods Test ${stamp} custom`,
      goodImage: 'https://example.com/custom.png',
      goodPrice: '29.90',
      countryId: Number(countryId),
      categoryId: Number(categoryId),
      tagIds: [Number(tagId)],
      detail: {
        productCode: `CUSTOM-${stamp}`,
        materialDescription: 'Cotton',
        sizeChart: { columns: [], rows: [] },
        packageSpecs: { rows: [] },
      },
      variants: [
        { sku: `CUSTOM-SKU-${stamp}`, sizeName: 'S', price: '29.90' },
      ],
    });

    expect(created.originGood?.source).toBe('CUSTOM');
    expect(created.originGood?.isCustom).toBe(true);
    expect(created.originGood?.goodPrice).toBe('29.9');
    expect(created.variants).toHaveLength(1);

    const updated = await service.updateCustomContent(BigInt(created.id), {
      goodName: `Goods Test ${stamp} custom edited`,
      goodPrice: '39.90',
      detail: { materialDescription: 'Organic cotton' },
      variants: [
        { sku: `CUSTOM-SKU-${stamp}-M`, sizeName: 'M', price: '39.90' },
      ],
    });
    expect(updated.goodName).toContain('custom edited');
    expect(updated.originGood?.goodPrice).toBe('39.9');
    expect(updated.originDetail?.materialDescription).toBe('Organic cotton');
    expect(updated.originDetail?.productCode).toBe(`CUSTOM-${stamp}`);
    expect(updated.variants[0]?.sizeName).toBe('M');

    const customOriginId = BigInt(updated.originGoodId);
    await service.remove(BigInt(updated.id));
    await expect(
      prisma.originGood.findUnique({ where: { id: customOriginId } }),
    ).resolves.toBeNull();
  });

  it('filters by countryId, tagId, positionId and keyword', async () => {
    const result = await service.findAll({
      page: 1,
      pageSize: 20,
      countryId: Number(countryId),
      tagId: Number(tagId),
      keyword: `Goods Test ${stamp}`,
    });
    expect(result.items.length).toBeGreaterThan(0);
    expect(result.items.every((g) => g.countryId === countryId.toString())).toBe(true);
    expect(
      result.items.every((g) => g.tags.some((t) => t.id === tagId.toString())),
    ).toBe(true);
  });

  it('categoryId filter includes descendants recursively', async () => {
    const inChild = await service.create({
      goodName: `Goods Test ${stamp} child`,
      originGoodId: Number(originGoodIds[1]),
      countryId: Number(countryId),
      categoryId: Number(childCategoryId),
    });
    const result = await service.findAll({
      page: 1,
      pageSize: 20,
      categoryId: Number(categoryId),
      keyword: `Goods Test ${stamp}`,
    });
    const ids = result.items.map((g) => g.id);
    expect(ids).toContain(inChild.id);
  });

  it('batch update priority is atomic', async () => {
    const created = await service.create({
      goodName: `Goods Test ${stamp} prio`,
      originGoodId: Number(originGoodIds[2]),
      countryId: Number(countryId),
      categoryId: Number(categoryId),
    });
    const result = await service.batchUpdatePriority({
      items: [{ id: Number(created.id), priority: 42 }],
    });
    expect(result.count).toBe(1);
    const after = await service.findOne(BigInt(created.id));
    expect(after.goodPriority).toBe(42);
  });

  it('batch create creates all rows or none', async () => {
    // Count of goods whose originGoodId is one of the two fixture ids,
    // so we are independent of goodName (which batch derives from origin).
    const before = await service.findAll({
      page: 1,
      pageSize: 100,
      keyword: `Goods Origin ${stamp}`,
    });
    const created = await service.batchCreate({
      countryId: Number(countryId),
      categoryId: Number(categoryId),
      defaultPriority: 1,
      items: [
        { originGoodId: Number(originGoodIds[3]) },
        { originGoodId: Number(originGoodIds[4]) },
      ],
    });
    expect(created.length).toBe(2);
    const after = await service.findAll({
      page: 1,
      pageSize: 100,
      keyword: `Goods Origin ${stamp}`,
    });
    expect(after.total).toBe(before.total + 2);
  });

  it('batch create rolls back on failure', async () => {
    const before = await service.findAll({
      page: 1,
      pageSize: 100,
      keyword: `Goods Origin ${stamp}`,
    });
    await expect(
      service.batchCreate({
        countryId: Number(countryId),
        categoryId: Number(categoryId),
        items: [
          { originGoodId: Number(originGoodIds[0]) },
          { originGoodId: 99999999 }, // missing -> failure
        ],
      }),
    ).rejects.toBeInstanceOf(BadRequestException);
    const after = await service.findAll({
      page: 1,
      pageSize: 100,
      keyword: `Goods Origin ${stamp}`,
    });
    expect(after.total).toBe(before.total);
  });

  describe('good name normalization', () => {
    it('create/update 时把结构完整的原始链接名规范化为 品名+型号', async () => {
      const created = await service.create({
        goodName: '德国(不包邮)230g水洗T恤-DETM002-双面印花',
        originGoodId: Number(originGoodIds[0]),
        countryId: Number(countryId),
        categoryId: Number(categoryId),
      });
      try {
        expect(created.goodName).toBe('230g水洗T恤 DETM002');
        const updated = await service.update(BigInt(created.id), {
          goodName: '加拿大（不包邮）180g纯棉T恤-CATM001-单面印花',
        });
        expect(updated.goodName).toBe('180g纯棉T恤 CATM001');
      } finally {
        await prisma.good.delete({ where: { id: BigInt(created.id) } });
      }
    });

    it('非链接结构的名称原样保留（已解析名/自定义名）', async () => {
      const created = await service.create({
        goodName: '230g水洗T恤 DETM002',
        originGoodId: Number(originGoodIds[0]),
        countryId: Number(countryId),
        categoryId: Number(categoryId),
      });
      try {
        expect(created.goodName).toBe('230g水洗T恤 DETM002');
        const updated = await service.update(BigInt(created.id), {
          goodName: '自定义商品 ABC',
        });
        expect(updated.goodName).toBe('自定义商品 ABC');
      } finally {
        await prisma.good.delete({ where: { id: BigInt(created.id) } });
      }
    });
  });

  describe('merged origin goods', () => {
    it('creates a good with merged origin goods and reads them back', async () => {
      const created = await service.create({
        goodName: `Goods Test ${stamp} merged`,
        originGoodId: Number(originGoodIds[1]),
        mergedOriginGoodIds: [Number(originGoodIds[2]), Number(originGoodIds[3])],
        countryId: Number(countryId),
        categoryId: Number(categoryId),
      });
      expect(created.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
        [originGoodIds[2].toString(), originGoodIds[3].toString()].sort(),
      );
      const fetched = await service.findOne(BigInt(created.id));
      expect(fetched.mergedOriginGoods.length).toBe(2);
    });

    it('rejects mergedOriginGoodIds containing the primary', async () => {
      await expect(
        service.create({
          goodName: `Goods Test ${stamp} bad-primary`,
          originGoodId: Number(originGoodIds[1]),
          mergedOriginGoodIds: [Number(originGoodIds[1])],
          countryId: Number(countryId),
          categoryId: Number(categoryId),
        }),
      ).rejects.toThrow(BadRequestException);
    });

    it('rejects mergedOriginGoodIds that do not exist', async () => {
      await expect(
        service.create({
          goodName: `Goods Test ${stamp} bad-missing`,
          originGoodId: Number(originGoodIds[1]),
          mergedOriginGoodIds: [999999999],
          countryId: Number(countryId),
          categoryId: Number(categoryId),
        }),
      ).rejects.toThrow(BadRequestException);
    });

    it('replaces merged origin goods on update', async () => {
      const created = await service.create({
        goodName: `Goods Test ${stamp} replace`,
        originGoodId: Number(originGoodIds[1]),
        mergedOriginGoodIds: [Number(originGoodIds[2])],
        countryId: Number(countryId),
        categoryId: Number(categoryId),
      });
      const updated = await service.update(BigInt(created.id), {
        mergedOriginGoodIds: [Number(originGoodIds[3]), Number(originGoodIds[4])],
      });
      expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
        [originGoodIds[3].toString(), originGoodIds[4].toString()].sort(),
      );
    });

    it('moves old primary into merged list when switching primary', async () => {
      const created = await service.create({
        goodName: `Goods Test ${stamp} switch`,
        originGoodId: Number(originGoodIds[1]),
        mergedOriginGoodIds: [Number(originGoodIds[2])],
        countryId: Number(countryId),
        categoryId: Number(categoryId),
      });
      const updated = await service.update(BigInt(created.id), {
        originGoodId: Number(originGoodIds[2]),
        mergedOriginGoodIds: [Number(originGoodIds[1]), Number(originGoodIds[3])],
      });
      expect(updated.originGoodId).toBe(originGoodIds[2].toString());
      expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
        [originGoodIds[1].toString(), originGoodIds[3].toString()].sort(),
      );
    });

    it('cascades merged rows on good removal', async () => {
      const created = await service.create({
        goodName: `Goods Test ${stamp} cascade`,
        originGoodId: Number(originGoodIds[1]),
        mergedOriginGoodIds: [Number(originGoodIds[2])],
        countryId: Number(countryId),
        categoryId: Number(categoryId),
      });
      await service.remove(BigInt(created.id));
      const rows = await prisma.goodOriginGood.count({
        where: { goodId: BigInt(created.id) },
      });
      expect(rows).toBe(0);
    });

    it('returns merged variants with source annotation in detail', async () => {
      const v1 = await prisma.originGoodVariant.create({
        data: {
          originGoodId: originGoodIds[1],
          sdsVariantId: `mv-pri-${stamp}`,
          sku: `MV-PRI-${stamp}`,
          colorName: '黑色',
        },
      });
      const v2 = await prisma.originGoodVariant.create({
        data: {
          originGoodId: originGoodIds[2],
          sdsVariantId: `mv-sec-${stamp}`,
          sku: `MV-SEC-${stamp}`,
          colorName: '白色',
        },
      });
      try {
        const created = await service.create({
          goodName: `Goods Test ${stamp} variants`,
          originGoodId: Number(originGoodIds[1]),
          mergedOriginGoodIds: [Number(originGoodIds[2])],
          countryId: Number(countryId),
          categoryId: Number(categoryId),
        });
        const detail = await service.findOne(BigInt(created.id));
        const sources = new Set(
          detail.variants.map((v) => v['originGoodId'] as string),
        );
        expect(sources.has(originGoodIds[1].toString())).toBe(true);
        expect(sources.has(originGoodIds[2].toString())).toBe(true);
        expect(detail.variants).toHaveLength(2);
        expect(detail.mergedOriginGoods.find((m) => m.id === originGoodIds[2].toString())?.variantCount).toBe(1);
      } finally {
        await prisma.originGoodVariant.delete({ where: { id: v1.id } });
        await prisma.originGoodVariant.delete({ where: { id: v2.id } });
      }
    });
  });

  it('throws NotFoundException for unknown id', async () => {
    await expect(service.findOne(BigInt(99999999))).rejects.toBeInstanceOf(
      NotFoundException,
    );
  });
});
