import logging

import testutil
import test_engine
from test_engine import Table, Column, View, Section, Field

log = logging.getLogger(__name__)

class TestTableActions(test_engine.EngineTestCase):

  address_table_data = [
    ["id",  "city",     "state", "amount" ],
    [ 21,   "New York", "NY"   , 1.       ],
    [ 22,   "Albany",   "NY"   , 2.       ],
    [ 23,   "Seattle",  "WA"   , 3.       ],
    [ 24,   "Chicago",  "IL"   , 4.       ],
    [ 25,   "Bedford",  "MA"   , 5.       ],
    [ 26,   "New York", "NY"   , 6.       ],
    [ 27,   "Buffalo",  "NY"   , 7.       ],
    [ 28,   "Bedford",  "NY"   , 8.       ],
    [ 29,   "Boston",   "MA"   , 9.       ],
    [ 30,   "Yonkers",  "NY"   , 10.      ],
    [ 31,   "New York", "NY"   , 11.      ],
  ]

  people_table_data = [
    ["id",  "name",   "address" ],
    [ 1,    "Alice",  22        ],
    [ 2,    "Bob",    25        ],
    [ 3,    "Carol",  27        ],
  ]

  def init_sample_data(self):
    # Add a couple of tables, including references.
    self.apply_user_action(["AddTable", "Address", [
      {"id": "city",    "type": "Text"},
      {"id": "state",   "type": "Text"},
      {"id": "amount",  "type": "Numeric"},
    ]])
    self.apply_user_action(["AddTable", "People", [
      {"id": "name",    "type": "Text"},
      {"id": "address", "type": "Ref:Address"},
      {"id": "city",    "type": "Any", "formula": "$address.city" }
    ]])

    # Populate some data.
    d = testutil.table_data_from_rows("Address", self.address_table_data[0],
                                      self.address_table_data[1:])
    self.apply_user_action(["BulkAddRecord", "Address", d.row_ids, d.columns])

    d = testutil.table_data_from_rows("People", self.people_table_data[0],
                                      self.people_table_data[1:])
    self.apply_user_action(["BulkAddRecord", "People", d.row_ids, d.columns])

    # Add a view with several sections, including a summary table.
    self.apply_user_action(["CreateViewSection", 1, 0, 'record', None, None])
    self.apply_user_action(["CreateViewSection", 1, 3, 'record', [3], None])
    self.apply_user_action(["CreateViewSection", 2, 3, 'record', None, None])

    # Verify the new structure of tables and views.
    self.assertTables([
      Table(1, "Address", primaryViewId=1, summarySourceTable=0, columns=[
        Column(1, "manualSort", "ManualSortPos", False, "", 0),
        Column(2, "city",       "Text", False, "", 0),
        Column(3, "state",      "Text", False, "", 0),
        Column(4, "amount",     "Numeric", False, "", 0),
      ]),
      Table(2, "People", primaryViewId=2, summarySourceTable=0, columns=[
        Column(5, "manualSort", "ManualSortPos", False, "", 0),
        Column(6, "name",       "Text",         False, "", 0),
        Column(7, "address",    "Ref:Address",  False, "", 0),
        Column(8, "city",       "Any", True, "$address.city", 0),
      ]),
      Table(3, "Address_summary_state", 0, 1, columns=[
        Column(9, "state", "Text", False, "", summarySourceCol=3),
        Column(10, "group", "RefList:Address", True, summarySourceCol=0,
               formula="table.getSummarySourceGroup(rec)"),
        Column(11, "count", "Int", True, summarySourceCol=0, formula="len($group)"),
        Column(12, "amount", "Numeric", True, summarySourceCol=0, formula="SUM($group.amount)"),
      ]),
    ])
    self.assertViews([
      View(1, sections=[
        Section(1, parentKey="record", tableRef=1, fields=[
          Field(1, colRef=2),
          Field(2, colRef=3),
          Field(3, colRef=4),
        ]),
      ]),
      View(2, sections=[
        Section(3, parentKey="record", tableRef=2, fields=[
          Field(7, colRef=6),
          Field(8, colRef=7),
          Field(9, colRef=8),
        ]),
      ]),
      View(3, sections=[
        Section(5, parentKey="record", tableRef=1, fields=[
          Field(13, colRef=2),
          Field(14, colRef=3),
          Field(15, colRef=4),
        ]),
        Section(7, parentKey="record", tableRef=3, fields=[
          Field(19, colRef=9),
          Field(20, colRef=11),
          Field(21, colRef=12),
        ]),
        Section(8, parentKey="record", tableRef=2, fields=[
          Field(22, colRef=6),
          Field(23, colRef=7),
          Field(24, colRef=8),
        ]),
      ]),
    ])

    # Verify the data we've loaded.
    self.assertTableData('Address', cols="subset", data=self.address_table_data)
    self.assertTableData('People', cols="subset", data=self.people_table_data)
    self.assertTableData("Address_summary_state", cols="subset", data=[
      [ "id", "state", "count", "amount"          ],
      [ 1,    "NY",     7,      1.+2+6+7+8+10+11  ],
      [ 2,    "WA",     1,      3.                ],
      [ 3,    "IL",     1,      4.                ],
      [ 4,    "MA",     2,      5.+9              ],
    ])

  #----------------------------------------------------------------------

  @test_engine.test_undo
  def test_table_updates(self):
    # Verify table renames triggered by UpdateRecord actions, and related behavior.

    # Load a sample with a few table and views.
    self.init_sample_data()

    # Verify that we can rename tables via UpdatRecord actions, including multiple tables.
    self.apply_user_action(["BulkUpdateRecord", "_grist_Tables", [1,2],
                            {"tableId": ["Location", "Persons"]}])

    # Check that requested tables and summary tables got renamed correctly.
    self.assertTableData('_grist_Tables', cols="subset", data=[
      ["id",  "tableId"],
      [1,     "Location"],
      [2,     "Persons"],
      [3,     "Location_summary_state"],
    ])

    # Check that reference columns to renamed tables get their type modified.
    self.assertTableData('_grist_Tables_column', rows="subset", cols="subset", data=[
      ["id",  "colId",    "type"],
      [7,     "address",  "Ref:Location"],
      [10,    "group",    "RefList:Location"],
    ])

    # Do a bulk update to rename A and B to conflicting names.
    self.apply_user_action(["AddTable", "A", [{"id": "a", "type": "Text"}]])
    out_actions = self.apply_user_action(["BulkUpdateRecord", "_grist_Tables", [1,2],
                            {"tableId": ["A", "A"]}])

    # See what doc-actions get generated.
    self.assertPartialOutActions(out_actions, {
      "stored": [
        ["ModifyColumn", "Persons", "address", {"type": "Int"}],
        ["ModifyColumn", "Location_summary_state", "group", {"type": "Int"}],
        ["RenameTable", "Location", "A2"],
        ["RenameTable", "Location_summary_state", "A2_summary_state"],
        ["RenameTable", "Persons", "A3"],
        ["BulkUpdateRecord", "_grist_Tables", [1, 3, 2],
         {"tableId": ["A2", "A2_summary_state", "A3"]}],
        ["ModifyColumn", "A3", "address", {"type": "Ref:A2"}],
        ["ModifyColumn", "A2_summary_state", "group", {"type": "RefList:A2"}],
        ["BulkUpdateRecord", "_grist_Tables_column", [7, 10], {"type": ["Ref:A2", "RefList:A2"]}],
      ]
    })

    # Check that requested tables and summary tables got renamed correctly.
    self.assertTableData('_grist_Tables', cols="subset", data=[
      ["id",  "tableId"],
      [1,     "A2"],
      [2,     "A3"],
      [3,     "A2_summary_state"],
      [4,     "A"],
    ])

    # Check that reference columns to renamed tables get their type modified.
    self.assertTableData('_grist_Tables_column', rows="subset", cols="subset", data=[
      ["id",  "colId",    "type"],
      [7,     "address",  "Ref:A2"],
      [10,    "group",    "RefList:A2"],
    ])

    # Verify the data we've loaded.
    self.assertTableData('A2', cols="subset", data=self.address_table_data)
    self.assertTableData('A3', cols="subset", data=self.people_table_data)
    self.assertTableData("A2_summary_state", cols="subset", data=[
      [ "id", "state", "count", "amount"          ],
      [ 1,    "NY",     7,      1.+2+6+7+8+10+11  ],
      [ 2,    "WA",     1,      3.                ],
      [ 3,    "IL",     1,      4.                ],
      [ 4,    "MA",     2,      5.+9              ],
    ])

  #----------------------------------------------------------------------

  @test_engine.test_undo
  def test_table_renames_summary_by_ref(self):
    # Verify table renames when there is a group-by column that's a Reference.

    # This tests a potential bug since a table rename needs to modify Reference types, but
    # group-by columns aren't supposed to be modifiable.
    self.init_sample_data()

    # Add a table grouped by a reference column (the 'Ref:Address' column named 'address').
    self.apply_user_action(["CreateViewSection", 2, 0, 'record', [7], None])
    self.assertTableData('_grist_Tables_column', cols="subset", data=[
      ["id",  "colId",    "type",           "isFormula",    "formula" ],
      [ 13,   "address",  "Ref:Address",    False,          ""        ],
      [ 14,   "group",    "RefList:People", True, "table.getSummarySourceGroup(rec)" ],
      [ 15,   "count",    "Int",            True,           "len($group)" ],
    ], rows=lambda r: (r.parentId.id == 4))

    # Now rename the table Address -> Location.
    out_actions = self.apply_user_action(["RenameTable", "Address", "Location"])

    # See what doc-actions get generated.
    self.assertPartialOutActions(out_actions, {
      "stored": [
        ["ModifyColumn", "People", "address", {"type": "Int"}],
        ["ModifyColumn", "Address_summary_state", "group", {"type": "Int"}],
        ["ModifyColumn", "People_summary_address", "address", {"type": "Int"}],
        ["RenameTable", "Address", "Location"],
        ["RenameTable", "Address_summary_state", "Location_summary_state"],
        ["BulkUpdateRecord", "_grist_Tables", [1, 3],
         {"tableId": ["Location", "Location_summary_state"]}],
        ["ModifyColumn", "People", "address", {"type": "Ref:Location"}],
        ["ModifyColumn", "Location_summary_state", "group", {"type": "RefList:Location"}],
        ["ModifyColumn", "People_summary_address", "address", {"type": "Ref:Location"}],
        ["BulkUpdateRecord", "_grist_Tables_column", [7, 10, 13],
         {"type": ["Ref:Location", "RefList:Location", "Ref:Location"]}],
      ]
    })

    self.assertTableData('_grist_Tables_column', cols="subset", data=[
      ["id",  "colId",    "type",           "isFormula",    "formula" ],
      [ 13,   "address",  "Ref:Location",    False,          ""        ],
      [ 14,   "group",    "RefList:People", True, "table.getSummarySourceGroup(rec)" ],
      [ 15,   "count",    "Int",            True,           "len($group)" ],
    ], rows=lambda r: (r.parentId.id == 4))


  #----------------------------------------------------------------------

  @test_engine.test_undo
  def test_table_removes(self):
    # Verify table removals triggered by UpdateRecord actions, and related behavior.

    # Same setup as previous test.
    self.init_sample_data()

    # Add one more table, and one more view for tables #1 and #4 (those we are about to delete).
    self.apply_user_action(["AddEmptyTable", None])
    out_actions = self.apply_user_action(["CreateViewSection", 1, 0, 'detail', None, None])
    self.assertEqual(out_actions.retValues[0]["viewRef"], 5)
    self.apply_user_action(["CreateViewSection", 4, 5, 'detail', None, None])

    # See what's in TabBar table, to verify after we remove a table.
    self.assertTableData('_grist_TabBar', cols="subset", data=[
      ["id",  "viewRef"],
      [1,     1],
      [2,     2],
      [3,     3],
      [4,     4],
      [5,     5],
    ])

    # Remove two tables, ensure certain views get removed.
    self.apply_user_action(["BulkRemoveRecord", "_grist_Tables", [1, 4]])

    # See that some TabBar entries disappear, or tableRef gets unset.
    self.assertTableData('_grist_TabBar', cols="subset", data=[
      ["id",  "viewRef"],
      [2,     2],
      [3,     3],
    ])

    # Check that reference columns to this table get converted
    self.assertTables([
      Table(2, "People", primaryViewId=2, summarySourceTable=0, columns=[
        Column(5, "manualSort", "ManualSortPos", False, "", 0),
        Column(6, "name",       "Text",         False, "", 0),
        Column(7, "address",    "Int", False, "", 0),
        Column(8, "city",       "Any", True, "$address.city", 0),
      ]),
      # Note that the summary table is also gone.
    ])
    self.assertViews([
      View(2, sections=[
        Section(3, parentKey="record", tableRef=2, fields=[
          Field(7, colRef=6),
          Field(8, colRef=7),
          Field(9, colRef=8),
        ]),
      ]),
      View(3, sections=[
        Section(8, parentKey="record", tableRef=2, fields=[
          Field(22, colRef=6),
          Field(23, colRef=7),
          Field(24, colRef=8),
        ]),
      ]),
    ])

  @test_engine.test_undo
  def test_remove_table_referenced_by_formula(self):
    self.init_sample_data()

    # Add a simple formula column of reference type.
    # Its type will be changed to Any.
    self.add_column(
      "People", "address2", type="Ref:Address", isFormula=True, formula="$address"
    )
    # Add a similar reflist column, but change it to a data column.
    # The formula is just an easy way to populate values for the test.
    # A data column of type reflist should be changed to text.
    self.add_column(
      "People", "addresses", type="RefList:Address", isFormula=True, formula="[$address, $address]"
    )
    self.modify_column("People", "addresses", isFormula=False)

    # Now remove the referenced table and see what happens to the ref columns.
    self.apply_user_action(["RemoveTable", "Address"])
    self.assertTables([
      Table(2, "People", primaryViewId=2, summarySourceTable=0, columns=[
        Column(5, "manualSort", "ManualSortPos", False, "", 0),
        Column(6, "name",       "Text",         False, "", 0),
        # Original data column of type Ref:Address is changed to Int.
        Column(7, "address",    "Int", False, "", 0),
        Column(8, "city",       "Any", True, "$address.city", 0),
        # Formula column of type Ref:Address is changed to Any.
        Column(13, "address2",  "Any", True, "$address", 0),
        # Data column of type RefList:Address is changed to Text.
        Column(14, "addresses", "Text", False, "[$address, $address]", 0),
      ]),
    ])
    self.assertTableData('People', cols="subset", data=[
    ["id",  "name",   "address", "address2", "addresses"],
    [ 1,    "Alice",  22,        22,         "22,22"],
    [ 2,    "Bob",    25,        25,         "25,25"],
    [ 3,    "Carol",  27,        27,         "27,27"],
  ])

  @test_engine.test_undo
  def test_remove_table_referenced_by_summary_groupby_col_without_visible_col(self):
    self.init_sample_data()
    # Create a summary table of People grouped by address (a reference column).
    self.apply_user_action(["CreateViewSection", 2, 0, 'record', [7], None])

    self.assertTables([
      Table(1, "Address", primaryViewId=1, summarySourceTable=0, columns=[
        Column(1, "manualSort", "ManualSortPos", False, "", 0),
        Column(2, "city",       "Text", False, "", 0),
        Column(3, "state",      "Text", False, "", 0),
        Column(4, "amount",     "Numeric", False, "", 0),
      ]),
      Table(2, "People", primaryViewId=2, summarySourceTable=0, columns=[
        Column(5, "manualSort", "ManualSortPos", False, "", 0),
        Column(6, "name",       "Text",         False, "", 0),
        Column(7, "address",    "Ref:Address",  False, "", 0),
        Column(8, "city",       "Any", True, "$address.city", 0),
      ]),
      Table(3, "Address_summary_state", 0, 1, columns=[
        Column(9, "state", "Text", False, "", summarySourceCol=3),
        Column(10, "group", "RefList:Address", True, summarySourceCol=0,
               formula="table.getSummarySourceGroup(rec)"),
        Column(11, "count", "Int", True, summarySourceCol=0, formula="len($group)"),
        Column(12, "amount", "Numeric", True, summarySourceCol=0, formula="SUM($group.amount)"),
      ]),
      Table(4, "People_summary_address", 0, 2, columns=[
        Column(13, "address", "Ref:Address", False, "", summarySourceCol=7),
        Column(14, "group", "RefList:People", True, summarySourceCol=0,
               formula="table.getSummarySourceGroup(rec)"),
        Column(15, "count", "Int", True, summarySourceCol=0, formula="len($group)"),
      ]),
    ])
    self.assertTableData('People_summary_address', data=[
      ["id", "address", "count", "group"],
      [1, 22, 1, [1]],
      [2, 25, 1, [2]],
      [3, 27, 1, [3]],
    ])

    # Now remove the referenced table.
    self.apply_user_action(["RemoveTable", "Address"])
    # In both the People and summary tables, the 'address' reference column
    # is converted to Int, because it didn't have a visible/display column.
    self.assertTables([
      Table(2, "People", primaryViewId=2, summarySourceTable=0, columns=[
        Column(5, "manualSort", "ManualSortPos", False, "", 0),
        Column(6, "name",       "Text",         False, "", 0),
        Column(7, "address",    "Int",  False, "", 0),
        Column(8, "city",       "Any", True, "$address.city", 0),
      ]),
      Table(4, "People_summary_address", 0, 2, columns=[
        Column(13, "address", "Int", False, "", summarySourceCol=7),
        Column(14, "group", "RefList:People", True, summarySourceCol=0,
               formula="table.getSummarySourceGroup(rec)"),
        Column(15, "count", "Int", True, summarySourceCol=0, formula="len($group)"),
      ]),
    ])
    self.assertTableData('People', cols="subset", data=self.people_table_data)
    self.assertTableData('People_summary_address', data=[
      ["id", "address", "count", "group"],
      [1, 22, 1, [1]],
      [2, 25, 1, [2]],
      [3, 27, 1, [3]],
    ])

  @test_engine.test_undo
  def test_remove_table_referenced_by_summary_groupby_col_with_visible_col(self):
    # Similar to the test above, but now the reference column has a visible column.
    self.init_sample_data()
    self.modify_column("People", "address", visibleCol=2)
    self.apply_user_action(["SetDisplayFormula", "People", 0, 7, "$address.city"])
    self.apply_user_action(["CreateViewSection", 2, 0, 'record', [7], None])

    self.assertTables([
      Table(1, "Address", primaryViewId=1, summarySourceTable=0, columns=[
        Column(1, "manualSort", "ManualSortPos", False, "", 0),
        Column(2, "city",       "Text", False, "", 0),
        Column(3, "state",      "Text", False, "", 0),
        Column(4, "amount",     "Numeric", False, "", 0),
      ]),
      Table(2, "People", primaryViewId=2, summarySourceTable=0, columns=[
        Column(5, "manualSort", "ManualSortPos", False, "", 0),
        Column(6, "name",       "Text",         False, "", 0),
        Column(7, "address",    "Ref:Address",  False, "", 0),
        Column(8, "city",       "Any", True, "$address.city", 0),
        Column(13, "gristHelper_Display", "Any", True, "$address.city", 0),
      ]),
      Table(3, "Address_summary_state", 0, 1, columns=[
        Column(9, "state", "Text", False, "", summarySourceCol=3),
        Column(10, "group", "RefList:Address", True, summarySourceCol=0,
               formula="table.getSummarySourceGroup(rec)"),
        Column(11, "count", "Int", True, summarySourceCol=0, formula="len($group)"),
        Column(12, "amount", "Numeric", True, summarySourceCol=0, formula="SUM($group.amount)"),
      ]),
      Table(4, "People_summary_address", 0, 2, columns=[
        Column(14, "address", "Ref:Address", False, "", summarySourceCol=7),
        Column(15, "group", "RefList:People", True, summarySourceCol=0,
               formula="table.getSummarySourceGroup(rec)"),
        Column(16, "count", "Int", True, summarySourceCol=0, formula="len($group)"),
        Column(17, "gristHelper_Display", "Any", True, "$address.city", 0),
      ]),
    ])
    self.assertTableData('People_summary_address', data=[
      ["id", "address", "count", "group", "gristHelper_Display"],
      [1, 22, 1, [1], "Albany"],
      [2, 25, 1, [2], "Bedford"],
      [3, 27, 1, [3], "Buffalo"],
    ])

    self.apply_user_action(["RemoveTable", "Address"])
    self.assertTables([
      Table(2, "People", primaryViewId=2, summarySourceTable=0, columns=[
        Column(5, "manualSort", "ManualSortPos", False, "", 0),
        Column(6, "name",       "Text",         False, "", 0),
        # Reference column is converted to the visible column type, i.e. Text.
        Column(7, "address",    "Text",  False, "", 0),
        Column(8, "city",       "Any", True, "$address.city", 0),
      ]),
      Table(4, "People_summary_address", 0, 2, columns=[
        # Reference column is converted to the visible column type, i.e. Text.
        Column(14, "address", "Text", False, "", summarySourceCol=7),
        Column(15, "group", "RefList:People", True, summarySourceCol=0,
               formula="table.getSummarySourceGroup(rec)"),
        Column(16, "count", "Int", True, summarySourceCol=0, formula="len($group)"),
      ]),
    ])
    self.assertTableData('People', cols="subset", data=[
      ["id",  "name",   "address" ],
      [ 1,    "Alice",  "Albany"],
      [ 2,    "Bob",    "Bedford"],
      [ 3,    "Carol",  "Buffalo"],
    ])
    self.assertTableData('People_summary_address', data=[
      ["id", "address", "count", "group"],
      [ 4,   "Albany",  1,       [1]],
      [ 5,   "Bedford", 1,       [2]],
      [ 6,   "Buffalo", 1,       [3]],
    ])