import React, { useState, useEffect } from 'react';

import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { format as formatDate, parse as parseDate } from 'date-fns';
import { v4 as uuidv4 } from 'uuid';

import {
  generateTransaction,
  generateAccount,
  generateCategoryGroups,
} from 'loot-core/src/mocks';
import { TestProvider } from 'loot-core/src/mocks/redux';
import { initServer } from 'loot-core/src/platform/client/fetch';
import {
  addSplitTransaction,
  realizeTempTransactions,
  splitTransaction,
  updateTransaction,
} from 'loot-core/src/shared/transactions';
import { integerToCurrency } from 'loot-core/src/shared/util';

import { SelectedProviderWithItems } from '../../hooks/useSelected';
import { ResponsiveProvider } from '../../ResponsiveProvider';

import { SplitsExpandedProvider, TransactionTable } from './TransactionsTable';

jest.mock('loot-core/src/platform/client/fetch');
jest.mock('../../hooks/useFeatureFlag', () => jest.fn().mockReturnValue(false));

const accounts = [generateAccount('Bank of America')];
const payees = [
  { id: 'payed-to', name: 'Payed To' },
  { id: 'guy', name: 'This guy on the side of the road' },
];
const categoryGroups = generateCategoryGroups([
  {
    name: 'Investments and Savings',
    categories: [{ name: 'Savings' }],
  },
  {
    name: 'Usual Expenses',
    categories: [{ name: 'Food' }, { name: 'General' }, { name: 'Home' }],
  },
  {
    name: 'Projects',
    categories: [{ name: 'Big Projects' }, { name: 'Shed' }],
  },
]);
const usualGroup = categoryGroups[1];

function generateTransactions(count, splitAtIndexes = [], showError = false) {
  const transactions = [];

  for (let i = 0; i < count; i++) {
    const isSplit = splitAtIndexes.includes(i);

    transactions.push.apply(
      transactions,
      generateTransaction(
        {
          account: accounts[0].id,
          category:
            i === 0
              ? null
              : i === 1
              ? usualGroup.categories[1].id
              : usualGroup.categories[0].id,
          amount: isSplit ? 50 : undefined,
          sort_order: i,
        },
        isSplit ? 30 : undefined,
        showError,
      ),
    );
  }

  return transactions;
}

function LiveTransactionTable(props) {
  const [transactions, setTransactions] = useState(props.transactions);

  useEffect(() => {
    if (transactions === props.transactions) return;
    props.onTransactionsChange?.(transactions);
  }, [transactions]);

  const onSplit = id => {
    let { data, diff } = splitTransaction(transactions, id);
    setTransactions(data);
    return diff.added[0].id;
  };

  const onSave = transaction => {
    let { data } = updateTransaction(transactions, transaction);
    setTransactions(data);
  };

  const onAdd = newTransactions => {
    newTransactions = realizeTempTransactions(newTransactions);
    setTransactions(trans => [...newTransactions, ...trans]);
  };

  const onAddSplit = id => {
    let { data, diff } = addSplitTransaction(transactions, id);
    setTransactions(data);
    return diff.added[0].id;
  };

  const onCreatePayee = () => 'id';

  // It's important that these functions are they same instances
  // across renders. Doing so tests that the transaction table
  // implementation properly uses the right latest state even if the
  // hook dependencies haven't changed
  return (
    <TestProvider>
      <ResponsiveProvider>
        <SelectedProviderWithItems
          name="transactions"
          items={transactions}
          fetchAllIds={() => transactions.map(t => t.id)}
        >
          <SplitsExpandedProvider>
            <TransactionTable
              {...props}
              transactions={transactions}
              loadMoreTransactions={() => {}}
              payees={payees}
              addNotification={n => console.log(n)}
              onSave={onSave}
              onSplit={onSplit}
              onAdd={onAdd}
              onAddSplit={onAddSplit}
              onCreatePayee={onCreatePayee}
            />
          </SplitsExpandedProvider>
        </SelectedProviderWithItems>
      </ResponsiveProvider>
    </TestProvider>
  );
}

function initBasicServer() {
  initServer({
    query: async query => {
      switch (query.table) {
        case 'payees':
          return { data: payees, dependencies: [] };
        case 'accounts':
          return { data: accounts, dependencies: [] };
        default:
          throw new Error(`queried unknown table: ${query.table}`);
      }
    },
  });
}

