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

describe('Outbound: orders -> wave -> RF pick (short) -> pack -> load -> dispatch', () => {
  const h = new Harness();
  let t: TenantClient;
  let storerId: string;
  let skuId: string;
  let o1: { id: string; orderNumber: string };
  let o2: { id: string };
  let shipmentId: string;

  beforeAll(async () => {
    await h.start();
    t = await h.tenant('out');
    const w = await t.standardWorld();
    storerId = w.storer.id;
    skuId = w.sku.id;
    await t.receiveStandardAsn(storerId); // 96 AVAILABLE in a pick face, 12 DAMAGED
  });
  afterAll(() => h.stop());

  it('ATP excludes damaged stock', async () => {
    const atp = (await t.get('/outbound/atp', { warehouseId: t.warehouseId, storerId, skuId }).expect(200)).body;
    expect(Number(atp.available ?? atp.atp ?? atp[0]?.available)).toBe(96);
  });

  it('creates orders and a wave that allocates (FIFO/FEFO) and releases pick tasks', async () => {
    o1 = (await t.post('/outbound/orders', { warehouseId: t.warehouseId, storerId, externalRef: 'SO-1', shipTo: { name: 'Store A', address: { city: 'Kuwait' } }, lines: [{ skuCode: 'WIDGET-1', orderedQty: '5', packLevel: 'CASE' }] }).expect(201)).body;
    o2 = (await t.post('/outbound/orders', { warehouseId: t.warehouseId, storerId, externalRef: 'SO-2', shipTo: { name: 'Store B' }, lines: [{ skuCode: 'WIDGET-1', orderedQty: '3', packLevel: 'CASE' }] }).expect(201)).body;
    const wave = (await t.post('/outbound/waves', { warehouseId: t.warehouseId, strategy: 'BY_ORDER', orderIds: [o1.id, o2.id], allocateNow: true, releaseNow: true }).expect(201)).body;
    expect(wave.status).toBe('RELEASED');
    expect(wave.orderCount).toBe(2);
    expect(wave.taskCount).toBeGreaterThanOrEqual(2);
    expect(wave.shortLines).toBe(0);
    const atp = (await t.get('/outbound/atp', { warehouseId: t.warehouseId, storerId, skuId }).expect(200)).body;
    expect(Number(atp.available ?? atp.atp)).toBe(0); // 96 - 60 - 36
    const pickList = (await t.post(`/outbound/waves/${wave.id}/pick-list`).expect(201)).body;
    expect(pickList.number).toBeTruthy();
  });

  it('RF picking: full pick, then a short pick raises a spot count and re-allocation attempt', async () => {
    const first = (await t.post('/tasks/next', { warehouseId: t.warehouseId, types: ['PICK'] }).expect(201)).body.task;
    expect(first.type).toBe('PICK');
    await t.post(`/tasks/${first.id}/confirm`, { fromLocationCode: first.fromLocationCode, barcode: 'CS-WIDGET-1', toLpnNumber: 'TOTE-1' }).expect(201);

    const second = (await t.post('/tasks/next', { warehouseId: t.warehouseId, types: ['PICK'] }).expect(201)).body.task;
    const r = (await t.post(`/tasks/${second.id}/confirm`, { fromLocationCode: second.fromLocationCode, barcode: 'CS-WIDGET-1', qty: '2', packLevel: 'CASE', shortQty: '1', toLpnNumber: 'TOTE-2' }).expect(201)).body;
    expect(JSON.stringify(r)).toMatch(/short/i);

    const counts = (await t.get('/inventory/counts', { warehouseId: t.warehouseId }).expect(200)).body.items;
    expect(counts.some((c: { type: string }) => c.type === 'SPOT')).toBe(true);
  });

  it('an unresolved short pick blocks packing until a CSO re-allocates or accepts the short shipment', async () => {
    const shortOrder = (await t.get(`/outbound/orders/${o2.id}`).expect(200)).body;
    expect(shortOrder.attributes.shortPending).toBe(true);
    const blocked = await t.post('/outbound/packing/start', { orderId: o2.id }).expect(422);
    expect(blocked.body.message).toMatch(/short/i);
    const accepted = (await t.post(`/outbound/orders/${o2.id}/accept-short`, { notes: 'customer agreed' }).expect(201)).body;
    expect(accepted.attributes.shortPending).toBe(false);
    expect(accepted.attributes.shortAccepted.notes).toBe('customer agreed');
    await t.post(`/outbound/orders/${o2.id}/accept-short`, {}).expect(422); // nothing pending any more
    // a CSO may instead re-allocate a released order; new reservations get pick tasks straight away
    const re = (await t.post('/outbound/orders/allocate', { orderIds: [o2.id] }).expect(201)).body;
    expect(re[0].orderId).toBe(o2.id);
    const tasksForOrder = (await t.get('/tasks', { warehouseId: t.warehouseId, refType: 'ORDER', refId: o2.id }).expect(200)).body.items;
    expect(tasksForOrder.length).toBeGreaterThanOrEqual(1);
  });

  it('order statuses reflect picking progress', async () => {
    const orders = (await t.get('/outbound/orders', { warehouseId: t.warehouseId }).expect(200)).body.items;
    const statuses = Object.fromEntries(orders.map((o: { externalRef: string; status: string }) => [o.externalRef, o.status]));
    expect(['PICKED', 'PICKING']).toContain(statuses['SO-1']);
    expect(['PICKED', 'PICKING', 'PARTIALLY_ALLOCATED', 'ALLOCATED', 'RELEASED']).toContain(statuses['SO-2']);
  });

  it('packs the fully picked order into a carton and completes with a packing list', async () => {
    // Make sure SO-1 is the fully picked one; pick its remaining task if any.
    for (let i = 0; i < 3; i++) {
      const o = (await t.get(`/outbound/orders/${o1.id}`).expect(200)).body;
      if (o.status === 'PICKED') break;
      const next = (await t.post('/tasks/next', { warehouseId: t.warehouseId, types: ['PICK'] }).expect(201)).body.task;
      if (!next) break;
      await t.post(`/tasks/${next.id}/confirm`, { fromLocationCode: next.fromLocationCode, barcode: 'CS-WIDGET-1', toLpnNumber: `TOTE-${3 + i}` }).expect(201);
    }
    const sh = (await t.post('/outbound/packing/start', { orderId: o1.id }).expect(201)).body;
    shipmentId = sh.id;
    expect(sh.shipmentNumber).toMatch(/^SH-/);
    const line = sh.lines[0];
    const carton = (await t.post('/outbound/packing/carton', { shipmentId, lines: [{ orderLineId: line.id, qty: String(line.toPack), lotNumber: 'L1', expiryDate: '2027-01-01' }], weightKg: '30' }).expect(201)).body;
    expect(carton.cartonCount).toBe(1);
    const done = (await t.post('/outbound/packing/complete', { shipmentId }).expect(201)).body;
    expect(['PACKED', 'STAGED']).toContain(done.status); // auto-staged when a staging location exists
    expect(done.packingListDocumentId).toBeTruthy();
  });

  it('builds a load, confirms loading, dispatches with gate pass and DN, and posts goods issue', async () => {
    const load = (await t.post('/outbound/loads', { warehouseId: t.warehouseId, vehicleNo: 'KWT 9999', driverName: 'Ali', carrierName: 'Own fleet', shipmentIds: [shipmentId] }).expect(201)).body;
    await t.post(`/outbound/loads/${load.id}/shipments/${shipmentId}/loaded`).expect(201);
    const dp = (await t.post(`/outbound/loads/${load.id}/dispatch`, { sealNo: 'SL-1' }).expect(201)).body;
    expect(dp.status).toBe('DISPATCHED');
    expect(dp.gatePassNumber).toMatch(/^GP-/);
    expect(dp.shipments[0].dnNumber).toMatch(/^DN-/);
    const order = (await t.get(`/outbound/orders/${o1.id}`).expect(200)).body;
    expect(order.status).toBe('SHIPPED');
    expect(Number(order.totalShippedQty)).toBe(60);
    const gp = await t.get(`/documents/${dp.gatePassDocumentId}/download`).expect(200);
    expect(gp.headers['content-type']).toContain('application/pdf');
  });

  it('ledger: shipped quantity left the warehouse, no negative balances', async () => {
    const balances = (await t.get('/inventory/balances', { warehouseId: t.warehouseId, groupBy: 'BALANCE' }).expect(200)).body.items;
    for (const b of balances) {
      expect(Number(b.qtyOnHand)).toBeGreaterThanOrEqual(0);
      expect(Number(b.qtyAllocated)).toBeLessThanOrEqual(Number(b.qtyOnHand));
    }
    const movs = (await t.get('/inventory/movements', { warehouseId: t.warehouseId, type: 'SHIP' }).expect(200)).body.items;
    expect(movs.reduce((a: number, m: { qty: string }) => a + Number(m.qty), 0)).toBe(60);
  });

  it('cancelling an allocated order releases its allocations', async () => {
    const o3 = (await t.post('/outbound/orders', { warehouseId: t.warehouseId, storerId, externalRef: 'SO-3', shipTo: { name: 'Store C' }, lines: [{ skuCode: 'WIDGET-1', orderedQty: '1', packLevel: 'EACH' }] }).expect(201)).body;
    const before = (await t.get('/outbound/atp', { warehouseId: t.warehouseId, storerId, skuId }).expect(200)).body;
    const alloc = (await t.post('/outbound/orders/allocate', { orderIds: [o3.id] }).expect(201)).body;
    expect(alloc).toBeTruthy();
    await t.post(`/outbound/orders/${o3.id}/cancel`, { reason: 'customer cancelled' }).expect(201);
    const after = (await t.get('/outbound/atp', { warehouseId: t.warehouseId, storerId, skuId }).expect(200)).body;
    expect(Number(after.available ?? after.atp)).toBe(Number(before.available ?? before.atp));
  });
});
