# -*- coding: utf-8 -*-
# pylint:disable=line-too-long

import unittest
from acl_formula import parse_acl_formula
import test_engine


class TestACLFormula(unittest.TestCase):
  def test_basic(self):
    # Test a few basic formulas and structures, hitting everything we expect to support
    self.assertEqual(parse_acl_formula(
      "user.Email == 'X@'"),
      ["Eq", ["Attr", ["Name", "user"], "Email"],
        ["Const", "X@"]])

    self.assertEqual(parse_acl_formula(
      "user.Role in ('editors', 'owners')"),
      ["In", ["Attr", ["Name", "user"], "Role"],
             ["List", ["Const", "editors"], ["Const", "owners"]]])

    self.assertEqual(parse_acl_formula(
      "user.Role not in ('editors', 'owners')"),
      ["NotIn", ["Attr", ["Name", "user"], "Role"],
                ["List", ["Const", "editors"], ["Const", "owners"]]])

    self.assertEqual(parse_acl_formula(
      "rec.office == 'Seattle' and user.email in ['sally@', 'xie@']"),
      ['And',
        ['Eq', ['Attr', ['Name', 'rec'], 'office'], ['Const', 'Seattle']],
        ['In',
         ['Attr', ['Name', 'user'], 'email'],
         ['List', ['Const', 'sally@'], ['Const', 'xie@']]
        ]])

    self.assertEqual(parse_acl_formula(
      "user.IsAdmin or rec.assigned is None or (not newRec.HasDuplicates and rec.StatusIndex <= newRec.StatusIndex)"),
      ['Or',
        ['Attr', ['Name', 'user'], 'IsAdmin'],
        ['Is', ['Attr', ['Name', 'rec'], 'assigned'], ['Const', None]],
        ['And',
          ['Not', ['Attr', ['Name', 'newRec'], 'HasDuplicates']],
          ['LtE', ['Attr', ['Name', 'rec'], 'StatusIndex'], ['Attr', ['Name', 'newRec'], 'StatusIndex']]
        ]
      ])

    self.assertEqual(parse_acl_formula(
      "r.A <= n.A + 1 or r.A >= n.A - 1 or r.B < n.B * 2.5 or r.B > n.B / 2.5 or r.C % 2 != 0"),
      ['Or',
        ['LtE',
          ['Attr', ['Name', 'r'], 'A'],
          ['Add', ['Attr', ['Name', 'n'], 'A'], ['Const', 1]]],
        ['GtE',
          ['Attr', ['Name', 'r'], 'A'],
          ['Sub', ['Attr', ['Name', 'n'], 'A'], ['Const', 1]]],
        ['Lt',
          ['Attr', ['Name', 'r'], 'B'],
          ['Mult', ['Attr', ['Name', 'n'], 'B'], ['Const', 2.5]]],
        ['Gt',
          ['Attr', ['Name', 'r'], 'B'],
          ['Div', ['Attr', ['Name', 'n'], 'B'], ['Const', 2.5]]],
        ['NotEq',
          ['Mod', ['Attr', ['Name', 'r'], 'C'], ['Const', 2]],
          ['Const', 0]]
      ])

    self.assertEqual(parse_acl_formula(
      "rec.A is True or rec.A is not False"),
      ['Or',
        ['Is', ['Attr', ['Name', 'rec'], 'A'], ['Const', True]],
        ['IsNot', ['Attr', ['Name', 'rec'], 'A'], ['Const', False]]
      ])

    self.assertEqual(parse_acl_formula(
      "user.Office.City == 'Seattle' and user.Status.IsActive"),
      ['And',
        ['Eq',
          ['Attr', ['Attr', ['Name', 'user'], 'Office'], 'City'],
          ['Const', 'Seattle']],
        ['Attr', ['Attr', ['Name', 'user'], 'Status'], 'IsActive']
      ])

    self.assertEqual(parse_acl_formula(
      "True # Comment!  "),
      ['Comment', ['Const', True], 'Comment!'])

    self.assertEqual(parse_acl_formula(
      "\"#x\" == \" # Not a comment \"#Comment!"),
      ['Comment',
       ['Eq', ['Const', '#x'], ['Const', ' # Not a comment ']],
       'Comment!'
      ])

    self.assertEqual(parse_acl_formula(
      "# Allow owners\nuser.Access == 'owners' # ignored\n# comment ignored"),
      ['Comment',
       ['Eq', ['Attr', ['Name', 'user'], 'Access'], ['Const', 'owners']],
       'Allow owners'
      ])

  def test_unsupported(self):
    # Test a few constructs we expect to fail
    # Not an expression
    self.assertRaises(SyntaxError, parse_acl_formula, "return 1")
    self.assertRaises(SyntaxError, parse_acl_formula, "def foo(): pass")

    # Unsupported node type
    self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "max(rec)")
    self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "user.id in {1, 2, 3}")
    self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "1 if user.IsAnon else 2")

    # Unsupported operation
    self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "1 | 2")
    self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "1 << 2")
    self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "~test")

    # Syntax error
    self.assertRaises(SyntaxError, parse_acl_formula, "[(]")
    self.assertRaises(SyntaxError, parse_acl_formula, "user.id in (1,2))")
    self.assertRaisesRegex(SyntaxError, r'invalid syntax on line 1 col 9', parse_acl_formula, "foo and !bar")