beforeEach(() => {
  initBasicServer();
});

afterEach(() => {
  global.__resetWorld();
});

// Not good, see `Autocomplete.js` for details
function waitForAutocomplete() {
  return new Promise(resolve => setTimeout(resolve, 0));
}

const categories = categoryGroups.reduce(
  (all, group) => all.concat(group.categories),
  [],
);

function prettyDate(date) {
  return formatDate(parseDate(date, 'yyyy-MM-dd', new Date()), 'MM/dd/yyyy');
}

function renderTransactions(extraProps) {
  let transactions = generateTransactions(5, [6]);
  // Hardcoding the first value makes it easier for tests to do
  // various this
  transactions[0].amount = -2777;

  let defaultProps = {
    transactions,
    payees: payees,
    accounts: accounts,
    categoryGroups: categoryGroups,
    currentAccountId: accounts[0].id,
    showAccount: true,
    showCategory: true,
    showCleared: true,
    isAdding: false,
    onTransactionsChange: t => {
      transactions = t;
    },
  };

  let result = render(
    <LiveTransactionTable {...defaultProps} {...extraProps} />,
  );
  return {
    ...result,
    getTransactions: () => transactions,
    updateProps: props =>
      render(
        <LiveTransactionTable {...defaultProps} {...extraProps} {...props} />,
        { container: result.container },
      ),
  };
}

function queryNewField(container, name, subSelector = '', idx = 0) {
  const field = container.querySelectorAll(
    `[data-testid="new-transaction"] [data-testid="${name}"]`,
  )[idx];
  if (subSelector !== '') {
    return field.querySelector(subSelector);
  }
  return field;
}

function queryField(container, name, subSelector = '', idx) {
  const field = container.querySelectorAll(
    `[data-testid="transaction-table"] [data-testid="${name}"]`,
  )[idx];
  if (subSelector !== '') {
    return field.querySelector(subSelector);
  }
  return field;
}

async function _editField(field, container) {
  // We only short-circuit this for inputs
  let input = field.querySelector(`input`);
  if (input) {
    expect(container.ownerDocument.activeElement).toBe(input);
    return input;
  }

  let element;
  let buttonQuery = 'button,div[data-testid=cell-button]';

  if (field.querySelector(buttonQuery)) {
    let btn = field.querySelector(buttonQuery);
    await userEvent.click(btn);
    element = field.querySelector(':focus');
    expect(element).toBeTruthy();
  } else {
    await userEvent.click(field.querySelector('div'));
    element = field.querySelector('input');
    expect(element).toBeTruthy();
    expect(container.ownerDocument.activeElement).toBe(element);
  }

  return element;
}

function editNewField(container, name, rowIndex) {
  const field = queryNewField(container, name, '', rowIndex);
  return _editField(field, container);
}

function editField(container, name, rowIndex) {
  const field = queryField(container, name, '', rowIndex);
  return _editField(field, container);
}

function expectToBeEditingField(container, name, rowIndex, isNew) {
  let field;
  if (isNew) {
    field = queryNewField(container, name, '', rowIndex);
  } else {
    field = queryField(container, name, '', rowIndex);
  }
  const input = field.querySelector(':focus');
  expect(input).toBeTruthy();
  expect(container.ownerDocument.activeElement).toBe(input);
  return input;
}

