import {
  assertDoesNotThrow,
  assertThrows
} from 'backend/database/tests/helpers';
import { ModelNameEnum } from 'models/types';
import { default as tape, default as test } from 'tape';
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
import { StockMovement } from '../StockMovement';
import { MovementType } from '../types';
import { getItem, getSLEs, getStockMovement } from './helpers';

const fyo = getTestFyo();

setupTestFyo(fyo, __filename);

const itemMap = {
  Pen: {
    name: 'Pen',
    rate: 700,
  },
  Ink: {
    name: 'Ink',
    rate: 50,
  },
};

const locationMap = {
  LocationOne: 'LocationOne',
  LocationTwo: 'LocationTwo',
};

/**
 * Section 1: Test Creation of Items and Locations
 */

test('create dummy items & locations', async (t) => {
  // Create Items
  for (const { name, rate } of Object.values(itemMap)) {
    const item = getItem(name, rate);
    await fyo.doc.getNewDoc(ModelNameEnum.Item, item).sync();
    t.ok(await fyo.db.exists(ModelNameEnum.Item, name), `${name} exists`);
  }

  // Create Locations
  for (const name of Object.values(locationMap)) {
    await fyo.doc.getNewDoc(ModelNameEnum.Location, { name }).sync();
    t.ok(await fyo.db.exists(ModelNameEnum.Location, name), `${name} exists`);
  }
});

/**
 * Section 2: Test Creation of Stock Movements
 */

test('create stock movement, material receipt', async (t) => {
  const { rate } = itemMap.Ink;
  const quantity = 2;
  const amount = rate * quantity;
  const stockMovement = await getStockMovement(
    MovementType.MaterialReceipt,
    new Date('2022-11-03T09:57:04.528'),
    [
      {
        item: itemMap.Ink.name,
        to: locationMap.LocationOne,
        quantity,
        rate,
      },
    ],
    fyo
  );

  await (await stockMovement.sync()).submit();
  t.ok(stockMovement.name?.startsWith('SMOV-'));
  t.equal(stockMovement.amount?.float, amount);
  t.equal(stockMovement.items?.[0].amount?.float, amount);

  const name = stockMovement.name!;

  const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
  t.equal(sles.length, 1);

  const sle = sles[0];
  t.notEqual(new Date(sle.date).toString(), 'Invalid Date');
  t.equal(parseInt(sle.name), 1);
  t.equal(sle.item, itemMap.Ink.name);
  t.equal(parseFloat(sle.rate), rate);
  t.equal(sle.quantity, quantity);
  t.equal(sle.location, locationMap.LocationOne);
  t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), quantity);
});

test('create stock movement, material transfer', async (t) => {
  const { rate } = itemMap.Ink;
  const quantity = 2;

  const stockMovement = await getStockMovement(
    MovementType.MaterialTransfer,
    new Date('2022-11-03T09:58:04.528'),
    [
      {
        item: itemMap.Ink.name,
        from: locationMap.LocationOne,
        to: locationMap.LocationTwo,
        quantity,
        rate,
      },
    ],
    fyo
  );

  await (await stockMovement.sync()).submit();
  const name = stockMovement.name!;

  const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
  t.equal(sles.length, 2);

  for (const sle of sles) {
    t.notEqual(new Date(sle.date).toString(), 'Invalid Date');
    t.equal(sle.item, itemMap.Ink.name);
    t.equal(parseFloat(sle.rate), rate);

    if (sle.location === locationMap.LocationOne) {
      t.equal(sle.quantity, -quantity);
    } else if (sle.location === locationMap.LocationTwo) {
      t.equal(sle.quantity, quantity);
    } else {
      t.ok(false, 'no-op');
    }
  }

  t.equal(
    await fyo.db.getStockQuantity(itemMap.Ink.name, locationMap.LocationOne),
    0
  );
  t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), quantity);
});

test('create stock movement, material issue', async (t) => {
  const { rate } = itemMap.Ink;
  const quantity = 2;

  const stockMovement = await getStockMovement(
    MovementType.MaterialIssue,
    new Date('2022-11-03T09:59:04.528'),
    [
      {
        item: itemMap.Ink.name,
        from: locationMap.LocationTwo,
        quantity,
        rate,
      },
    ],
    fyo
  );

  await (await stockMovement.sync()).submit();
  const name = stockMovement.name!;

  const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
  t.equal(sles.length, 1);

  const sle = sles[0];
  t.notEqual(new Date(sle.date).toString(), 'Invalid Date');
  t.equal(sle.item, itemMap.Ink.name);
  t.equal(parseFloat(sle.rate), rate);
  t.equal(sle.quantity, -quantity);
  t.equal(sle.location, locationMap.LocationTwo);
  t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), 0);
});

