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,
    { commitReceiptMatches: jest.fn() } 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', () => {
    function makeContext(aliases: any[], products: any[], unitMappings: any[] = [], userId?: number) {
      return { userId, aliases, products, unitMappings, categories, aiEnabled: false };
    }

    it('prioriterar user-alias före global alias för samma receiptName', async () => {
      const aliases = [
        {
          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' },
          },
        },
      ];

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

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

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

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

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

    it('normaliserar whitespace vid alias-lookup', async () => {
      const aliases = [
        {
          receiptName: 'arla mjolk 1l',
          productId: 700,
          product: {
            id: 700,
            name: 'Arla Mjolk 1l',
            canonicalName: 'Mjolk',
            categoryId: 30,
            categoryRef: { id: 30, name: 'Mejeri' },
          },
        },
      ];

      const context = makeContext(aliases, [], [], 42);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: '  ARLA   MJOLK   1L  ' }, context);

      expect(result.matchedProductId).toBe(700);
      expect(result.matchedVia).toBe('alias');
    });

    it('flöde: manuell korrigering lär alias och nästa import matchar direkt', async () => {
      const products = [
        {
          id: 700,
          name: 'Arla Mjolk 1l',
          canonicalName: 'Mjolk',
          categoryId: 30,
          categoryRef: { id: 30, name: 'Mejeri' },
        },
      ];

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

      expect(first.matchedProductId).toBeUndefined();
      expect(first.suggestedProductId).toBe(700);

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

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

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

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

      const unitMappings = [{ productId: 501, originalUnit: 'l', preferredUnit: 'st' }];
      const context = makeContext(aliases, [], unitMappings, 77);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: 'MJOLK 1L', unit: 'L' }, context);

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

  describe('matchedVia', () => {
    function makeContext(aliases: any[], products: any[], unitMappings: any[] = [], userId?: number) {
      return { userId, aliases, products, unitMappings, categories, aiEnabled: false };
    }

    it('sätter matchedVia: alias vid aliasträff', async () => {
      const aliases = [
        {
          receiptName: 'snickers',
          productId: 222,
          product: { id: 222, name: 'Snickers', canonicalName: 'Snickers', categoryId: null, categoryRef: null },
        },
      ];
      const context = makeContext(aliases, [], [], 10);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: 'SNICKERS' }, context);

      expect(result.matchedVia).toBe('alias');
    });

    it('sätter matchedVia: wordmatch vid ordbaserad matchning', async () => {
      const products = [
        { id: 300, name: 'Mjolk', canonicalName: 'Mjolk', categoryId: null, categoryRef: null },
      ];
      const context = makeContext([], products, [], 10);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: 'MJOLK 1L' }, context);

      expect(result.matchedVia).toBe('wordmatch');
    });

    it('sätter matchedVia: none när ingen matchning finns', async () => {
      const context = makeContext([], [], [], 10);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: 'XYZXYZ' }, context);

      expect(result.matchedVia).toBe('none');
    });
  });

  describe('AI fallback och prioriteringskedja', () => {
    function makeContext(
      aliases: any[],
      products: any[],
      aiEnabled: boolean,
      unitMappings: any[] = [],
      userId?: number,
    ) {
      return { userId, aliases, products, unitMappings, categories, aiEnabled };
    }

    it('använder AI-fallback när ingen alias/wordmatch/regelträff finns och aiEnabled=true', async () => {
      aiServiceMock.suggestCategory.mockResolvedValue({
        categoryId: 42,
        categoryName: 'Kyld juice & nektar',
        path: 'Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar',
        confidence: 'low',
      });

      const context = makeContext([], [], true, [], 10);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: 'XYZXYZ 123' }, context);

      expect(aiServiceMock.suggestCategory).toHaveBeenCalledWith('XYZXYZ 123', categories);
      expect(result.matchedVia).toBe('none');
      expect(result.categorySuggestion?.categoryId).toBe(42);
      expect(result.categorySuggestion?.path).toBe('Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar');
    });

    it('kallar inte AI när aiEnabled=false och ingen annan kategorisering finns', async () => {
      const context = makeContext([], [], false, [], 10);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: 'XYZXYZ 123' }, context);

      expect(aiServiceMock.suggestCategory).not.toHaveBeenCalled();
      expect(result.categorySuggestion).toBeUndefined();
      expect(result.matchedVia).toBe('none');
    });

    it('hoppar över AI när kategori redan satts via produktmatchning', async () => {
      const products = [
        {
          id: 808,
          name: 'Mjolk',
          canonicalName: 'Mjolk',
          categoryRef: { id: 30, name: 'Mejeri, ost & ägg' },
        },
      ];

      const context = makeContext([], products, true, [], 10);
      const result = await (service as any).matchAndEnrichReceiptItem({ rawName: 'MJOLK 1L' }, context);

      expect(result.matchedVia).toBe('wordmatch');
      expect(result.categorySuggestion?.categoryId).toBe(30);
      expect(aiServiceMock.suggestCategory).not.toHaveBeenCalled();
    });

    it('följer prioritering: user alias -> global alias -> wordmatch -> AI', async () => {
      aiServiceMock.suggestCategory.mockResolvedValue({
        categoryId: 51,
        categoryName: 'Godis',
        path: 'Glass, godis & snacks > Godis',
        confidence: 'low',
      });

      const globalAlias = {
        receiptName: 'mixad vara',
        productId: 900,
        product: {
          id: 900,
          name: 'Global Produkt',
          canonicalName: 'Global Produkt',
          categoryRef: { id: 53, name: 'Choklad' },
        },
      };

      const userAlias = {
        receiptName: 'mixad vara',
        productId: 901,
        product: {
          id: 901,
          name: 'User Produkt',
          canonicalName: 'User Produkt',
          categoryRef: { id: 30, name: 'Mejeri, ost & ägg' },
        },
      };

      const wordProducts = [
        {
          id: 777,
          name: 'Specialprodukt',
          canonicalName: 'Specialprodukt',
          categoryRef: null,
        },
      ];

      const userAliasContext = makeContext([userAlias, globalAlias], wordProducts, true, [], 10);
      const userAliasResult = await (service as any).matchAndEnrichReceiptItem({ rawName: 'MIXAD VARA' }, userAliasContext);
      expect(userAliasResult.matchedVia).toBe('alias');
      expect(userAliasResult.matchedProductId).toBe(901);

      const globalAliasContext = makeContext([globalAlias], wordProducts, true, [], 10);
      const globalAliasResult = await (service as any).matchAndEnrichReceiptItem({ rawName: 'MIXAD VARA' }, globalAliasContext);
      expect(globalAliasResult.matchedVia).toBe('alias');
      expect(globalAliasResult.matchedProductId).toBe(900);

      const wordMatchContext = makeContext([], wordProducts, true, [], 10);
      const wordMatchResult = await (service as any).matchAndEnrichReceiptItem({ rawName: 'SPECIALPRODUKT 1st' }, wordMatchContext);
      expect(wordMatchResult.matchedVia).toBe('wordmatch');
      expect(wordMatchResult.suggestedProductId).toBe(777);

      const aiFallbackContext = makeContext([], [], true, [], 10);
      const aiFallbackResult = await (service as any).matchAndEnrichReceiptItem({ rawName: 'helt okänd vara' }, aiFallbackContext);
      expect(aiFallbackResult.matchedVia).toBe('none');
      expect(aiServiceMock.suggestCategory).toHaveBeenCalled();
      expect(aiFallbackResult.categorySuggestion?.categoryId).toBe(51);
    });
  });
});
