import unittest
import gpath

class TestGpath(unittest.TestCase):
  def setUp(self):
    self.obj = {
      "foo": [{"bar": 1}, {"bar": 2}, {"baz": 3}],
      "hello": "world"
    }

  def test_get(self):
    self.assertEqual(gpath.get(self.obj, ["foo", 0, "bar"]), 1)
    self.assertEqual(gpath.get(self.obj, ["foo", 2]), {"baz": 3})
    self.assertEqual(gpath.get(self.obj, ["hello"]), "world")
    self.assertEqual(gpath.get(self.obj, []), self.obj)

    self.assertEqual(gpath.get(self.obj, ["foo", 0, "baz"]), None)
    self.assertEqual(gpath.get(self.obj, ["foo", 4]), None)
    self.assertEqual(gpath.get(self.obj, ["foo", 4, "baz"]), None)
    self.assertEqual(gpath.get(self.obj, [0]), None)

  def test_set(self):
    gpath.place(self.obj, ["foo"], {"bar": 1, "baz": 2})
    self.assertEqual(self.obj["foo"], {"bar": 1, "baz": 2})
    gpath.place(self.obj, ["foo", "bar"], 17)
    self.assertEqual(self.obj["foo"], {"bar": 17, "baz": 2})
    gpath.place(self.obj, ["foo", "baz"], None)
    self.assertEqual(self.obj["foo"], {"bar": 17})

    self.assertEqual(self.obj["hello"], "world")
    gpath.place(self.obj, ["hello"], None)
    self.assertFalse("hello" in self.obj)
    gpath.place(self.obj, ["hello"], None)    # OK to remove a non-existent property.
    self.assertFalse("hello" in self.obj)
    gpath.place(self.obj, ["hello"], "blah")
    self.assertEqual(self.obj["hello"], "blah")

  def test_set_strict(self):
    with self.assertRaisesRegex(Exception, r"non-existent"):
      gpath.place(self.obj, ["bar", 4], 17)

    with self.assertRaisesRegex(Exception, r"not a plain object"):
      gpath.place(self.obj, ["foo", 0], 17)


  def test_insert(self):
    self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
    gpath.insert(self.obj, ["foo", 0], "asdf")
    self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, {"baz": 3}])
    gpath.insert(self.obj, ["foo", 3], "hello")
    self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}])
    gpath.insert(self.obj, ["foo", None], "world")
    self.assertEqual(self.obj["foo"],
                     ["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}, "world"])

  def test_insert_strict(self):
    with self.assertRaisesRegex(Exception, r'not an array'):
      gpath.insert(self.obj, ["foo"], "asdf")

    with self.assertRaisesRegex(Exception, r'invalid.*index'):
      gpath.insert(self.obj, ["foo", -1], 17)

    with self.assertRaisesRegex(Exception, r'invalid.*index'):
      gpath.insert(self.obj, ["foo", "foo"], 17)

  def test_update(self):
    """update should update array items"""
    self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
    gpath.update(self.obj, ["foo", 0], "asdf")
    self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, {"baz": 3}])
    gpath.update(self.obj, ["foo", 2], "hello")
    self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, "hello"])
    gpath.update(self.obj, ["foo", 1], None)
    self.assertEqual(self.obj["foo"], ["asdf", None, "hello"])

  def test_update_strict(self):
    """update should be strict"""
    with self.assertRaisesRegex(Exception, r'non-existent'):
      gpath.update(self.obj, ["bar", 4], 17)
    with self.assertRaisesRegex(Exception, r'not an array'):
      gpath.update(self.obj, ["foo"], 17)
    with self.assertRaisesRegex(Exception, r'invalid.*index'):
      gpath.update(self.obj, ["foo", -1], 17)
    with self.assertRaisesRegex(Exception, r'invalid.*index'):
      gpath.update(self.obj, ["foo", None], 17)

  def test_remove(self):
    """remove should remove indices"""
    self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
    gpath.remove(self.obj, ["foo", 0])
    self.assertEqual(self.obj["foo"], [{"bar": 2}, {"baz": 3}])
    gpath.remove(self.obj, ["foo", 1])
    self.assertEqual(self.obj["foo"], [{"bar": 2}])
    gpath.remove(self.obj, ["foo", 0])
    self.assertEqual(self.obj["foo"], [])

  def test_remove_strict(self):
    """remove should be strict"""
    with self.assertRaisesRegex(Exception, r'non-existent'):
      gpath.remove(self.obj, ["bar", 4])
    with self.assertRaisesRegex(Exception, r'not an array'):
      gpath.remove(self.obj, ["foo"])
    with self.assertRaisesRegex(Exception, r'invalid.*index'):
      gpath.remove(self.obj, ["foo", -1])
    with self.assertRaisesRegex(Exception, r'invalid.*index'):
      gpath.remove(self.obj, ["foo", None])

  def test_glob(self):
    """glob should scan arrays"""
    self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])

    self.assertEqual(gpath.place(self.obj, ["foo", "*", "bar"], 17), 3)
    self.assertEqual(self.obj["foo"], [{"bar": 17}, {"bar": 17}, {"baz": 3, "bar": 17}])

    with self.assertRaisesRegex(Exception, r'non-existent object at \/foo\/\*\/bad'):
      gpath.place(self.obj, ["foo", "*", "bad", "test"], 10)

    self.assertEqual(gpath.update(self.obj, ["foo", "*"], "hello"), 3)
    self.assertEqual(self.obj["foo"], ["hello", "hello", "hello"])

  def test_glob_strict_wildcard(self):
    """should only support tail wildcard for updates"""
    with self.assertRaisesRegex(Exception, r'invalid array index'):
      gpath.remove(self.obj, ["foo", "*"])
    with self.assertRaisesRegex(Exception, r'invalid array index'):
      gpath.insert(self.obj, ["foo", "*"], 1)

  def test_glob_wildcard_keys(self):
    """should not scan object keys"""
    self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])

    self.assertEqual(gpath.place(self.obj, ["foo", 0, "*"], 17), 1)
    self.assertEqual(self.obj["foo"], [{"bar": 1, '*': 17}, {"bar": 2}, {"baz": 3}])

    with self.assertRaisesRegex(Exception, r'non-existent'):
      gpath.place(self.obj, ["*", 0, "bar"], 17)

  def test_glob_nested(self):
    """should scan nested arrays"""
    self.obj = [{"a": [1,2,3]}, {"a": [4,5,6]}, {"a": [7,8,9]}]
    self.assertEqual(gpath.update(self.obj, ["*", "a", "*"], 5), 9)
    self.assertEqual(self.obj, [{"a": [5,5,5]}, {"a": [5,5,5]}, {"a": [5,5,5]}])

  def test_dirname(self):
    """dirname should return path without last component"""
    self.assertEqual(gpath.dirname(["foo", "bar", "baz"]), ["foo", "bar"])
    self.assertEqual(gpath.dirname([1, 2]), [1])
    self.assertEqual(gpath.dirname(["foo"]), [])
    self.assertEqual(gpath.dirname([]), [])

  def test_basename(self):
    """basename should return the last component of path"""
    self.assertEqual(gpath.basename(["foo", "bar", "baz"]), "baz")
    self.assertEqual(gpath.basename([1, 2]), 2)
    self.assertEqual(gpath.basename(["foo"]), "foo")
    self.assertEqual(gpath.basename([]), None)

if __name__ == "__main__":
  unittest.main()
