#!/usr/bin/env python3
"""Test for the ee.geometry module.

The API objects, like ee.Geometry, are usually tested by comparing the
serialized object to a golden. To be brief, the tests in this file just validate
the Geometry function's arguments instead of the serialized object.
"""

import math

from absl.testing import parameterized

import unittest
import ee
from ee import apitestcase


_EPSILON = 1e-5


class GeometryTest(apitestcase.ApiTestCase, parameterized.TestCase):

  def epsg4326_rectangle(self, west, south, east, north):
    # If we call ee.Geometry.Rectangle with geodesic=False we would get a
    # computed call.
    return ee.Geometry({
        'coordinates': [
            [[west, north], [west, south], [east, south], [east, north]]
        ],
        'type': 'Polygon',
        'geodesic': False,
    })

  @parameterized.named_parameters(
      ('_simple', (-10, -20, 10, 20), (-10, -20, 10, 20)),
      (
          '_latitude_widerThanPolesClamped_south',
          (-10, -90, 10, 73),
          (-10, -1000, 10, 73),
      ),
      (
          '_latitude_widerThanPolesClamped_north',
          (-10, -34, 10, 90),
          (-10, -34, 10, 10000),
      ),
      ('_latitude_zeroSpan', (-10, 20, 10, 20), (-10, 20, 10, 20)),
      (
          '_longitude_crossingMeridianWithOppositeSigns',
          (170, -20, 190, 20),
          (170, -20, -170, 20),
      ),
      (
          '_longitude_crossingMeridianWithNegativeSigns',
          (170, -20, 190, 20),
          (-190, -20, -170, 20),
      ),
      (
          '_longitude_crossingMeridianWithPositiveSigns',
          (170, -20, 190, 20),
          (170, -20, 190, 20),
      ),
      ('_longitude_exactlyGlobal', (-180, -20, 180, 20), (-180, -20, 180, 20)),
      (
          '_longitude_excessOfGlobalIsClamped',
          (-180, -20, 180, 20),
          (-180 - _EPSILON, -20, 180 + _EPSILON, 20),
      ),
      ('_longitude_zeroSpan', (10, -20, 10, 20), (10, -20, 10, 20)),
      (
          '_infinity_validDirection_isClamped_west',
          (-180, -20, 180, 20),
          (-math.inf, -20, 10, 20),
      ),
      (
          '_infinity_validDirection_isClamped_east',
          (-180, -20, 180, 20),
          (-10, -20, math.inf, 20),
      ),
      (
          '_infinity_validDirection_isClamped_south',
          (-10, -90, 10, 20),
          (-10, -math.inf, 10, 20),
      ),
      (
          '_infinity_validDirection_isClamped_north',
          (-10, -20, 10, 90),
          (-10, -20, 10, math.inf),
      ),
  )
  def test_bbox(self, expected, coords):
    self.assertEqual(
        self.epsg4326_rectangle(*expected),
        ee.Geometry.BBox(*coords),
    )

  def test_bbox_computed_args(self):
    ten = ee.Number(5).add(5)
    box = ee.Geometry.BBox(-10, -20, ten, 20)
    func = ee.ApiFunction.lookup('GeometryConstructors.BBox')

    self.assertIsInstance(box, ee.Geometry)
    self.assertEqual(func, box.func)
    expected_args = {
        'west': ee.Number(-10),
        'south': ee.Number(-20),
        'east': ten,
        'north': ee.Number(20),
    }
    self.assertEqual(expected_args, box.args)

  @parameterized.named_parameters(
      # Reject cases which, if we clamped them instead, would move a box whose
      # bounds lie past a pole to being a point at the pole.
      (
          '_latitude_notBeyondPoles_north',
          (-10, -100, 10, -95),
          r'Geometry\.BBox: north must be at least -90°, but was -95°',
      ),
      (
          '_latitude_notBeyondPoles_south',
          (-10, 95, 10, 100),
          r'Geometry\.BBox: south must be at most \+90°, but was 95°',
      ),
      # NaN is rejected.
      (
          '_NaN_isRejected_west',
          (math.nan, -20, 10, 20),
          r'Geometry\.BBox: west must not be nan',
      ),
      (
          '_NaN_isRejected_south',
          (-10, math.nan, 10, 20),
          r'Geometry\.BBox: south must be at most \+90°, but was nan°',
      ),
      (
          '_NaN_isRejected_east',
          (-10, -20, math.nan, 20),
          r'Geometry\.BBox: east must not be nan',
      ),
      (
          '_NaN_isRejected_north',
          (-10, -20, 10, math.nan),
          r'Geometry.BBox: north must be at least -90°, but was nan°',
      ),
      # Infinities in the wrong direction are rejected.
      (
          '_infinity_invalidDirection_isRejected_west',
          (math.inf, -20, 10, 20),
          r'Geometry\.BBox: west must not be inf',
      ),
      (
          '_infinity_invalidDirection_isRejected_south',
          (-10, math.inf, 10, 20),
          r'Geometry\.BBox: south must be at most \+90°, but was inf°',
      ),
      (
          '_infinity_invalidDirection_isRejected_east',
          (-10, -20, -math.inf, 20),
          r'Geometry\.BBox: east must not be -inf',
      ),
      (
          '_infinity_invalidDirection_isRejected_north',
          (-10, -20, 10, -math.inf),
          r'Geometry\.BBox: north must be at least -90°, but was -inf°',
      ),
  )
  def test_bbox_invalid(self, coords, message):
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.BBox(*coords)

  def test_line_string(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    max_error = 1000
    func = ee.ApiFunction.lookup('GeometryConstructors.LineString')

    geometry = ee.Geometry.LineString(coords, proj, True, max_error)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
            'geodesic': True,
            'maxError': ee.ErrorMargin(max_error),
        },
        geometry.args,
    )

  def test_line_string_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.LineString')

    geometry = ee.Geometry.LineString(coords, geodesic=True)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'geodesic': True},
        geometry.args,
    )

  def test_line_string_computed_args(self):
    """Verifies that a LineString with computed inputs can be constructed."""
    p1 = ee.Geometry.Point([1, 2])
    p2 = ee.Geometry.Point([3, 4])
    line = ee.Geometry.LineString([p1, p2])

    self.assertIsInstance(line, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.LineString'), line.func
    )
    self.assertEqual({'coordinates': ee.List([p1, p2])}, line.args)

  @parameterized.named_parameters(
      ('_coords', (1, 2, 3, 4, 5), 'Invalid number of coordinates: 5'),
      ('_coordsList', [[1, 2, 3, 4, 5]], 'Invalid number of coordinates: 5'),
      ('_geometry', [[[1, 2], [3, 4], 5]], 'Invalid geometry'),
      ('_nesting', [[[[1, 2], [3, 4]]]], 'Invalid geometry'),
  )
  def test_line_string_invalid(self, coords, message):
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.LineString(*coords)

  def test_linear_ring(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    geodesic = True
    max_error = 1000
    func = ee.ApiFunction.lookup('GeometryConstructors.LinearRing')

    geometry = ee.Geometry.LinearRing(coords, proj, geodesic, max_error)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
            'geodesic': geodesic,
            'maxError': ee.ErrorMargin(max_error),
        },
        geometry.args,
    )

  def test_linear_ring_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.LinearRing')
    geodesic = True

    geometry = ee.Geometry.LinearRing(coords, geodesic=geodesic)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'geodesic': geodesic},
        geometry.args,
    )

  def test_linear_ring_computed_args(self):
    """Verifies that a LinearRing with computed inputs can be constructed."""
    p1 = ee.Geometry.Point([1, 2])
    p2 = ee.Geometry.Point([3, 4])
    ring = ee.Geometry.LinearRing([p1, p2])

    self.assertIsInstance(ring, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.LinearRing'), ring.func
    )
    self.assertEqual({'coordinates': ee.List([p1, p2])}, ring.args)

  @parameterized.named_parameters(
      ('_coords', (1, 2, 3, 4, 5), 'Invalid number of coordinates: 5'),
      ('_coordsList', [[1, 2, 3, 4, 5]], 'Invalid number of coordinates: 5'),
      ('_geometry', [[[1, 2], [3, 4], 5]], 'Invalid geometry'),
      ('_nesting', [[[[1, 2], [3, 4]]]], 'Invalid geometry'),
  )
  def test_linear_ring_invalid(self, coords, message):
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.LinearRing(*coords)

  def test_multi_line_string(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    geodesic = True
    max_error = 1000
    func = ee.ApiFunction.lookup('GeometryConstructors.MultiLineString')

    geometry = ee.Geometry.MultiLineString(coords, proj, geodesic, max_error)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
            'geodesic': geodesic,
            'maxError': ee.ErrorMargin(max_error),
        },
        geometry.args,
    )

  def test_multi_line_string_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.MultiLineString')
    geodesic = True

    geometry = ee.Geometry.MultiLineString(coords, geodesic=geodesic)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'geodesic': geodesic},
        geometry.args,
    )

  def test_multi_line_string_computed_args(self):
    """Verifies that a MultiLineString with computed inputs can be constructed."""
    p1 = ee.Geometry.Point([1, 2])
    p2 = ee.Geometry.Point([3, 4])
    line = ee.Geometry.MultiLineString([p1, p2])

    self.assertIsInstance(line, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.MultiLineString'), line.func
    )
    self.assertEqual({'coordinates': ee.List([p1, p2])}, line.args)

  @parameterized.named_parameters(
      ('_coords', (1, 2, 3, 4, 5), 'Invalid number of coordinates: 5'),
      ('_coordsList', [[1, 2, 3, 4, 5]], 'Invalid number of coordinates: 5'),
      ('_geometry', [[[1, 2], [3, 4], 5]], 'Invalid geometry'),
      ('_nesting', [[[[[1, 2], [3, 4]]]]], 'Invalid geometry'),
  )
  def test_multi_line_string_invalid(self, coords, message):
    """Verifies MultiLineString constructor behavior with invalid arguments."""
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.MultiLineString(*coords)

  def test_multi_point(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    func = ee.ApiFunction.lookup('GeometryConstructors.MultiPoint')

    geometry = ee.Geometry.MultiPoint(coords, proj)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
        },
        geometry.args,
    )

  def test_multi_point_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.MultiPoint')
    proj = 'EPSG:4326'

    geometry = ee.Geometry.MultiPoint(coords, proj=proj)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'crs': ee.Projection(proj)},
        geometry.args,
    )

  def test_multi_point_computed_args(self):
    """Verifies that a MultiPoint with computed inputs can be constructed."""
    p1 = ee.Geometry.Point([1, 2])
    p2 = ee.Geometry.Point([3, 4])
    point = ee.Geometry.MultiPoint([p1, p2])

    self.assertIsInstance(point, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.MultiPoint'), point.func
    )
    self.assertEqual({'coordinates': ee.List([p1, p2])}, point.args)

  @parameterized.named_parameters(
      ('_coords', (1, 2, 3, 4, 5), 'Invalid number of coordinates: 5'),
      ('_coordsList', [[1, 2, 3, 4, 5]], 'Invalid number of coordinates: 5'),
      ('_geometry', [[[1, 2], [3, 4], 5]], 'Invalid geometry'),
      ('_nesting', [[[[1, 2], [3, 4]]]], 'Invalid geometry'),
  )
  def test_multi_point_invalid(self, coords, message):
    """Verifies MultiPoint constructor behavior with invalid arguments."""
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.MultiPoint(*coords)

  def test_multi_polygon(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    geodesic = True
    max_error = 1000
    even_odd = True
    func = ee.ApiFunction.lookup('GeometryConstructors.MultiPolygon')

    geometry = ee.Geometry.MultiPolygon(
        coords, proj, geodesic, max_error, even_odd
    )
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
            'geodesic': geodesic,
            'maxError': ee.ErrorMargin(max_error),
            'evenOdd': even_odd,
        },
        geometry.args,
    )

  def test_multi_polygon_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.MultiPolygon')
    geodesic = True

    geometry = ee.Geometry.MultiPolygon(coords, geodesic=geodesic)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'geodesic': geodesic},
        geometry.args,
    )

  def test_multi_polygon_computed_args(self):
    """Verifies that a MultiPolygon with computed inputs can be constructed."""
    p1 = ee.Geometry.Point([1, 2])
    p2 = ee.Geometry.Point([3, 4])
    line = ee.Geometry.MultiPolygon([p1, p2])

    self.assertIsInstance(line, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.MultiPolygon'), line.func
    )
    self.assertEqual({'coordinates': ee.List([p1, p2])}, line.args)

  @parameterized.named_parameters(
      ('_coords', (1, 2, 3, 4, 5), 'Invalid number of coordinates: 5'),
      ('_coordsList', [[1, 2, 3, 4, 5]], 'Invalid number of coordinates: 5'),
      ('_geometry', [[[[[[1, 2], [3, 4], [5, 6]]]]]], 'Invalid geometry'),
      ('_nesting', [[[[[1, 2], [3, 4]], [1, 2]]]], 'Invalid geometry'),
  )
  def test_multi_polygon_invalid(self, coords, message):
    """Verifies MultiPolygon constructor behavior with invalid arguments."""
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.MultiPolygon(*coords)

  def test_point(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    func = ee.ApiFunction.lookup('GeometryConstructors.Point')

    geometry = ee.Geometry.Point(coords, proj)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
        },
        geometry.args,
    )

  def test_point_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.Point')
    proj = 'EPSG:4326'

    geometry = ee.Geometry.Point(coords, proj=proj)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'crs': ee.Projection(proj)},
        geometry.args,
    )

  def test_point_old_keyword_args(self):
    """Verifies that Points still allow keyword lon/lat args."""
    self.assertEqual(ee.Geometry.Point(1, 2), ee.Geometry.Point(lon=1, lat=2))
    self.assertEqual(ee.Geometry.Point(1, 2), ee.Geometry.Point(1, lat=2))

  def test_point_computed_args(self):
    """Verifies that a Point with computed inputs can be constructed."""
    v1 = ee.Number(1)
    v2 = ee.Number(2)
    point = ee.Geometry.Point([v1, v2])

    self.assertIsInstance(point, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.Point'), point.func
    )
    self.assertEqual({'coordinates': ee.List([v1, v2])}, point.args)

  @parameterized.named_parameters(
      ('_geometry', [['-78.204948', '40.966539']], 'Invalid geometry'),
  )
  def test_point_invalid(self, coords, message):
    """Verifies Point constructor behavior with invalid arguments."""
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.Point(*coords)

  def test_polygon(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    geodesic = True
    max_error = 1000
    even_odd = True
    func = ee.ApiFunction.lookup('GeometryConstructors.Polygon')

    geometry = ee.Geometry.Polygon(coords, proj, geodesic, max_error, even_odd)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
            'geodesic': geodesic,
            'maxError': ee.ErrorMargin(max_error),
            'evenOdd': even_odd,
        },
        geometry.args,
    )

  def test_polygon_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.Polygon')
    geodesic = True

    geometry = ee.Geometry.Polygon(coords, geodesic=geodesic)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'geodesic': geodesic},
        geometry.args,
    )

  def test_polygon_computed_args(self):
    """Verifies that a Polygon with computed inputs can be constructed."""
    p1 = ee.Geometry.Point([1, 2])
    p2 = ee.Geometry.Point([3, 4])
    line = ee.Geometry.Polygon([p1, p2])

    self.assertIsInstance(line, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.Polygon'), line.func
    )
    self.assertEqual({'coordinates': ee.List([p1, p2])}, line.args)

  def test_polygon_even_odd(self):
    poly1 = ee.Geometry.Polygon([0, 0, 0, 5, 5, 0])
    self.assertTrue(poly1.toGeoJSON()['evenOdd'])

    # TODO: Use kwargs instead of positional args for evenOdd.
    poly2 = ee.Geometry.Polygon([0, 0, 0, 5, 5, 0], None, None, None, False)
    self.assertFalse(poly2.toGeoJSON()['evenOdd'])

  @parameterized.named_parameters(
      ('_coords', (1, 2, 3, 4, 5), 'Invalid number of coordinates: 5'),
      ('_coordsList', [[1, 2, 3, 4, 5]], 'Invalid number of coordinates: 5'),
      ('_geometry', [[[1, 2], [3, 4], 5]], 'Invalid geometry'),
      ('_tooMuchNesting', [[[[[1, 2], [3, 4], [5, 6]]]]], 'Invalid geometry'),
      ('_badNesting', [[[[1, 2], [3, 4]], [1, 2]]], 'Invalid geometry'),
  )
  def test_polygon_invalid(self, coords, message):
    """Verifies Polygon constructor behavior with invalid arguments."""
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.Polygon(*coords)

  def test_rectangle(self):
    coords = [1, 2, 3, 4]
    proj = 'EPSG:4326'
    geodesic = True
    even_odd = True
    func = ee.ApiFunction.lookup('GeometryConstructors.Rectangle')

    geometry = ee.Geometry.Rectangle(coords, proj, geodesic, even_odd)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {
            'coordinates': ee.List(coords),
            'crs': ee.Projection(proj),
            'geodesic': geodesic,
            'evenOdd': even_odd,
        },
        geometry.args,
    )

  def test_rectangle_kwargs(self):
    coords = [1, 2, 3, 4]
    func = ee.ApiFunction.lookup('GeometryConstructors.Rectangle')
    geodesic = True

    geometry = ee.Geometry.Rectangle(coords, geodesic=geodesic)
    self.assertEqual(func, geometry.func)
    self.assertEqual(
        {'coordinates': ee.List(coords), 'geodesic': geodesic},
        geometry.args,
    )

  def test_rectangle_old_keyword_args(self):
    """Verifies that Rectangles still allow keyword xlo/ylo/xhi/yhi args."""
    self.assertEqual(
        ee.Geometry.Rectangle(1, 2, 3, 4),
        ee.Geometry.Rectangle(xlo=1, ylo=2, xhi=3, yhi=4),
    )
    self.assertEqual(
        ee.Geometry.Rectangle(1, 2, 3, 4),
        ee.Geometry.Rectangle(1, 2, xhi=3, yhi=4),
    )

  def test_rectangle_computed_args(self):
    """Verifies that a Rectangle with computed inputs can be constructed."""
    p1 = ee.Geometry.Point([1, 2])
    p2 = ee.Geometry.Point([3, 4])
    line = ee.Geometry.Rectangle([p1, p2])

    self.assertIsInstance(line, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.Rectangle'), line.func
    )
    self.assertEqual({'coordinates': ee.List([p1, p2])}, line.args)

  @parameterized.named_parameters(
      ('_coords', (1, 2, 3, 4, 5), 'Invalid number of coordinates: 5'),
      ('_coordsList', [[1, 2, 3, 4, 5]], 'Invalid number of coordinates: 5'),
      ('_geometry', [[[1, 2], [3, 4], 5]], 'Invalid geometry'),
      ('_tooMuchNesting', [[[[1, 2], [3, 4]]]], 'Invalid geometry'),
  )
  def test_rectangle_invalid(self, coords, message):
    """Verifies Rectangle constructor behavior with invalid arguments."""
    with self.assertRaisesRegex(ee.EEException, message):
      ee.Geometry.Rectangle(*coords)

  def test_valid_geometry_collection(self):
    """Verifies GeometryCollection constructor behavior with valid arguments."""
    geometry = ee.Geometry({
        'type': 'GeometryCollection',
        'geometries': [
            {
                'type': 'Polygon',
                'coordinates': [[[-1, -1], [0, 1], [1, -1]]],
                'geodesic': True,
                'evenOdd': True,
            },
            {'type': 'Point', 'coordinates': [0, 0]},
            {
                'type': 'GeometryCollection',
                'geometries': [
                    {'type': 'Point', 'coordinates': [1, 2]},
                    {'type': 'Point', 'coordinates': [2, 1]},
                ],
            },
        ],
        'coordinates': [],
    })
    self.assertIsInstance(geometry, ee.Geometry)

  def test_array_constructors(self):
    """Verifies that constructors that take arrays fix nesting."""
    get_coordinates_count = lambda g: len(g.toGeoJSON()['coordinates'])

    point = ee.Geometry.Point([1, 2])
    self.assertEqual(2, get_coordinates_count(point))

    multipoint = ee.Geometry.MultiPoint([[1, 2], [3, 4], [5, 6]])
    self.assertEqual(3, get_coordinates_count(multipoint))

    line = ee.Geometry.LineString([[1, 2], [3, 4], [5, 6]])
    self.assertEqual(3, get_coordinates_count(line))

    ring = ee.Geometry.LinearRing([[1, 2], [3, 4], [5, 6]])
    self.assertEqual(3, get_coordinates_count(ring))

    multiline = ee.Geometry.MultiLineString(
        [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
    )
    self.assertEqual(2, get_coordinates_count(multiline))

    polygon = ee.Geometry.Polygon([[[1, 2], [3, 4], [5, 6]]])
    self.assertEqual(1, get_coordinates_count(polygon))

    mpolygon = ee.Geometry.MultiPolygon(
        [[[[1, 2], [3, 4], [5, 6]]], [[[1, 2], [3, 4], [5, 6]]]]
    )
    self.assertEqual(2, get_coordinates_count(mpolygon))

  def test_geodesic_flag(self):
    """Verifies that JSON parsing and generation preserves the geodesic flag."""
    geodesic = ee.Geometry({
        'type': 'LineString',
        'coordinates': [[1, 2], [3, 4]],
        'geodesic': True,
    })
    projected = ee.Geometry({
        'type': 'LineString',
        'coordinates': [[1, 2], [3, 4]],
        'geodesic': False,
    })
    self.assertTrue(geodesic.toGeoJSON()['geodesic'])
    self.assertFalse(projected.toGeoJSON()['geodesic'])

  def test_constructor(self):
    """Check the behavior of the Geometry constructor.

    There are 5 options:
      1) A geoJSON object.
      2) A not-computed geometry.
      3) A not-computed geometry with overrides.
      4) A computed geometry with no overrides.
      5) something to cast to geometry.
    """
    line = ee.Geometry.LineString(1, 2, 3, 4)

    # GeoJSON.
    from_json = ee.Geometry(line.toGeoJSON())
    self.assertIsNone(from_json.func)
    self.assertEqual(from_json._type, 'LineString')
    self.assertEqual(from_json._coordinates, [[1, 2], [3, 4]])

    # GeoJSON with a CRS specified.
    json_with_crs = line.toGeoJSON()
    json_with_crs['crs'] = {
        'type': 'name',
        'properties': {'name': 'SR-ORG:6974'},
    }
    from_json_with_crs = ee.Geometry(json_with_crs)
    self.assertIsNone(from_json_with_crs.func)
    self.assertEqual(from_json_with_crs._type, 'LineString')
    self.assertEqual(from_json_with_crs._proj, 'SR-ORG:6974')

    # A not-computed geometry.
    self.assertEqual(ee.Geometry(line), line)

    # A not-computed geometry with an override.
    with_override = ee.Geometry(line, 'SR-ORG:6974')
    self.assertEqual(with_override._proj, 'SR-ORG:6974')

    # A computed geometry.
    self.assertEqual(ee.Geometry(line.bounds()), line.bounds())

    # Something to cast to a geometry.
    computed = ee.ComputedObject(ee.Function(), {'a': 1})
    geom = ee.Geometry(computed)
    self.assertEqual(computed.func, geom.func)
    self.assertEqual(computed.args, geom.args)

  @parameterized.named_parameters(
      ('_proj', {'proj': 'some-proj'}),
      ('_geodesic', {'geodesic': False}),
      ('_geodesic_true', {'geodesic': True}),
      ('_evenOdd', {'evenOdd': False}),
      ('_evenOdd_true', {'evenOdd': True}),
  )
  def test_constructor_error_for_computed_geometry_with_overrides(self, kwargs):
    """Verifies that Geometry raises for computed geometries with overrides."""
    computed_geometry = ee.Geometry.Polygon(
        coords=[[[-35, -10], [-35, 10], [35, 10], [35, -10], [-35, -10]]],
        geodesic=True,
    )
    with self.assertRaisesRegex(
        ee.EEException,
        'Setting the CRS or geodesic on a computed Geometry is not supported.',
    ):
      ee.Geometry(computed_geometry, **kwargs)

  def test_computed_geometries(self):
    """Verifies the computed object behavior of the Geometry constructor."""
    line = ee.Geometry.LineString(1, 2, 3, 4)
    bounds = line.bounds()

    self.assertIsInstance(bounds, ee.Geometry)
    self.assertEqual(ee.ApiFunction.lookup('Geometry.bounds'), bounds.func)
    self.assertEqual(line, bounds.args['geometry'])
    self.assertTrue(hasattr(bounds, 'bounds'))

  def test_computed_coordinate(self):
    """Verifies that a computed coordinate produces a computed geometry."""
    coords = [1, ee.Number(1).add(1)]
    p = ee.Geometry.Point(coords)

    self.assertIsInstance(p, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.Point'), p.func
    )
    self.assertEqual({'coordinates': ee.List(coords)}, p.args)

  def test_computed_list(self):
    """Verifies that a computed coordinate produces a computed geometry."""
    lst = ee.List([1, 2, 3, 4]).slice(0, 2)
    p = ee.Geometry.Point(lst)

    self.assertIsInstance(p, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.Point'), p.func
    )
    self.assertEqual({'coordinates': lst}, p.args)

  def test_computed_projection(self):
    """Verifies that a geometry with a projection can be constructed."""
    p = ee.Geometry.Point([1, 2], 'epsg:4326')

    self.assertIsInstance(p, ee.Geometry)
    self.assertEqual(
        ee.ApiFunction.lookup('GeometryConstructors.Point'), p.func
    )
    expected_args = {
        'coordinates': ee.List([1, 2]),
        'crs': ee.ApiFunction.lookup('Projection').call('epsg:4326'),
    }
    self.assertEqual(expected_args, p.args)

  def test_invalid_crs(self):
    geo_json = ee.Geometry.LineString(1, 2, 3, 4).toGeoJSON()
    geo_json['crs'] = {'something': 'invalid-crs'}
    with self.assertRaisesRegex(ee.EEException, 'Invalid CRS declaration'):
      ee.Geometry(geo_json)

  def test_internals(self):
    """Test eq(), ne() and hash()."""
    a = ee.Geometry.Point(1, 2)
    b = ee.Geometry.Point(2, 1)
    c = ee.Geometry.Point(1, 2)

    self.assertEqual(a, a)
    self.assertNotEqual(a, b)
    self.assertEqual(a, c)
    self.assertNotEqual(b, c)
    self.assertNotEqual(hash(a), hash(b))

  def test_coordinates_to_line(self):
    # pylint: disable=protected-access
    self.assertEqual([], ee.Geometry._coordinatesToLine([]))
    self.assertEqual([1, 2], ee.Geometry._coordinatesToLine([1, 2]))
    # TODO: schwehr - Improve _coordinatesToLine typing.
    self.assertEqual(
        [[1, 2]],
        ee.Geometry._coordinatesToLine([[1, 2]]),  # pytype: disable=wrong-arg-types
    )
    self.assertEqual(
        [[1, 2], [3, 4]], ee.Geometry._coordinatesToLine([1, 2, 3, 4])
    )
    self.assertEqual(
        [[1, 2], [3, 4], [5, 6]],
        ee.Geometry._coordinatesToLine([1, 2, 3, 4, 5, 6]),
    )
    with self.assertRaisesRegex(
        ee.EEException, 'Invalid number of coordinates: 5'
    ):
      ee.Geometry._coordinatesToLine([1, 2, 3, 4, 5])
    # pylint: enable=protected-access

  def test_init_opt_params(self):
    result = ee.Geometry(
        geo_json={'type': 'Polygon', 'coordinates': [[[-2, 1]]]},
        opt_proj='abc',
        opt_geodesic=True,
        opt_evenOdd=True,
    ).serialize()
    self.assertIn('"crs": {"constantValue": "abc"}', result)
    self.assertIn('"geodesic": {"constantValue": true}', result)
    self.assertIn('"evenOdd": {"constantValue": true}', result)

  @parameterized.named_parameters(
      (
          'BBox',
          lambda: ee.Geometry.BBox(1, 2, 3, 4),
          dict(
              coordinates=[[[1, 4], [1, 2], [3, 2], [3, 4]]],
              geodesic=False,
              type='Polygon',
          ),
      ),
      (
          'LineString',
          lambda: ee.Geometry.LineString(1, 2, 3, 4),
          dict(coordinates=[[1, 2], [3, 4]], type='LineString'),
      ),
      (
          'LinearRing',
          lambda: ee.Geometry.LinearRing(1, 2, 3, 4),
          dict(coordinates=[[1, 2], [3, 4]], type='LinearRing'),
      ),
      (
          'MultiLineString',
          lambda: ee.Geometry.MultiLineString(1, 2, 3, 4),
          dict(coordinates=[[[1, 2], [3, 4]]], type='MultiLineString'),
      ),
      (
          'MultiLineString_Empty',
          lambda: ee.Geometry.MultiLineString(),  # pylint: disable=unnecessary-lambda
          dict(coordinates=[], type='MultiLineString'),
      ),
      (
          'MultiPoint',
          lambda: ee.Geometry.MultiPoint(1, 2, 3, 4),
          dict(coordinates=[[1, 2], [3, 4]], type='MultiPoint'),
      ),
      (
          'MultiPoint_Empty',
          lambda: ee.Geometry.MultiPoint(),  # pylint: disable=unnecessary-lambda
          dict(coordinates=[], type='MultiPoint'),
      ),
      (
          'MultiPolygon',
          lambda: ee.Geometry.MultiPolygon(1, 2, 3, 4),
          dict(
              coordinates=[[[[1, 2], [3, 4]]]],
              evenOdd=True,
              type='MultiPolygon',
          ),
      ),
      (
          'MultiPolygon_Empty',
          lambda: ee.Geometry.MultiPolygon(),  # pylint: disable=unnecessary-lambda
          dict(coordinates=[], evenOdd=True, type='MultiPolygon'),
      ),
      (
          'Point',
          lambda: ee.Geometry.Point(coords=[1, 2]),
          dict(coordinates=[1, 2], type='Point'),
      ),
      (
          'Polygon',
          lambda: ee.Geometry.Polygon(1, 2, 3, 4),
          dict(
              coordinates=[[[1, 2], [3, 4]]],
              evenOdd=True,
              type='Polygon',
          ),
      ),
      (
          'Rectangle',
          lambda: ee.Geometry.Rectangle(1, 2, 3, 4),
          dict(
              coordinates=[[[1, 4], [1, 2], [3, 2], [3, 4]]],
              evenOdd=True,
              type='Polygon',
          ),
      ),
  )
  def test_to_geo_json(self, geometry_func, expected_geojson):
    self.assertEqual(expected_geojson, geometry_func().toGeoJSON())

  @parameterized.named_parameters(
      # BBox doesn't have any other arguments.
      ('LineString', lambda: ee.Geometry.LineString([], 'EPSG:4326')),
      ('LinearRing', lambda: ee.Geometry.LinearRing([], 'EPSG:4326')),
      ('MultiLineString', lambda: ee.Geometry.MultiLineString([], 'EPSG:4326')),
      ('MultiPoint', lambda: ee.Geometry.MultiPoint([], 'EPSG:4326')),
      ('MultiPolygon', lambda: ee.Geometry.MultiPolygon([], 'EPSG:4326')),
      ('Point', lambda: ee.Geometry.Point([], 'EPSG:4326')),
      ('Polygon', lambda: ee.Geometry.Polygon([], 'EPSG:4326')),
      ('Rectangle', lambda: ee.Geometry.Rectangle([], 'EPSG:4326')),
  )
  def test_to_geo_json_fails_on_computed(self, geometry_func):
    message = 'Cannot convert a computed geometry to GeoJSON'
    with self.assertRaisesRegex(ee.EEException, message):
      geometry_func().toGeoJSON()


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