import { BadRequestException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { ReceiptImportService } from './receipt-import.service';
import { SaveReceiptDto } from './dto/save-receipt.dto';

// ──────────────────────────────────────────────────────────────────────────────
// Hjälpfunktioner
// ──────────────────────────────────────────────────────────────────────────────

function makeItem(overrides: Partial<SaveReceiptDto['items'][0]> = {}): SaveReceiptDto['items'][0] {
  return {
    rawName: 'ARLA MJOLK 1L',
    quantity: 2,
    unit: 'st',
    destination: 'inventory',
    productId: 100,
    ...overrides,
  } as SaveReceiptDto['items'][0];
}

// ──────────────────────────────────────────────────────────────────────────────
// Spec
// ──────────────────────────────────────────────────────────────────────────────

describe('ReceiptImportService.saveReceipt', () => {
  let txMock: {
    product: { findUnique: jest.Mock; create: jest.Mock; update: jest.Mock };
    inventoryItem: { create: jest.Mock; update: jest.Mock };
    pantryItem: { create: jest.Mock };
    receiptAlias: { upsert: jest.Mock };
    unitMapping: { upsert: jest.Mock };
  };

  let prismaMock: {
    pantryItem: { findMany: jest.Mock };
    inventoryItem: { findMany: jest.Mock };
    $transaction: jest.Mock;
  };

  let service: ReceiptImportService;

  beforeEach(() => {
    txMock = {
      product: {
        findUnique: jest.fn(),
        create: jest.fn(),
        update: jest.fn(),
      },
      inventoryItem: {
        create: jest.fn().mockResolvedValue({ id: 1 }),
        update: jest.fn().mockResolvedValue({ id: 1 }),
      },
      pantryItem: {
        create: jest.fn().mockResolvedValue({ id: 1 }),
      },
      receiptAlias: {
        upsert: jest.fn().mockResolvedValue({ id: 1 }),
      },
      unitMapping: {
        upsert: jest.fn().mockResolvedValue({ id: 1 }),
      },
    };

    prismaMock = {
      pantryItem: { findMany: jest.fn().mockResolvedValue([]) },
      inventoryItem: { findMany: jest.fn().mockResolvedValue([]) },
      $transaction: jest.fn().mockImplementation(async (cb: (tx: typeof txMock) => Promise<void>) => cb(txMock)),
    };

    service = new ReceiptImportService(
      prismaMock as any,
      {} as any, // aiService – används ej i saveReceipt
      {} as any, // categoriesService – används ej i saveReceipt
      { commitReceiptMatches: jest.fn().mockResolvedValue({ boughtCount: 0, ambiguousCount: 0, unmatchedCount: 0 }) } as any,
    );
  });

  // ── 1. Skapar ny inventariepost ─────────────────────────────────────────────
  it('skapar ny inventariepost när produkten finns och inte finns i inventariet', async () => {
    txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });

    const dto: SaveReceiptDto = { items: [makeItem()] };
    const result = await service.saveReceipt(1, dto);

    expect(txMock.inventoryItem.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ productId: 100, userId: 1 }),
      }),
    );
    expect(result.created).toBe(1);
    expect(result.merged).toBe(0);
    expect(result.errors).toHaveLength(0);
  });

  // ── 2. Slår samman mängd när produkten redan finns i inventariet ────────────
  it('slår samman mängd när produkten redan finns i inventariet', async () => {
    prismaMock.inventoryItem.findMany.mockResolvedValue([
      { id: 50, productId: 100, quantity: new Prisma.Decimal(3), unit: 'st' },
    ]);
    txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });

    const dto: SaveReceiptDto = { items: [makeItem({ quantity: 2 })] };
    const result = await service.saveReceipt(1, dto);

    expect(txMock.inventoryItem.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: 50 },
        data: { quantity: { increment: new Prisma.Decimal(2) } },
      }),
    );
    expect(result.merged).toBe(1);
    expect(result.created).toBe(0);
  });

  // ── 3. Lägger till i skafferi ───────────────────────────────────────────────
  it('lägger till i skafferi när destination är pantry och produkten inte finns där', async () => {
    txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });

    const dto: SaveReceiptDto = { items: [makeItem({ destination: 'pantry' })] };
    const result = await service.saveReceipt(1, dto);

    expect(txMock.pantryItem.create).toHaveBeenCalledWith(
      expect.objectContaining({ data: { userId: 1, productId: 100 } }),
    );
    expect(result.pantryAdded).toBe(1);
    expect(result.pantrySkipped).toBe(0);
  });

  // ── 4. Hoppar över skafferidublett ──────────────────────────────────────────
  it('hoppar över produkt som redan finns i skafferiet', async () => {
    prismaMock.pantryItem.findMany.mockResolvedValue([{ productId: 100 }]);
    txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });

    const dto: SaveReceiptDto = { items: [makeItem({ destination: 'pantry' })] };
    const result = await service.saveReceipt(1, dto);

    expect(txMock.pantryItem.create).not.toHaveBeenCalled();
    expect(result.pantrySkipped).toBe(1);
    expect(result.pantryAdded).toBe(0);
  });

  // ── 5. Lär in alias ─────────────────────────────────────────────────────────
  it('lär in alias när learnAlias är true', async () => {
    txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });

    const dto: SaveReceiptDto = {
      items: [makeItem({ learnAlias: true, learnAliasGlobally: false })],
    };
    const result = await service.saveReceipt(1, dto);

    expect(txMock.receiptAlias.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        create: expect.objectContaining({
          receiptName: 'arla mjolk 1l',
          productId: 100,
          isGlobal: false,
          ownerId: 1,
        }),
      }),
    );
    expect(result.aliasesLearned).toBe(1);
  });

  // ── 6. Skapar ny privat produkt via createProductName ───────────────────────
  it('skapar ny produkt när createProductName anges och produkten inte finns', async () => {
    txMock.product.findUnique.mockResolvedValue(null); // ingen befintlig

    txMock.product.create.mockResolvedValue({ id: 200 });

    const dto: SaveReceiptDto = {
      items: [
        makeItem({ productId: undefined, createProductName: 'Hemmagjord Senap' }),
      ],
    };
    const result = await service.saveReceipt(1, dto);

    expect(txMock.product.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          name: 'Hemmagjord Senap',
          isPrivate: true,
          ownerId: 1,
        }),
      }),
    );
    expect(result.created).toBe(1);
    expect(result.errors).toHaveLength(0);
  });

  // ── 7. Produkt hittades inte – registrerar fel utan att kasta ───────────────
  it('registrerar fel för okänt productId utan att kasta exception', async () => {
    txMock.product.findUnique.mockResolvedValue(null);

    const dto: SaveReceiptDto = { items: [makeItem({ productId: 999 })] };
    const result = await service.saveReceipt(1, dto);

    expect(result.errors).toHaveLength(1);
    expect(result.errors![0].index).toBe(0);
    expect(result.created).toBe(0);
  });

  // ── 8. Kastar BadRequestException om transaktionen misslyckas ───────────────
  it('kastar BadRequestException om $transaction kastar', async () => {
    prismaMock.$transaction.mockRejectedValue(new Error('DB krasch'));

    const dto: SaveReceiptDto = { items: [makeItem()] };

    await expect(service.saveReceipt(1, dto)).rejects.toThrow(BadRequestException);
  });
});
