import re
import test_engine
import testsamples
import testutil


class TestUndo(test_engine.EngineTestCase):
  def test_bad_undo(self):
    # Sometimes undo can make metadata inconsistent with schema. Check that we disallow it.
    self.load_sample(testsamples.sample_students)
    out_actions1 = self.apply_user_action(['AddEmptyTable', None])
    self.assertPartialData("_grist_Tables", ["id", "tableId", "columns"], [
      [1,   "Students", [1,2,4,5,6]],
      [2,   "Schools", [10,12]],
      [3,   "Address", [21]],
      [4,   "Table1", [22,23,24,25]],
    ])

    # Add a column, and check that it's present in the metadata.
    self.add_column('Table1', 'NewCol', type='Text')
    self.assertPartialData("_grist_Tables", ["id", "tableId", "columns"], [
      [1,   "Students", [1,2,4,5,6]],
      [2,   "Schools", [10,12]],
      [3,   "Address", [21]],
      [4,   "Table1", [22,23,24,25,26]],
    ])

    # Now undo just the first action. The list of undo DocActions for it does not mention the
    # newly added column, and fails to clean it up. This would leave the doc in an inconsistent
    # state, and we should not allow it.
    with self.assertRaisesRegex(AssertionError,
        re.compile(r"Internal schema inconsistent.*'NewCol'", re.S)):
      self.apply_undo_actions(out_actions1.undo)

    # Check that schema and metadata look OK.
    self.engine.assert_schema_consistent()

    # Doc state should be unchanged.

    # A little cheating here: assertPartialData() below checks the same thing, but the private
    # calculated field "columns" in _grist_Tables metadata is left out of date by the failed undo.
    # In practice it's harmless: properly calculated fields get restored correct, and the private
    # metadata fields get brought up-to-date when used via Record interface, which is what we do
    # using this assertEqual().
    self.assertEqual([[r.id, r.tableId, list(map(int, r.columns))]
                      for r in self.engine.docmodel.tables.table.filter_records()], [
      [1,   "Students", [1,2,4,5,6]],
      [2,   "Schools", [10,12]],
      [3,   "Address", [21]],
      [4,   "Table1", [22,23,24,25,26]],
    ])

    self.assertPartialData("_grist_Tables", ["id", "tableId", "columns"], [
      [1,   "Students", [1,2,4,5,6]],
      [2,   "Schools", [10,12]],
      [3,   "Address", [21]],
      [4,   "Table1", [22,23,24,25,26]],
    ])

  def test_import_undo(self):
    # Here we reproduce another bad situation. A more complex example with the same essence arose
    # during undo of imports when the undo could omit part of the action bundle.
    self.load_sample(testsamples.sample_students)

    out_actions1 = self.apply_user_action(['AddEmptyTable', None])
    out_actions2 = self.add_column('Table1', 'D', type='Text')
    out_actions3 = self.remove_column('Table1', 'D')
    out_actions4 = self.apply_user_action(['RemoveTable', 'Table1'])
    out_actions5 = self.apply_user_action(['AddTable', 'Table1', [{'id': 'X'}]])

    undo_actions = [da for out in [out_actions1, out_actions2, out_actions4, out_actions5]
                       for da in out.undo]
    with self.assertRaises(AssertionError):
      self.apply_undo_actions(undo_actions)

    # The undo failed, and data should look as before the undo.
    self.engine.assert_schema_consistent()
    self.assertEqual([[r.id, r.tableId, list(map(int, r.columns))]
                      for r in self.engine.docmodel.tables.table.filter_records()], [
      [1,   "Students", [1,2,4,5,6]],
      [2,   "Schools", [10,12]],
      [3,   "Address", [21]],
      [4,   "Table1", [22, 23]],
    ])

  @test_engine.test_undo
  def test_auto_remove_undo(self):
    """
    Test that a formula using docmodel.setAutoRemove doesn't break when undoing.
    We don't actually recommend using docmodel.setAutoRemove in formulas,
    but it'd be nice, and this is really testing that a bugfix about summary tables
    also helps outside of summary tables.
    """
    self.load_sample(testutil.parse_test_sample({
      "SCHEMA": [
        [1, "Table", [
          [11, "amount", "Numeric", False, "", "", ""],
          [12, "amount2", "Numeric", True, "$amount", "", ""],
          [13, "remove", "Any", True,
           "table.table._engine.docmodel.setAutoRemove(rec, not $amount2)", "", ""],
        ]]
      ],
      "DATA": {
        "Table": [
          ["id", "amount", "amount2"],
          [21, 1, 1],
          [22, 2, 2],
        ]
      }
    }))
    out_actions = self.update_record('Table', 21, amount=0)
    self.assertOutActions(out_actions, {
      'calc': [],
      'direct': [True, False],
      'stored': [['UpdateRecord', 'Table', 21, {'amount': 0.0}],
                 ['RemoveRecord', 'Table', 21]],
      'undo': [['UpdateRecord', 'Table', 21, {'amount2': 1.0}],
               ['UpdateRecord', 'Table', 21, {'amount': 1.0}],
               ['AddRecord', 'Table', 21, {}]],
    })
    self.assertTableData('Table', cols="subset", data=[
      ["id", "amount", "amount2"],
      [22, 2, 2],
    ])