class TestACLFormulaUserActions(test_engine.EngineTestCase):
  def test_acl_actions(self):
    # Adding or updating ACLRules automatically includes aclFormula compilation.

    # Single Add
    out_actions = self.apply_user_action(
      ['AddRecord', '_grist_ACLRules', None, {"resource": 1, "aclFormula": "user.UserID == 7"}],
    )
    self.assertPartialOutActions(out_actions, { "stored": [
      ["AddRecord", "_grist_ACLRules", 1, {"resource": 1, "aclFormula": "user.UserID == 7",
        "aclFormulaParsed": '["Eq", ["Attr", ["Name", "user"], "UserID"], ["Const", 7]]',
        "rulePos": 1.0
      }],
    ]})

    # Single Update
    out_actions = self.apply_user_action(
      ['UpdateRecord', '_grist_ACLRules', 1, {
        "aclFormula": "user.UserID == 8",
        "aclFormulaParsed": "hello"
      }],
    )
    self.assertPartialOutActions(out_actions, { "stored": [
      ["UpdateRecord", "_grist_ACLRules", 1, {
        "aclFormula": "user.UserID == 8",
        "aclFormulaParsed": '["Eq", ["Attr", ["Name", "user"], "UserID"], ["Const", 8]]',
      }],
    ]})

    # BulkAddRecord
    out_actions = self.apply_user_action(['BulkAddRecord', '_grist_ACLRules', [None, None], {
      "resource": [1, 1],
      "aclFormula": ["user.IsGood", "user.IsBad"],
      "aclFormulaParsed": ["[1]", '["ignored"]'],   # Should get overwritten
    }])
    self.assertPartialOutActions(out_actions, { "stored": [
      [ 'BulkAddRecord', '_grist_ACLRules', [2, 3], {
        "resource": [1, 1],
        "aclFormula": ["user.IsGood", "user.IsBad"],
        "aclFormulaParsed": [                         # Gets overwritten
          '["Attr", ["Name", "user"], "IsGood"]',
          '["Attr", ["Name", "user"], "IsBad"]',
        ],
        "rulePos": [2.0, 3.0],                        # Gets filled in.
      }],
    ]})

    # BulkUpdateRecord
    out_actions = self.apply_user_action(['BulkUpdateRecord', '_grist_ACLRules', [2, 3], {
      "aclFormula": ["not user.IsGood", ""],
    }])
    self.assertPartialOutActions(out_actions, { "stored": [
      [ 'BulkUpdateRecord', '_grist_ACLRules', [2, 3], {
        "aclFormula": ["not user.IsGood", ""],
        "aclFormulaParsed": ['["Not", ["Attr", ["Name", "user"], "IsGood"]]', ''],
      }],
    ]})
