import { CategorySuggestion } from '../ai/ai.service';
import { FlatCategory } from '../categories/categories.service';
import { isIgnoredReceiptName, ReceiptImportService } from './receipt-import.service';

function cat(id: number, name: string, path: string): FlatCategory {
  return { id, name, path };
}

describe('ReceiptImportService test matrix', () => {
  const categories: FlatCategory[] = [
    cat(1, 'Bröd & Kakor', 'Bröd & Kakor'),
    cat(2, 'Kondis & fika', 'Bröd & Kakor > Kondis & fika'),
    cat(3, 'Kaffebröd', 'Bröd & Kakor > Kondis & fika > Kaffebröd'),

    cat(10, 'Skafferi', 'Skafferi'),
    cat(11, 'Pasta, ris & matgryn', 'Skafferi > Pasta, ris & matgryn'),
    cat(12, 'Pasta', 'Skafferi > Pasta, ris & matgryn > Pasta'),

    cat(20, 'Frukt & Grönt', 'Frukt & Grönt'),
    cat(21, 'Potatis & rotsaker', 'Frukt & Grönt > Potatis & rotsaker'),
    cat(22, 'Potatis', 'Frukt & Grönt > Potatis & rotsaker > Potatis'),

    cat(30, 'Mejeri, ost & ägg', 'Mejeri, ost & ägg'),
    cat(31, 'Matlagning', 'Mejeri, ost & ägg > Matlagning'),
    cat(32, 'Grädde', 'Mejeri, ost & ägg > Matlagning > Grädde'),
    cat(33, 'Ägg', 'Mejeri, ost & ägg > Ägg'),

    cat(40, 'Dryck', 'Dryck'),
    cat(41, 'Juice, fruktdryck & smoothie', 'Dryck > Juice, fruktdryck & smoothie'),
    cat(42, 'Kyld juice & nektar', 'Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar'),

    cat(50, 'Glass, godis & snacks', 'Glass, godis & snacks'),
    cat(51, 'Godis', 'Glass, godis & snacks > Godis'),
    cat(52, 'Godispåsar', 'Glass, godis & snacks > Godis > Godispåsar'),
    cat(53, 'Choklad', 'Glass, godis & snacks > Choklad'),
    cat(54, 'Chokladkakor & rullar', 'Glass, godis & snacks > Choklad > Chokladkakor & rullar'),
  ];

  const prismaMock = {
    category: { findMany: jest.fn().mockResolvedValue([]) },
    receiptAlias: { findMany: jest.fn().mockResolvedValue([]) },
    product: { findMany: jest.fn().mockResolvedValue([]) },
    unitMapping: { findMany: jest.fn().mockResolvedValue([]) },
  };

  const aiServiceMock = {
    suggestCategory: jest.fn(),
  };

  const categoriesServiceMock = {
    findFlattened: jest.fn(),
  };

  const service = new ReceiptImportService(
    prismaMock as any,
    aiServiceMock as any,
    categoriesServiceMock as any,
  );

  beforeEach(() => {
    jest.clearAllMocks();
    prismaMock.receiptAlias.findMany.mockResolvedValue([]);
    prismaMock.product.findMany.mockResolvedValue([]);
    prismaMock.unitMapping.findMany.mockResolvedValue([]);
  });

  describe('ignore patterns', () => {
    it.each([
      'Willys Plus:Bröd',
      'willys plus: mjölk',
      'WILLYS PLUS - ÄGG',
      'Willys Plus  :   Ost',
      'Rabatt kupong',
      'Summa',
    ])('ignorerar "%s"', (raw: string) => {
      expect(isIgnoredReceiptName(raw)).toBe(true);
    });

    it.each([
      'Mezze Maniche',
      'Snickers',
      'Nappar Cola 80g',
      'Vispgrädde 5DL',
    ])('ignorerar inte "%s"', (raw: string) => {
      expect(isIgnoredReceiptName(raw)).toBe(false);
    });
  });

  describe('rule matrix', () => {
    const matrix: Array<{ raw: string; expectedPath: string }> = [
      { raw: 'Mezze Maniche', expectedPath: 'Skafferi > Pasta, ris & matgryn > Pasta' },
      { raw: 'Nappar Cola 80g', expectedPath: 'Glass, godis & snacks > Godis > Godispåsar' },
      { raw: 'Snickers', expectedPath: 'Glass, godis & snacks > Choklad > Chokladkakor & rullar' },
      { raw: 'Potatis Fast', expectedPath: 'Frukt & Grönt > Potatis & rotsaker > Potatis' },
      { raw: 'Ägg 24p Inne M', expectedPath: 'Mejeri, ost & ägg > Ägg' },
      { raw: 'Dryck Multivitamin', expectedPath: 'Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar' },
      { raw: 'Vispgrädde 5DL', expectedPath: 'Mejeri, ost & ägg > Matlagning > Grädde' },
      { raw: 'Wienerbröd', expectedPath: 'Bröd & Kakor > Kondis & fika > Kaffebröd' },
    ];

    it.each(matrix)('klassar "$raw" -> "$expectedPath"', ({ raw, expectedPath }: { raw: string; expectedPath: string }) => {
      const suggestion = (service as any).ruleBasedCategorySuggestion(raw, categories) as CategorySuggestion | null;
      expect(suggestion).not.toBeNull();
      expect(suggestion?.path).toBe(expectedPath);
    });
  });

  describe('alias fallback och prioritet', () => {
    it('prioriterar user-alias före global alias för samma receiptName', async () => {
      prismaMock.receiptAlias.findMany.mockResolvedValue([
        {
          receiptName: 'mjolk 1l',
          productId: 501,
          product: {
            id: 501,
            name: 'Mjolk user',
            canonicalName: 'Mjolk user',
            categoryId: 30,
            categoryRef: { id: 30, name: 'Mejeri' },
          },
        },
        {
          receiptName: 'mjolk 1l',
          productId: 999,
          product: {
            id: 999,
            name: 'Mjolk global',
            canonicalName: 'Mjolk global',
            categoryId: 30,
            categoryRef: { id: 30, name: 'Mejeri' },
          },
        },
      ]);

      prismaMock.product.findMany.mockResolvedValue([]);

      const result = await (service as any).matchProducts(
        [{ rawName: 'MJOLK 1L' }],
        77,
      );

      expect(prismaMock.receiptAlias.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            OR: [
              { ownerId: 77, isGlobal: false },
              { isGlobal: true },
            ],
          },
        }),
      );

      expect(result[0].matchedProductId).toBe(501);
      expect(result[0].matchedProductName).toBe('Mjolk user');
    });

    it('använder global alias när user-alias saknas', async () => {
      prismaMock.receiptAlias.findMany.mockResolvedValue([
        {
          receiptName: 'snickers',
          productId: 222,
          product: {
            id: 222,
            name: 'Snickers',
            canonicalName: 'Snickers',
            categoryId: 53,
            categoryRef: { id: 53, name: 'Choklad' },
          },
        },
      ]);

      prismaMock.product.findMany.mockResolvedValue([]);

      const result = await (service as any).matchProducts(
        [{ rawName: 'SNICKERS' }],
        88,
      );

      expect(result[0].matchedProductId).toBe(222);
      expect(result[0].matchedProductName).toBe('Snickers');
    });

    it('flöde: manuell korrigering lär alias och nästa import matchar direkt', async () => {
      const aliases: any[] = [];
      prismaMock.receiptAlias.findMany.mockImplementation(async () => aliases);

      prismaMock.product.findMany.mockResolvedValue([
        {
          id: 700,
          name: 'Arla Mjolk 1l',
          canonicalName: 'Mjolk',
          categoryId: 30,
          categoryRef: { id: 30, name: 'Mejeri' },
        },
      ]);

      const first = await (service as any).matchProducts(
        [{ rawName: 'ARLA MJOLK 1L' }],
        42,
      );

      expect(first[0].matchedProductId).toBeUndefined();
      expect(first[0].suggestedProductId).toBe(700);

      // Simulerar att användaren manuellt korrigerar och alias lärs in.
      aliases.push({
        receiptName: 'arla mjolk 1l',
        productId: 700,
        product: {
          id: 700,
          name: 'Arla Mjolk 1l',
          canonicalName: 'Mjolk',
          categoryId: 30,
          categoryRef: { id: 30, name: 'Mejeri' },
        },
      });

      const second = await (service as any).matchProducts(
        [{ rawName: 'ARLA MJOLK 1L' }],
        42,
      );

      expect(second[0].matchedProductId).toBe(700);
      expect(second[0].matchedProductName).toBe('Mjolk');
      expect(second[0].suggestedProductId).toBeUndefined();
    });

    it('använder inlärd enhetsmappning vid aliasträff', async () => {
      prismaMock.receiptAlias.findMany.mockResolvedValue([
        {
          receiptName: 'mjolk 1l',
          productId: 501,
          product: {
            id: 501,
            name: 'Mjolk user',
            canonicalName: 'Mjolk user',
            categoryId: 30,
            categoryRef: { id: 30, name: 'Mejeri' },
          },
        },
      ]);

      prismaMock.unitMapping.findMany.mockResolvedValue([
        {
          productId: 501,
          originalUnit: 'l',
          preferredUnit: 'st',
        },
      ]);

      const result = await (service as any).matchProducts(
        [{ rawName: 'MJOLK 1L', unit: 'L' }],
        77,
      );

      expect(result[0].matchedProductId).toBe(501);
      expect(result[0].unit).toBe('st');
    });
  });

  describe('matchedVia', () => {
    it('sätter matchedVia: alias vid aliasträff', async () => {
      prismaMock.receiptAlias.findMany.mockResolvedValue([
        {
          receiptName: 'snickers',
          productId: 222,
          product: { id: 222, name: 'Snickers', canonicalName: 'Snickers', categoryId: null, categoryRef: null },
        },
      ]);
      prismaMock.product.findMany.mockResolvedValue([]);

      const result = await (service as any).matchProducts([{ rawName: 'SNICKERS' }], 10);

      expect(result[0].matchedVia).toBe('alias');
    });

    it('sätter matchedVia: wordmatch vid ordbaserad matchning', async () => {
      prismaMock.receiptAlias.findMany.mockResolvedValue([]);
      prismaMock.product.findMany.mockResolvedValue([
        { id: 300, name: 'Mjolk', canonicalName: 'Mjolk', categoryId: null, categoryRef: null },
      ]);

      const result = await (service as any).matchProducts([{ rawName: 'MJOLK 1L' }], 10);

      expect(result[0].matchedVia).toBe('wordmatch');
    });

    it('sätter matchedVia: none när ingen matchning finns', async () => {
      prismaMock.receiptAlias.findMany.mockResolvedValue([]);
      prismaMock.product.findMany.mockResolvedValue([]);

      const result = await (service as any).matchProducts([{ rawName: 'XYZXYZ' }], 10);

      expect(result[0].matchedVia).toBe('none');
    });
  });
});