describe('Transactions', () => {
  test('transactions table shows the correct data', () => {
    const { container, getTransactions } = renderTransactions();

    getTransactions().forEach((transaction, idx) => {
      expect(queryField(container, 'date', 'div', idx).textContent).toBe(
        prettyDate(transaction.date),
      );
      expect(queryField(container, 'account', 'div', idx).textContent).toBe(
        accounts.find(acct => acct.id === transaction.account).name,
      );
      expect(queryField(container, 'payee', 'div', idx).textContent).toBe(
        payees.find(p => p.id === transaction.payee).name,
      );
      expect(queryField(container, 'notes', 'div', idx).textContent).toBe(
        transaction.notes,
      );
      expect(queryField(container, 'category', 'div', idx).textContent).toBe(
        transaction.category
          ? categories.find(category => category.id === transaction.category)
              .name
          : 'Categorize',
      );
      if (transaction.amount <= 0) {
        expect(queryField(container, 'debit', 'div', idx).textContent).toBe(
          integerToCurrency(-transaction.amount),
        );
        expect(queryField(container, 'credit', 'div', idx).textContent).toBe(
          '',
        );
      } else {
        expect(queryField(container, 'debit', 'div', idx).textContent).toBe('');
        expect(queryField(container, 'credit', 'div', idx).textContent).toBe(
          integerToCurrency(transaction.amount),
        );
      }
    });
  });

  test('keybindings enter/tab/alt should move around', async () => {
    const { container } = renderTransactions();

    // Enter/tab goes down/right
    let input = await editField(container, 'notes', 2);
    await userEvent.type(input, '[Enter]');
    expectToBeEditingField(container, 'notes', 3);

    input = await editField(container, 'payee', 2);
    await userEvent.type(input, '[Tab]');
    expectToBeEditingField(container, 'notes', 2);

    // Shift+enter/tab goes up/left
    input = await editField(container, 'notes', 2);
    await userEvent.type(input, '{Shift>}[Enter]{/Shift}');
    expectToBeEditingField(container, 'notes', 1);

    input = await editField(container, 'payee', 2);
    await userEvent.type(input, '{Shift>}[Tab]{/Shift}');
    expectToBeEditingField(container, 'account', 2);

    // Moving forward on the last cell moves to the next row
    input = await editField(container, 'cleared', 2);
    await userEvent.type(input, '[Tab]');
    expectToBeEditingField(container, 'select', 3);

    // Moving backward on the first cell moves to the previous row
    await editField(container, 'date', 2);
    input = await editField(container, 'select', 2);
    await userEvent.type(input, '{Shift>}[Tab]{/Shift}');
    expectToBeEditingField(container, 'cleared', 1);

    // Blurring should close the input
    input = await editField(container, 'credit', 1);
    fireEvent.blur(input);
    expect(container.querySelector('input')).toBe(null);

    // When reaching the bottom it shouldn't error
    input = await editField(container, 'notes', 4);
    await userEvent.type(input, '[Enter]');

    // TODO: fix flakiness and re-enable
    // When reaching the top it shouldn't error
    // input = await editField(container, 'notes', 0);
    // await userEvent.type(input, '{Shift>}[Enter]{/Shift}');
  });

  test('keybinding escape resets the value', async () => {
    const { container } = renderTransactions();

    let input = await editField(container, 'notes', 2);
    let oldValue = input.value;
    await userEvent.clear(input);
    await userEvent.type(input, 'yo new value');
    expect(input.value).toEqual('yo new value');
    await userEvent.type(input, '[Escape]');
    expect(input.value).toEqual(oldValue);

    input = await editField(container, 'category', 2);
    oldValue = input.value;
    await userEvent.clear(input);
    await userEvent.type(input, 'Gener');
    expect(input.value).toEqual('Gener');
    await userEvent.type(input, '[Escape]');
    expect(input.value).toEqual(oldValue);
  });

  test('text fields save when moved away from', async () => {
    const { container, getTransactions } = renderTransactions();

    // All of these keys move to a different field, and the value in
    // the previous input should be saved
    const ks = [
      '[Tab]',
      '[Enter]',
      '{Shift>}[Tab]{/Shift}',
      '{Shift>}[Enter]{/Shift}',
    ];

    for (let idx in ks) {
      let input = await editField(container, 'notes', 2);
      let oldValue = input.value;
      await userEvent.clear(input);
      await userEvent.type(input, 'a happy little note' + idx);
      // It's not saved yet
      expect(getTransactions()[2].notes).toBe(oldValue);
      await userEvent.type(input, '[Tab]');
      // Now it should be saved!
      expect(getTransactions()[2].notes).toBe('a happy little note' + idx);
      expect(queryField(container, 'notes', 'div', 2).textContent).toBe(
        'a happy little note' + idx,
      );
    }

    let input = await editField(container, 'notes', 2);
    let oldValue = input.value;
    await userEvent.clear(input);
    await userEvent.type(input, 'another happy note');
    // It's not saved yet
    expect(getTransactions()[2].notes).toBe(oldValue);
    // Blur the input to make it stop editing
    await userEvent.tab();
    expect(getTransactions()[2].notes).toBe('another happy note');
  });

  test('dropdown automatically opens and can be filtered', async () => {
    const { container } = renderTransactions();

    let input = await editField(container, 'category', 2);
    let tooltip = container.querySelector('[data-testid="tooltip"]');
    expect(tooltip).toBeTruthy();
    expect(
      [...tooltip.querySelectorAll('[data-testid*="category-item"]')].length,
    ).toBe(9);

    await userEvent.clear(input);
    await userEvent.type(input, 'Gener');

    // Make sure the list is filtered, the right items exist, and the
    // first item is highlighted
    let items = tooltip.querySelectorAll('[data-testid*="category-item"]');
    expect(items.length).toBe(2);
    expect(items[0].textContent).toBe('Usual Expenses');
    expect(items[1].textContent).toBe('General');
    expect(items[1].dataset['testid']).toBe('category-item-highlighted');

    // It should also allow filtering on group names
    await userEvent.clear(input);
    await userEvent.type(input, 'Usual');

    items = tooltip.querySelectorAll('[data-testid*="category-item"]');
    expect(items.length).toBe(4);
    expect(items[0].textContent).toBe('Usual Expenses');
    expect(items[1].textContent).toBe('Food');
    expect(items[2].textContent).toBe('General');
    expect(items[3].textContent).toBe('Home');
    expect(items[1].dataset['testid']).toBe('category-item-highlighted');
  });

  test('dropdown selects an item with keyboard', async () => {
    const { container, getTransactions } = renderTransactions();

    let input = await editField(container, 'category', 2);
    let tooltip = container.querySelector('[data-testid="tooltip"]');

    // No item should be highlighted
    let highlighted = tooltip.querySelector(
      '[data-testid="category-item-highlighted"]',
    );
    expect(highlighted).toBe(null);

    await userEvent.keyboard('[ArrowDown][ArrowDown][ArrowDown][ArrowDown]');

    // The right item should be highlighted
    highlighted = tooltip.querySelector(
      '[data-testid="category-item-highlighted"]',
    );
    expect(highlighted).toBeTruthy();
    expect(highlighted.textContent).toBe('General');

    expect(getTransactions()[2].category).toBe(
      categories.find(category => category.name === 'Food').id,
    );

    await userEvent.type(input, '[Enter]');
    await waitForAutocomplete();

    // The transactions data should be updated with the right category
    expect(getTransactions()[2].category).toBe(
      categories.find(category => category.name === 'General').id,
    );

    // The category field should still be editing
    expectToBeEditingField(container, 'category', 2);
    // No dropdown should be open
    expect(container.querySelector('[data-testid="tooltip"]')).toBe(null);

    // Pressing enter should now move down
    await userEvent.type(input, '[Enter]');
    expectToBeEditingField(container, 'category', 3);
  });

  test('dropdown selects an item when clicking', async () => {
    const { container, getTransactions } = renderTransactions();

    await editField(container, 'category', 2);

    let tooltip = container.querySelector('[data-testid="tooltip"]');

    // Make sure none of the items are highlighted
    let items = tooltip.querySelectorAll('[data-testid="category-item"]');
    let highlighted = tooltip.querySelector(
      '[data-testid="category-item-highlighted"]',
    );
    expect(highlighted).toBe(null);

    // Hover over an item
    await userEvent.hover(items[2]);

    // Make sure the expected category is highlighted
    highlighted = tooltip.querySelector(
      '[data-testid="category-item-highlighted"]',
    );
    expect(highlighted).toBeTruthy();
    expect(highlighted.textContent).toBe('General');

    // Click the item and check the before/after values
    expect(getTransactions()[2].category).toBe(
      categories.find(c => c.name === 'Food').id,
    );
    await userEvent.click(items[2]);
    await waitForAutocomplete();
    expect(getTransactions()[2].category).toBe(
      categories.find(c => c.name === 'General').id,
    );

    // It should still be editing the category
    tooltip = container.querySelector('[data-testid="tooltip"]');
    expect(tooltip).toBe(null);
    expectToBeEditingField(container, 'category', 2);
  });

  test('dropdown hovers but doesn’t change value', async () => {
    const { container, getTransactions } = renderTransactions();

    let input = await editField(container, 'category', 2);
    let oldCategory = getTransactions()[2].category;
    let tooltip = container.querySelector('[data-testid="tooltip"]');

    let items = tooltip.querySelectorAll('[data-testid="category-item"]');

    // Hover over a few of the items to highlight them
    await userEvent.hover(items[2]);
    await userEvent.hover(items[3]);

    // Make sure one of them is highlighted
    let highlighted = tooltip.querySelector(
      '[data-testid="category-item-highlighted"]',
    );
    expect(highlighted).toBeTruthy();

    // Navigate away from the field with the keyboard
    await userEvent.type(input, '[Tab]');

    // Make sure the category didn't update, and that the highlighted
    // field was different than the transactions' category
    let currentCategory = getTransactions()[2].category;
    expect(currentCategory).toBe(oldCategory);
    expect(highlighted.textContent).not.toBe(
      categories.find(c => c.id === currentCategory).name,
    );
  });

  // TODO: fix this test
  test.skip('dropdown invalid value resets correctly', async () => {
    const { container, getTransactions } = renderTransactions();

    // Invalid values should be rejected and nullified
    let input = await editField(container, 'category', 2);
    await userEvent.clear(input);
    await userEvent.type(input, 'aaabbbccc');

    // For this first test case, make sure the tooltip is gone. We
    // don't need to check this in all the other cases
    let tooltipItems = container.querySelectorAll(
      '[data-testid="category-item-group"]',
    );
    expect(tooltipItems.length).toBe(0);

    expect(getTransactions()[2].category).not.toBe(null);
    await userEvent.tab();
    expect(getTransactions()[2].category).toBe(null);

    // Clear out the category value
    input = await editField(container, 'category', 3);
    await userEvent.clear(input);

    // The category should be null when the value is cleared
    expect(getTransactions()[3].category).not.toBe(null);
    await userEvent.tab();
    expect(getTransactions()[3].category).toBe(null);

    // Clear out the payee value
    input = await editField(container, 'payee', 3);
    await new Promise(resolve => setTimeout(resolve, 10));
    await userEvent.clear(input);

    // The payee should be empty when the value is cleared
    expect(getTransactions()[3].payee).not.toBe('');
    await userEvent.tab();
    expect(getTransactions()[3].payee).toBe(null);
  });

  test('dropdown escape resets the value ', async () => {
    const { container } = renderTransactions();

    let input = await editField(container, 'category', 2);
    let oldValue = input.value;
    await userEvent.type(input, 'aaabbbccc[Escape]');
    expect(input.value).toBe(oldValue);

    // The tooltip be closed
    let tooltip = container.querySelector('[data-testid="tooltip"]');
    expect(tooltip).toBeNull();
  });

  test('adding a new transaction works', async () => {
    const { queryByTestId, container, getTransactions, updateProps } =
      renderTransactions();

    expect(getTransactions().length).toBe(5);
    expect(queryByTestId('new-transaction')).toBe(null);
    updateProps({ isAdding: true });
    expect(queryByTestId('new-transaction')).toBeTruthy();

    let input = queryNewField(container, 'date', 'input');

    // The date input should exist and have a default value
    expect(input).toBeTruthy();
    expect(container.ownerDocument.activeElement).toBe(input);
    expect(input.value).not.toBe('');

    input = await editNewField(container, 'notes');
    await userEvent.clear(input);
    await userEvent.type(input, 'a transaction');

    input = await editNewField(container, 'debit');
    expect(input.value).toBe('0.00');
    await userEvent.clear(input);
    await userEvent.type(input, '100[Enter]');

    expect(getTransactions().length).toBe(6);
    expect(getTransactions()[0].amount).toBe(-10000);
    expect(getTransactions()[0].notes).toBe('a transaction');

    // The date field should be re-focused to enter a new transaction
    expect(container.ownerDocument.activeElement).toBe(
      queryNewField(container, 'date', 'input'),
    );
    expect(queryNewField(container, 'debit').textContent).toBe('0.00');
  });

  test('adding a new split transaction works', async () => {
    const { container, getTransactions, updateProps } = renderTransactions();
    updateProps({ isAdding: true });

    let input = await editNewField(container, 'debit');
    await userEvent.clear(input);
    await userEvent.type(input, '55.00');

    await editNewField(container, 'category');
    let splitButton = document.body.querySelector(
      '[data-testid="tooltip"] [data-testid="split-transaction-button"]',
    );
    await userEvent.click(splitButton);
    await waitForAutocomplete();
    await waitForAutocomplete();
    await waitForAutocomplete();

    await userEvent.click(
      container.querySelector('[data-testid="transaction-error"] button'),
    );

    input = await editNewField(container, 'debit', 1);
    await userEvent.clear(input);
    await userEvent.type(input, '45.00');
    expect(
      container.querySelector('[data-testid="transaction-error"]'),
    ).toBeTruthy();

    input = await editNewField(container, 'debit', 2);
    await userEvent.clear(input);
    await userEvent.type(input, '10.00');
    await userEvent.tab();
    expect(container.querySelector('[data-testid="transaction-error"]')).toBe(
      null,
    );

    let addButton = container.querySelector('[data-testid="add-button"]');

    expect(getTransactions().length).toBe(5);
    await userEvent.click(addButton);
    expect(getTransactions().length).toBe(8);
    expect(getTransactions()[0].is_parent).toBe(true);
    expect(getTransactions()[0].amount).toBe(-5500);
    expect(getTransactions()[1].is_child).toBe(true);
    expect(getTransactions()[1].amount).toBe(-4500);
    expect(getTransactions()[2].is_child).toBe(true);
    expect(getTransactions()[2].amount).toBe(-1000);
  });

  test('escape closes the new transaction rows', async () => {
    const { container, updateProps } = renderTransactions({
      onCloseAddTransaction: () => {
        updateProps({ isAdding: false });
      },
    });
    updateProps({ isAdding: true });

    // While adding a transaction, pressing escape should close the
    // new transaction form
    let input = expectToBeEditingField(container, 'date', 0, true);
    await userEvent.type(input, '[Tab]');
    input = expectToBeEditingField(container, 'account', 0, true);
    // The first escape closes the dropdown
    await userEvent.type(input, '[Escape]');
    expect(
      container.querySelector('[data-testid="new-transaction"]'),
    ).toBeTruthy();

    // TODO: Fix this
    // Now it should close the new transaction form
    // await userEvent.type(input, '[Escape]');
    // expect(
    //   container.querySelector('[data-testid="new-transaction"]')
    // ).toBeNull();

    // The cancel button should also close the new transaction form
    updateProps({ isAdding: true });
    let cancelButton = container.querySelectorAll(
      '[data-testid="new-transaction"] [data-testid="cancel-button"]',
    )[0];
    await userEvent.click(cancelButton);
    expect(container.querySelector('[data-testid="new-transaction"]')).toBe(
      null,
    );
  });

  test('transaction can be selected', async () => {
    const { container } = renderTransactions();

    await editField(container, 'date', 2);
    const selectCell = queryField(
      container,
      'select',
      '[data-testid=cell-button]',
      2,
    );

    await userEvent.click(selectCell);
    // The header is is selected as well as the single transaction
    expect(container.querySelectorAll('[data-testid=select] svg').length).toBe(
      2,
    );
  });

  test('transaction can be split, updated, and deleted', async () => {
    const { container, getTransactions, updateProps } = renderTransactions();

    let transactions = [...getTransactions()];
    // Change the id to simulate a new transaction being added, and
    // work with that one. This makes sure that the transaction table
    // properly references new data.
    transactions[0] = { ...transactions[0], id: uuidv4() };
    updateProps({ transactions });

    function expectErrorToNotExist(transactions) {
      transactions.forEach((transaction, idx) => {
        expect(transaction.error).toBeFalsy();
      });
    }

    function expectErrorToExist(transactions) {
      transactions.forEach((transaction, idx) => {
        if (idx === 0) {
          expect(transaction.error).toBeTruthy();
        } else {
          expect(transaction.error).toBeFalsy();
        }
      });
    }

    let input = await editField(container, 'category', 0);
    let tooltip = container.querySelector('[data-testid="tooltip"]');
    let splitButton = tooltip.querySelector(
      '[data-testid="split-transaction-button"]',
    );

    // Make it clear that we are expected a negative transaction
    expect(getTransactions()[0].amount).toBe(-2777);
    expectErrorToNotExist([getTransactions()[0]]);

    // Make sure splitting a transaction works
    expect(getTransactions().length).toBe(5);
    await userEvent.click(splitButton);
    await waitForAutocomplete();
    expect(getTransactions().length).toBe(6);
    expect(getTransactions()[0].is_parent).toBe(true);
    expect(getTransactions()[1].is_child).toBe(true);
    expect(getTransactions()[1].amount).toBe(0);
    expectErrorToExist(getTransactions().slice(0, 2));

    let toolbars = container.querySelectorAll(
      '[data-testid="transaction-error"]',
    );
    // Make sure the toolbar has appeared
    expect(toolbars.length).toBe(1);
    let toolbar = toolbars[0];

    // Enter an amount for the new split transaction and make sure the
    // toolbar updates
    input = await editField(container, 'debit', 1);
    await userEvent.clear(input);
    await userEvent.type(input, '10.00[tab]');
    expect(toolbar.innerHTML.includes('17.77')).toBeTruthy();

    // Add another split transaction and make sure everything is
    // updated properly
    await userEvent.click(toolbar.querySelector('button'));
    expect(getTransactions().length).toBe(7);
    expect(getTransactions()[2].amount).toBe(0);
    expectErrorToExist(getTransactions().slice(0, 3));

    // Change the amount to resolve the whole transaction. The toolbar
    // should disappear and no error should exist
    input = await editField(container, 'debit', 2);
    await userEvent.clear(input);
    await userEvent.type(input, '17.77[tab]');
    await userEvent.tab();
    expect(screen.queryAllByTestId('transaction-error')).toHaveLength(0);
    expectErrorToNotExist(getTransactions().slice(0, 3));

    // This snapshot makes sure the data is as we expect. It also
    // shows the sort order and makes sure that is correct
    const parentId = getTransactions()[0].id;
    expect(getTransactions().slice(0, 3)).toEqual([
      {
        account: accounts[0].id,
        amount: -2777,
        category: null,
        cleared: false,
        date: '2017-01-01',
        error: null,
        id: expect.any(String),
        is_parent: true,
        notes: 'Notes',
        payee: 'payed-to',
        sort_order: 0,
      },
      {
        account: accounts[0].id,
        amount: -1000,
        cleared: false,
        date: '2017-01-01',
        error: null,
        id: expect.any(String),
        is_child: true,
        parent_id: parentId,
        payee: 'payed-to',
        sort_order: -1,
        starting_balance_flag: null,
      },
      {
        account: accounts[0].id,
        amount: -1777,
        cleared: false,
        date: '2017-01-01',
        error: null,
        id: expect.any(String),
        is_child: true,
        parent_id: parentId,
        payee: 'payed-to',
        sort_order: -2,
        starting_balance_flag: null,
      },
    ]);

    // Make sure deleting a split transaction updates the state again,
    // and deleting all split transactions turns it into a normal
    // transaction
    //
    // Deleting is disabled, unfortunately we can't delete in tests
    // yet because it doesn't do any batch editing
    //
    // const deleteCell = queryField(container, 'delete', '', 2);
    // await userEvent.click(deleteCell);
    // expect(getTransactions().length).toBe(6);
    // toolbar = container.querySelector('[data-testid="transaction-error"]');
    // expect(toolbar).toBeTruthy();
    // expect(toolbar.innerHTML.includes('17.77')).toBeTruthy();

    // await userEvent.click(queryField(container, 'delete', '', 1));
    // expect(getTransactions()[0].isParent).toBe(false);
  });

  test('transaction with splits shows 0 in correct column', async () => {
    const { container, getTransactions } = renderTransactions();

    let input = await editField(container, 'category', 0);
    let tooltip = container.querySelector('[data-testid="tooltip"]');
    let splitButton = tooltip.querySelector(
      '[data-testid="split-transaction-button"',
    );

    // The first transaction should always be a negative amount
    expect(getTransactions()[0].amount).toBe(-2777);

    // Add two new split transactions
    expect(getTransactions().length).toBe(5);
    await userEvent.click(splitButton);
    await waitForAutocomplete();
    await userEvent.click(
      container.querySelector('[data-testid="transaction-error"] button'),
    );
    expect(getTransactions().length).toBe(7);

    // The debit field should show the zeros
    expect(queryField(container, 'debit', '', 1).textContent).toBe('0.00');
    expect(queryField(container, 'credit', '', 1).textContent).toBe('');
    expect(queryField(container, 'debit', '', 2).textContent).toBe('0.00');
    expect(queryField(container, 'credit', '', 2).textContent).toBe('');

    // Change it to a credit transaction
    input = await editField(container, 'credit', 0);
    await userEvent.type(input, '55.00{Tab}');

    // The zeros should now display in the credit column
    expect(queryField(container, 'debit', '', 1).textContent).toBe('');
    expect(queryField(container, 'credit', '', 1).textContent).toBe('0.00');
    expect(queryField(container, 'debit', '', 2).textContent).toBe('');
    expect(queryField(container, 'credit', '', 2).textContent).toBe('0.00');
  });
});