import { Harness, TenantClient } from './harness';

describe('Inbound: ASN -> receive -> putaway -> close', () => {
  const h = new Harness();
  let t: TenantClient;
  let storerId: string;

  beforeAll(async () => {
    await h.start();
    t = await h.tenant('inb');
    storerId = (await t.standardWorld()).storer.id;
  });
  afterAll(() => h.stop());

  it('creates an ASN with case-pack lines converted to base units', async () => {
    const asn = (await t.post('/inbound/asns', { warehouseId: t.warehouseId, storerId, externalRef: 'PO-1001', containerSize: '40', lines: [{ skuCode: 'WIDGET-1', expectedQty: '10', packLevel: 'CASE', expectedLot: 'L1' }] }).expect(201)).body;
    expect(asn.asnNumber).toMatch(/^ASN-KW1-\d{6}$/);
    expect(asn.status).toBe('EXPECTED');
    expect(Number(asn.totalExpectedQty)).toBe(120);
    expect(asn.lines[0].packLevel).toBe('CASE');
  });

  it('rejects a duplicate open external reference for the same storer', async () => {
    const res = await t.post('/inbound/asns', { warehouseId: t.warehouseId, storerId, externalRef: 'PO-1001', lines: [{ skuCode: 'WIDGET-1', expectedQty: '1' }] }).expect(409);
    expect(res.body.code).toBe('DUPLICATE');
  });

  describe('receiving', () => {
    let asn: { id: string; lines: { id: string }[] };
    let lineId: string;

    beforeAll(async () => {
      asn = (await t.get('/inbound/asns', { externalRef: 'PO-1001' }).expect(200)).body.items[0];
      asn = (await t.get(`/inbound/asns/${asn.id}`).expect(200)).body;
      lineId = asn.lines[0].id;
      const arrived = (await t.post(`/inbound/asns/${asn.id}/arrive`, { vehicleNo: 'KWT 1234', sealNo: 'S-77' }).expect(201)).body;
      expect(arrived.status).toBe('ARRIVED');
    });

    it('receives by barcode, creates an LPN on the dock and a directed putaway task', async () => {
      const r = (await t.post(`/inbound/asns/${asn.id}/receive`, { asnLineId: lineId, barcode: 'CS-WIDGET-1', qty: '8', lotNumber: 'L1', expiryDate: '2027-01-01', idempotencyKey: 'rcv-1' }).expect(201)).body;
      expect(r.lpnNumber).toMatch(/^LP-/);
      expect(r.putawayTask.toLocationCode).toMatch(/^A-0\d-\d$/); // directed into the AMB zone (ground level is pick)
      expect(r.line.status).toBe('PARTIAL');
    });

    it('is idempotent on the same idempotency key', async () => {
      const r = (await t.post(`/inbound/asns/${asn.id}/receive`, { asnLineId: lineId, barcode: 'CS-WIDGET-1', qty: '8', lotNumber: 'L1', expiryDate: '2027-01-01', idempotencyKey: 'rcv-1' }).expect(201)).body;
      expect(r.idempotent).toBe(true);
      const detail = (await t.get(`/inbound/asns/${asn.id}`).expect(200)).body;
      expect(Number(detail.lines[0].receivedQty)).toBe(96);
    });

    it('requires lot and expiry for controlled SKUs', async () => {
      const res = await t.post(`/inbound/asns/${asn.id}/receive`, { asnLineId: lineId, qty: '1', packLevel: 'CASE' }).expect(422);
      expect(['LOT_REQUIRED', 'EXPIRY_REQUIRED']).toContain(res.body.code);
    });

    it('routes damaged stock to the damage location and records a discrepancy', async () => {
      const rc = (await t.get('/reason-codes', { type: 'DAMAGE' }).expect(200)).body[0];
      const r = (await t.post(`/inbound/asns/${asn.id}/receive`, { asnLineId: lineId, qty: '1', packLevel: 'CASE', lotNumber: 'L1', expiryDate: '2027-01-01', condition: 'DAMAGED', reasonCodeId: rc.id }).expect(201)).body;
      expect(r.putawayTask.toLocationCode).toBe('DMG-01');
      const detail = (await t.get(`/inbound/asns/${asn.id}`).expect(200)).body;
      const damaged = detail.discrepancies.find((d: { type: string }) => d.type === 'DAMAGED');
      expect(damaged).toBeTruthy();
      expect(Number(damaged.varianceQty)).toBe(-12);
    });

    it('blocks over-receipt beyond the storer tolerance', async () => {
      const res = await t.post(`/inbound/asns/${asn.id}/receive`, { asnLineId: lineId, qty: '5', packLevel: 'CASE', lotNumber: 'L1', expiryDate: '2027-01-01' }).expect(422);
      expect(res.body.code).toBe('OVER_RECEIPT');
    });

    it('RF putaway: next task, wrong location rejected, confirm moves the LPN', async () => {
      const next = (await t.post('/tasks/next', { warehouseId: t.warehouseId, types: ['PUTAWAY'] }).expect(201)).body;
      expect(next.task.type).toBe('PUTAWAY');
      expect(next.task.status).toBe('ASSIGNED');
      const wrong = await t.post(`/tasks/${next.task.id}/confirm`, { toLocationCode: 'DOCK-01', lpnNumber: next.task.lpnNumber }).expect(422);
      expect(wrong.body.code).toBe('WRONG_LOCATION');
      await t.post(`/tasks/${next.task.id}/confirm`, { toLocationCode: next.task.toLocationCode, lpnNumber: next.task.lpnNumber }).expect(201);
      const balances = (await t.get('/inventory/balances', { warehouseId: t.warehouseId, groupBy: 'BALANCE' }).expect(200)).body.items;
      const putaway = balances.find((b: { locationCode: string }) => b.locationCode === next.task.toLocationCode);
      expect(putaway).toBeTruthy();
      expect(Number(putaway.qtyOnHand)).toBe(96);
      expect(putaway.status).toBe('AVAILABLE');
    });

    it('closes short with a GRN, a SHORT discrepancy and a PDF', async () => {
      const closed = (await t.post(`/inbound/asns/${asn.id}/close`, { forceCloseShort: true, sendRcr: true }).expect(201)).body;
      expect(closed.status).toBe('CLOSED');
      expect(closed.grn.grnNumber).toMatch(/^GRN-/);
      const detail = (await t.get(`/inbound/asns/${asn.id}`).expect(200)).body;
      const short = detail.discrepancies.find((d: { type: string }) => d.type === 'SHORT');
      expect(Number(short.varianceQty)).toBe(-12); // 120 expected, 108 received (96 good + 12 damaged)
      const pdf = await t.get(`/documents/${closed.grn.documentId}/download`).expect(200);
      expect(pdf.headers['content-type']).toContain('application/pdf');
      expect(pdf.body.length ?? pdf.text?.length).toBeGreaterThan(1000);
    });

    it('keeps an append-only movement ledger that reconciles with balances', async () => {
      const movs = (await t.get('/inventory/movements', { warehouseId: t.warehouseId }).expect(200)).body.items;
      const types = movs.map((m: { type: string }) => m.type).sort();
      expect(types).toEqual(expect.arrayContaining(['RECEIPT', 'RECEIPT', 'PUTAWAY']));
      const receipts = movs.filter((m: { type: string }) => m.type === 'RECEIPT').reduce((a: number, m: { qty: string }) => a + Number(m.qty), 0);
      const balances = (await t.get('/inventory/balances', { warehouseId: t.warehouseId, groupBy: 'BALANCE' }).expect(200)).body.items;
      const onHand = balances.reduce((a: number, b: { qtyOnHand: string }) => a + Number(b.qtyOnHand), 0);
      expect(receipts).toBe(108);
      expect(onHand).toBe(108);
    });
  });
});