/**
 * Section 3: Test Cancellation of Stock Movements
 */

test('cancel stock movement', async (t) => {
  const names = (await fyo.db.getAllRaw(ModelNameEnum.StockMovement)) as {
    name: string;
  }[];

  for (const { name } of names) {
    const slesBefore = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
    const doc = (await fyo.doc.getDoc(
      ModelNameEnum.StockMovement,
      name
    )) as StockMovement;

    if (doc.movementType === MovementType.MaterialTransfer) {
      t.equal(slesBefore.length, (doc.items?.length ?? 0) * 2);
    } else {
      t.equal(slesBefore.length, doc.items?.length ?? 0);
    }

    await doc.cancel();
    const slesAfter = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
    t.equal(slesAfter.length, 0);
  }

  t.equal(await fyo.db.getStockQuantity(itemMap.Ink.name), null);
});

/**
 * Section 4: Test Invalid entries
 */

async function runEntries(
  item: string,
  entries: {
    type: MovementType;
    date: Date;
    valid: boolean;
    postQuantity: number;
    items: {
      item: string;
      to?: string;
      from?: string;
      quantity: number;
      rate: number;
    }[];
  }[],
  t: tape.Test
) {
  for (const { type, date, items, valid, postQuantity } of entries) {
    const stockMovement = await getStockMovement(type, date, items, fyo);
    await stockMovement.sync();

    if (valid) {
      await assertDoesNotThrow(async () => await stockMovement.submit());
    } else {
      await assertThrows(async () => await stockMovement.submit());
    }

    t.equal(await fyo.db.getStockQuantity(item), postQuantity);
  }
}

test('create stock movements, invalid entries, in sequence', async (t) => {
  const { name: item, rate } = itemMap.Ink;
  const quantity = 10;
  await runEntries(
    item,
    [
      {
        type: MovementType.MaterialReceipt,
        date: new Date('2022-11-03T09:58:04.528'),
        valid: true,
        postQuantity: quantity,
        items: [
          {
            item,
            to: locationMap.LocationOne,
            quantity,
            rate,
          },
        ],
      },
      {
        type: MovementType.MaterialTransfer,
        date: new Date('2022-11-03T09:58:05.528'),
        valid: false,
        postQuantity: quantity,
        items: [
          {
            item,
            from: locationMap.LocationOne,
            to: locationMap.LocationTwo,
            quantity: quantity + 1,
            rate,
          },
        ],
      },
      {
        type: MovementType.MaterialIssue,
        date: new Date('2022-11-03T09:58:06.528'),
        valid: false,
        postQuantity: quantity,
        items: [
          {
            item,
            from: locationMap.LocationOne,
            quantity: quantity + 1,
            rate,
          },
        ],
      },
      {
        type: MovementType.MaterialTransfer,
        date: new Date('2022-11-03T09:58:07.528'),
        valid: true,
        postQuantity: quantity,
        items: [
          {
            item,
            from: locationMap.LocationOne,
            to: locationMap.LocationTwo,
            quantity,
            rate,
          },
        ],
      },
      {
        type: MovementType.MaterialIssue,
        date: new Date('2022-11-03T09:58:08.528'),
        valid: true,
        postQuantity: 0,
        items: [
          {
            item,
            from: locationMap.LocationTwo,
            quantity,
            rate,
          },
        ],
      },
    ],
    t
  );
});

test('create stock movements, invalid entries, out of sequence', async (t) => {
  const { name: item, rate } = itemMap.Ink;
  const quantity = 10;
  await runEntries(
    item,
    [
      {
        type: MovementType.MaterialReceipt,
        date: new Date('2022-11-15'),
        valid: true,
        postQuantity: quantity,
        items: [
          {
            item,
            to: locationMap.LocationOne,
            quantity,
            rate,
          },
        ],
      },
      {
        type: MovementType.MaterialIssue,
        date: new Date('2022-11-17'),
        valid: true,
        postQuantity: quantity - 5,
        items: [
          {
            item,
            from: locationMap.LocationOne,
            quantity: quantity - 5,
            rate,
          },
        ],
      },
      {
        type: MovementType.MaterialTransfer,
        date: new Date('2022-11-16'),
        valid: false,
        postQuantity: quantity - 5,
        items: [
          {
            item,
            from: locationMap.LocationOne,
            to: locationMap.LocationTwo,
            quantity,
            rate,
          },
        ],
      },
    ],
    t
  );
});

closeTestFyo(fyo, __filename);
