# This test verifies behavior when a formula produces side effects. The prime example is
# lookupOrAddDerived() function, which adds new records (and is the basis for summary tables).

import objtypes
import test_engine
import testutil

class TestSideEffects(test_engine.EngineTestCase):
  address_table_data = [
    ["id",  "city",     "state", "amount" ],
    [ 21,   "New York", "NY"   , 1        ],
    [ 22,   "Albany",   "NY"   , 2        ],
  ]

  schools_table_data = [
    ["id",  "city"     , "name" ],
    [1,    "Boston"    , "MIT"  ],
    [2,    "New York"  , "NYU"  ],
  ]

  sample = testutil.parse_test_sample({
    "SCHEMA": [
      [1, "Address", [
        [1, "city",        "Text",       False, "", "", ""],
        [2, "state",       "Text",       False, "", "", ""],
        [3, "amount",      "Numeric",    False, "", "", ""],
      ]],
      [2, "Schools", [
        [11,   "name",        "Text",      False, "", "", ""],
        [12,   "city",        "Text",      False, "", "", ""],
      ]],
    ],
    "DATA": {
      "Address": address_table_data,
      "Schools": schools_table_data,
    }
  })

  def test_failure_after_side_effect(self):
    # Verify that when a formula fails after a side-effect, the effect is reverted.
    self.load_sample(self.sample)

    formula = 'Schools.lookupOrAddDerived(city="TESTCITY")\nraise Exception("test-error")'
    out_actions = self.apply_user_action(['AddColumn', 'Address', "A", { 'formula': formula }])
    self.assertPartialOutActions(out_actions, { "stored": [
      ["AddColumn", "Address", "A", {"formula": formula, "isFormula": True, "type": "Any"}],
      ["AddRecord", "_grist_Tables_column", 13, {
        "colId": "A", "formula": formula, "isFormula": True, "label": "A",
        "parentId": 1, "parentPos": 4.0, "type": "Any", "widgetOptions": ""
      }],
      ["BulkUpdateRecord", "Address", [21, 22], {"A": [["E", "Exception"], ["E", "Exception"]]}],
      # The thing to note  here is that while lookupOrAddDerived() should have added a row to
      # Schools, the Exception negated it, and there is no action to add that row.
    ]})

    # Check that data is as expected: no new records in Schools, one new column in Address.
    self.assertTableData('Schools', cols="all", data=self.schools_table_data)
    self.assertTableData('Address', cols="all", data=[
      ["id",  "city",     "state", "amount", "A"            ],
      [ 21,   "New York", "NY"   , 1,        objtypes.RaisedException(Exception())  ],
      [ 22,   "Albany",   "NY"   , 2,        objtypes.RaisedException(Exception())  ],
    ])


  def test_calc_actions_in_side_effect_rollback(self):
    self.load_sample(self.sample)

    # Formula which allows a side effect to be conditionally rolled back.
    formula = '''
Schools.lookupOrAddDerived(city=$city)
if $amount < 0:
  raise Exception("test-error")
'''
    self.add_column('Schools', 'ucity', formula='$city.upper()')
    self.add_column('Address', 'A', formula=formula)

    self.assertTableData('Schools', cols="all", data=[
      ["id", "city", "name", "ucity"],
      [1, "Boston", "MIT", "BOSTON"],
      [2, "New York", "NYU", "NEW YORK"],
      [3, "Albany", "", "ALBANY"],
    ])

    # Check that a successful side-effect which adds a row triggers calc actions for that row.
    out_actions = self.update_record('Address', 22, city="aaa", amount=1000)
    self.assertPartialOutActions(out_actions, {
      "stored": [
        ["UpdateRecord", "Address", 22, {"amount": 1000.0, "city": "aaa"}],
        ["AddRecord", "Schools", 4, {"city": "aaa"}],
        ["UpdateRecord", "Schools", 4, {"ucity": "AAA"}],
      ],
    })
    self.assertTableData('Schools', cols="all", data=[
      ["id", "city", "name", "ucity"],
      [1, "Boston", "MIT", "BOSTON"],
      [2, "New York", "NYU", "NEW YORK"],
      [3, "Albany", "", "ALBANY"],
      [4, "aaa", "", "AAA"],
    ])

    # Check that a side effect that failed and got rolled back does not include calc actions for
    # the rows that didn't stay.
    out_actions = self.update_record('Address', 22, city="bbb", amount=-3)
    self.assertPartialOutActions(out_actions, {
      "stored": [
        ["UpdateRecord", "Address", 22, {"amount": -3.0, "city": "bbb"}],
        ["UpdateRecord", "Address", 22, {"A": ["E", "Exception"]}],
      ],
    })
    self.assertTableData('Schools', cols="all", data=[
      ["id", "city", "name", "ucity"],
      [1, "Boston", "MIT", "BOSTON"],
      [2, "New York", "NYU", "NEW YORK"],
      [3, "Albany", "", "ALBANY"],
      [4, "aaa", "", "AAA"],
    ])
