import json
import pickle
import struct
from unittest import mock

import numpy as np
import pytest

import pygeos
from pygeos.testing import assert_geometries_equal

from .common import all_types, empty_point, empty_point_z, point, point_z

# fmt: off
POINT11_WKB = b"\x01\x01\x00\x00\x00" + struct.pack("<2d", 1.0, 1.0)
NAN = struct.pack("<d", float("nan"))
POINT_NAN_WKB = b'\x01\x01\x00\x00\x00' + (NAN * 2)
POINTZ_NAN_WKB = b'\x01\x01\x00\x00\x80' + (NAN * 3)
MULTIPOINT_NAN_WKB = b'\x01\x04\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x00' + (NAN * 2)
MULTIPOINTZ_NAN_WKB = b'\x01\x04\x00\x00\x80\x01\x00\x00\x00\x01\x01\x00\x00\x80' + (NAN * 3)
GEOMETRYCOLLECTION_NAN_WKB = b'\x01\x07\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x00' + (NAN * 2)
GEOMETRYCOLLECTIONZ_NAN_WKB = b'\x01\x07\x00\x00\x80\x01\x00\x00\x00\x01\x01\x00\x00\x80' + (NAN * 3)
NESTED_COLLECTION_NAN_WKB = b'\x01\x07\x00\x00\x00\x01\x00\x00\x00\x01\x04\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x00' + (NAN * 2)
NESTED_COLLECTIONZ_NAN_WKB = b'\x01\x07\x00\x00\x80\x01\x00\x00\x00\x01\x04\x00\x00\x80\x01\x00\x00\x00\x01\x01\x00\x00\x80' + (NAN * 3)
INVALID_WKB = "01030000000100000002000000507daec600b1354100de02498e5e3d41306ea321fcb03541a011a53d905e3d41"
# fmt: on


GEOJSON_GEOMETRY = json.dumps({"type": "Point", "coordinates": [125.6, 10.1]}, indent=4)
GEOJSON_FEATURE = json.dumps(
    {
        "type": "Feature",
        "geometry": {"type": "Point", "coordinates": [125.6, 10.1]},
        "properties": {"name": "Dinagat Islands"},
    },
    indent=4,
)
GEOJSON_FEATURECOLECTION = json.dumps(
    {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {"type": "Point", "coordinates": [102.0, 0.6]},
                "properties": {"prop0": "value0"},
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [102.0, 0.0],
                        [103.0, 1.0],
                        [104.0, 0.0],
                        [105.0, 1.0],
                    ],
                },
                "properties": {"prop1": 0.0, "prop0": "value0"},
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "Polygon",
                    "coordinates": [
                        [
                            [100.0, 0.0],
                            [101.0, 0.0],
                            [101.0, 1.0],
                            [100.0, 1.0],
                            [100.0, 0.0],
                        ]
                    ],
                },
                "properties": {"prop1": {"this": "that"}, "prop0": "value0"},
            },
        ],
    },
    indent=4,
)

GEOJSON_GEOMETRY_EXPECTED = pygeos.points(125.6, 10.1)
GEOJSON_COLLECTION_EXPECTED = [
    pygeos.points([102.0, 0.6]),
    pygeos.linestrings([[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]),
    pygeos.polygons(
        [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]
    ),
]


class ShapelyGeometryMock:
    def __init__(self, g):
        self.g = g
        self.__geom__ = g._ptr if hasattr(g, "_ptr") else g

    @property
    def __array_interface__(self):
        # this should not be called
        # (starting with numpy 1.20 it is called, but not used)
        return np.array([1.0, 2.0]).__array_interface__

    @property
    def wkb(self):
        return pygeos.to_wkb(self.g)

    @property
    def geom_type(self):
        idx = pygeos.get_type_id(self.g)
        return [
            "None",
            "Point",
            "LineString",
            "LinearRing",
            "Polygon",
            "MultiPoint",
            "MultiLineString",
            "MultiPolygon",
            "GeometryCollection",
        ][idx]

    @property
    def is_empty(self):
        return pygeos.is_empty(self.g)


class ShapelyPreparedMock:
    def __init__(self, g):
        self.context = ShapelyGeometryMock(g)


def shapely_wkb_loads_mock(wkb):
    geom = pygeos.from_wkb(wkb)
    return ShapelyGeometryMock(geom)


def test_from_wkt():
    expected = pygeos.points(1, 1)
    actual = pygeos.from_wkt("POINT (1 1)")
    assert_geometries_equal(actual, expected)
    # also accept bytes
    actual = pygeos.from_wkt(b"POINT (1 1)")
    assert_geometries_equal(actual, expected)


def test_from_wkt_none():
    # None propagates
    assert pygeos.from_wkt(None) is None


def test_from_wkt_exceptions():
    with pytest.raises(TypeError, match="Expected bytes or string, got int"):
        pygeos.from_wkt(1)

    with pytest.raises(
        pygeos.GEOSException, match="Expected word but encountered end of stream"
    ):
        pygeos.from_wkt("")

    with pytest.raises(pygeos.GEOSException, match="Unknown type: 'NOT'"):
        pygeos.from_wkt("NOT A WKT STRING")


def test_from_wkt_warn_on_invalid():
    with pytest.warns(Warning, match="Invalid WKT"):
        pygeos.from_wkt("", on_invalid="warn")

    with pytest.warns(Warning, match="Invalid WKT"):
        pygeos.from_wkt("NOT A WKT STRING", on_invalid="warn")


def test_from_wkb_ignore_on_invalid():
    with pytest.warns(None):
        pygeos.from_wkt("", on_invalid="ignore")

    with pytest.warns(None):
        pygeos.from_wkt("NOT A WKT STRING", on_invalid="ignore")


def test_from_wkt_on_invalid_unsupported_option():
    with pytest.raises(ValueError, match="not a valid option"):
        pygeos.from_wkt(b"\x01\x01\x00\x00\x00\x00", on_invalid="unsupported_option")


@pytest.mark.parametrize("geom", all_types)
def test_from_wkt_all_types(geom):
    wkt = pygeos.to_wkt(geom)
    actual = pygeos.from_wkt(wkt)
    assert_geometries_equal(actual, geom)


@pytest.mark.parametrize(
    "wkt",
    ("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY"),
)
def test_from_wkt_empty(wkt):
    geom = pygeos.from_wkt(wkt)
    assert pygeos.is_geometry(geom).all()
    assert pygeos.is_empty(geom).all()
    assert pygeos.to_wkt(geom) == wkt


def test_from_wkb():
    expected = pygeos.points(1, 1)
    actual = pygeos.from_wkb(POINT11_WKB)
    assert_geometries_equal(actual, expected)


def test_from_wkb_hex():
    # HEX form
    expected = pygeos.points(1, 1)
    actual = pygeos.from_wkb("0101000000000000000000F03F000000000000F03F")
    assert_geometries_equal(actual, expected)
    actual = pygeos.from_wkb(b"0101000000000000000000F03F000000000000F03F")
    assert_geometries_equal(actual, expected)


def test_from_wkb_none():
    # None propagates
    assert pygeos.from_wkb(None) is None


def test_from_wkb_exceptions():
    with pytest.raises(TypeError, match="Expected bytes or string, got int"):
        pygeos.from_wkb(1)

    # invalid WKB
    with pytest.raises(pygeos.GEOSException, match="Unexpected EOF parsing WKB"):
        result = pygeos.from_wkb(b"\x01\x01\x00\x00\x00\x00")
        assert result is None

    # invalid ring in WKB
    with pytest.raises(
        pygeos.GEOSException,
        match="Points of LinearRing do not form a closed linestring",
    ):
        result = pygeos.from_wkb(INVALID_WKB)
        assert result is None


def test_from_wkb_warn_on_invalid_warn():
    # invalid WKB
    with pytest.warns(Warning, match="Invalid WKB"):
        result = pygeos.from_wkb(b"\x01\x01\x00\x00\x00\x00", on_invalid="warn")
        assert result is None

    # invalid ring in WKB
    with pytest.warns(Warning, match="Invalid WKB"):
        result = pygeos.from_wkb(INVALID_WKB, on_invalid="warn")
        assert result is None


def test_from_wkb_ignore_on_invalid_ignore():
    # invalid WKB
    with pytest.warns(None) as w:
        result = pygeos.from_wkb(b"\x01\x01\x00\x00\x00\x00", on_invalid="ignore")
        assert result is None
        assert len(w) == 0  # no warning

    # invalid ring in WKB
    with pytest.warns(None) as w:
        result = pygeos.from_wkb(INVALID_WKB, on_invalid="ignore")
        assert result is None
        assert len(w) == 0  # no warning


def test_from_wkb_on_invalid_unsupported_option():
    with pytest.raises(ValueError, match="not a valid option"):
        pygeos.from_wkb(b"\x01\x01\x00\x00\x00\x00", on_invalid="unsupported_option")


@pytest.mark.parametrize("geom", all_types)
@pytest.mark.parametrize("use_hex", [False, True])
@pytest.mark.parametrize("byte_order", [0, 1])
def test_from_wkb_all_types(geom, use_hex, byte_order):
    if pygeos.get_type_id(geom) == pygeos.GeometryType.LINEARRING:
        pytest.skip("Linearrings are not preserved in WKB")
    wkb = pygeos.to_wkb(geom, hex=use_hex, byte_order=byte_order)
    actual = pygeos.from_wkb(wkb)
    assert_geometries_equal(actual, geom)


@pytest.mark.parametrize(
    "wkt",
    ("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY"),
)
def test_from_wkb_empty(wkt):
    wkb = pygeos.to_wkb(pygeos.Geometry(wkt))
    geom = pygeos.from_wkb(wkb)
    assert pygeos.is_geometry(geom).all()
    assert pygeos.is_empty(geom).all()
    assert pygeos.to_wkb(geom) == wkb


def test_to_wkt():
    point = pygeos.points(1, 1)
    actual = pygeos.to_wkt(point)
    assert actual == "POINT (1 1)"

    actual = pygeos.to_wkt(point, trim=False)
    assert actual == "POINT (1.000000 1.000000)"

    actual = pygeos.to_wkt(point, rounding_precision=3, trim=False)
    assert actual == "POINT (1.000 1.000)"


def test_to_wkt_3D():
    # 3D points
    point_z = pygeos.points(1, 1, 1)
    actual = pygeos.to_wkt(point_z)
    assert actual == "POINT Z (1 1 1)"
    actual = pygeos.to_wkt(point_z, output_dimension=3)
    assert actual == "POINT Z (1 1 1)"

    actual = pygeos.to_wkt(point_z, output_dimension=2)
    assert actual == "POINT (1 1)"

    actual = pygeos.to_wkt(point_z, old_3d=True)
    assert actual == "POINT (1 1 1)"


def test_to_wkt_none():
    # None propagates
    assert pygeos.to_wkt(None) is None


def test_to_wkt_exceptions():
    with pytest.raises(TypeError):
        pygeos.to_wkt(1)

    with pytest.raises(pygeos.GEOSException):
        pygeos.to_wkt(point, output_dimension=4)


def test_to_wkt_point_empty():
    assert pygeos.to_wkt(empty_point) == "POINT EMPTY"


@pytest.mark.skipif(
    pygeos.geos_version < (3, 9, 0),
    reason="Empty geometries have no dimensionality on GEOS < 3.9",
)
@pytest.mark.parametrize(
    "wkt",
    [
        "POINT Z EMPTY",
        "LINESTRING Z EMPTY",
        "LINEARRING Z EMPTY",
        "POLYGON Z EMPTY",
    ],
)
def test_to_wkt_empty_z(wkt):
    assert pygeos.to_wkt(pygeos.Geometry(wkt)) == wkt


def test_to_wkt_geometrycollection_with_point_empty():
    collection = pygeos.geometrycollections([empty_point, point])
    # do not check the full value as some GEOS versions give
    # GEOMETRYCOLLECTION Z (...) and others give GEOMETRYCOLLECTION (...)
    assert pygeos.to_wkt(collection).endswith("(POINT EMPTY, POINT (2 3))")


@pytest.mark.skipif(
    pygeos.geos_version < (3, 9, 0),
    reason="MULTIPOINT (EMPTY, 2 3) only works for GEOS >= 3.9",
)
def test_to_wkt_multipoint_with_point_empty():
    geom = pygeos.multipoints([empty_point, point])
    assert pygeos.to_wkt(geom) == "MULTIPOINT (EMPTY, 2 3)"


@pytest.mark.skipif(
    pygeos.geos_version >= (3, 9, 0),
    reason="MULTIPOINT (EMPTY, 2 3) gives ValueError on GEOS < 3.9",
)
def test_to_wkt_multipoint_with_point_empty_errors():
    # test if segfault is prevented
    geom = pygeos.multipoints([empty_point, point])
    with pytest.raises(ValueError):
        pygeos.to_wkt(geom)


def test_repr():
    assert repr(point) == "<pygeos.Geometry POINT (2 3)>"


def test_repr_max_length():
    # the repr is limited to 80 characters
    geom = pygeos.linestrings(np.arange(1000), np.arange(1000))
    representation = repr(geom)
    assert len(representation) == 80
    assert representation.endswith("...>")


@pytest.mark.skipif(
    pygeos.geos_version >= (3, 9, 0),
    reason="MULTIPOINT (EMPTY, 2 3) gives Exception on GEOS < 3.9",
)
def test_repr_multipoint_with_point_empty():
    # Test if segfault is prevented
    geom = pygeos.multipoints([point, empty_point])
    assert repr(geom) == "<pygeos.Geometry Exception in WKT writer>"


@pytest.mark.skipif(
    pygeos.geos_version < (3, 9, 0),
    reason="Empty geometries have no dimensionality on GEOS < 3.9",
)
def test_repr_point_z_empty():
    assert repr(empty_point_z) == "<pygeos.Geometry POINT Z EMPTY>"


def test_to_wkb():
    point = pygeos.points(1, 1)
    actual = pygeos.to_wkb(point, byte_order=1)
    assert actual == POINT11_WKB


def test_to_wkb_hex():
    point = pygeos.points(1, 1)
    actual = pygeos.to_wkb(point, hex=True, byte_order=1)
    le = "01"
    point_type = "01000000"
    coord = "000000000000F03F"  # 1.0 as double (LE)
    assert actual == le + point_type + 2 * coord


def test_to_wkb_3D():
    point_z = pygeos.points(1, 1, 1)
    actual = pygeos.to_wkb(point_z, byte_order=1)
    # fmt: off
    assert actual == b"\x01\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?"  # noqa
    # fmt: on
    actual = pygeos.to_wkb(point_z, output_dimension=2, byte_order=1)
    assert actual == POINT11_WKB


def test_to_wkb_none():
    # None propagates
    assert pygeos.to_wkb(None) is None


def test_to_wkb_exceptions():
    with pytest.raises(TypeError):
        pygeos.to_wkb(1)

    with pytest.raises(pygeos.GEOSException):
        pygeos.to_wkb(point, output_dimension=4)


def test_to_wkb_byte_order():
    point = pygeos.points(1.0, 1.0)
    be = b"\x00"
    le = b"\x01"
    point_type = b"\x01\x00\x00\x00"  # 1 as 32-bit uint (LE)
    coord = b"\x00\x00\x00\x00\x00\x00\xf0?"  # 1.0 as double (LE)

    assert pygeos.to_wkb(point, byte_order=1) == le + point_type + 2 * coord
    assert pygeos.to_wkb(point, byte_order=0) == be + point_type[::-1] + 2 * coord[::-1]


def test_to_wkb_srid():
    # hex representation of POINT (0 0) with SRID=4
    ewkb = "01010000200400000000000000000000000000000000000000"
    wkb = "010100000000000000000000000000000000000000"

    actual = pygeos.from_wkb(ewkb)
    assert pygeos.to_wkt(actual, trim=True) == "POINT (0 0)"

    assert pygeos.to_wkb(actual, hex=True, byte_order=1) == wkb
    assert pygeos.to_wkb(actual, hex=True, include_srid=True, byte_order=1) == ewkb

    point = pygeos.points(1, 1)
    point_with_srid = pygeos.set_srid(point, np.int32(4326))
    result = pygeos.to_wkb(point_with_srid, include_srid=True, byte_order=1)
    assert np.frombuffer(result[5:9], "<u4").item() == 4326


@pytest.mark.parametrize(
    "geom,expected",
    [
        (empty_point, POINT_NAN_WKB),
        (empty_point_z, POINT_NAN_WKB),
        (pygeos.multipoints([empty_point]), MULTIPOINT_NAN_WKB),
        (pygeos.multipoints([empty_point_z]), MULTIPOINT_NAN_WKB),
        (pygeos.geometrycollections([empty_point]), GEOMETRYCOLLECTION_NAN_WKB),
        (pygeos.geometrycollections([empty_point_z]), GEOMETRYCOLLECTION_NAN_WKB),
        (
            pygeos.geometrycollections([pygeos.multipoints([empty_point])]),
            NESTED_COLLECTION_NAN_WKB,
        ),
        (
            pygeos.geometrycollections([pygeos.multipoints([empty_point_z])]),
            NESTED_COLLECTION_NAN_WKB,
        ),
    ],
)
def test_to_wkb_point_empty_2d(geom, expected):
    actual = pygeos.to_wkb(geom, output_dimension=2, byte_order=1)
    # Split 'actual' into header and coordinates
    coordinate_length = 16
    header_length = len(expected) - coordinate_length
    # Check the total length (this checks the correct dimensionality)
    assert len(actual) == header_length + coordinate_length
    # Check the header
    assert actual[:header_length] == expected[:header_length]
    # Check the coordinates (using numpy.isnan; there are many byte representations for NaN)
    assert np.isnan(struct.unpack("<2d", actual[header_length:])).all()


@pytest.mark.xfail(
    pygeos.geos_version[:2] == (3, 8), reason="GEOS==3.8 never outputs 3D empty points"
)
@pytest.mark.parametrize(
    "geom,expected",
    [
        (empty_point_z, POINTZ_NAN_WKB),
        (pygeos.multipoints([empty_point_z]), MULTIPOINTZ_NAN_WKB),
        (pygeos.geometrycollections([empty_point_z]), GEOMETRYCOLLECTIONZ_NAN_WKB),
        (
            pygeos.geometrycollections([pygeos.multipoints([empty_point_z])]),
            NESTED_COLLECTIONZ_NAN_WKB,
        ),
    ],
)
def test_to_wkb_point_empty_3d(geom, expected):
    actual = pygeos.to_wkb(geom, output_dimension=3, byte_order=1)
    # Split 'actual' into header and coordinates
    coordinate_length = 24
    header_length = len(expected) - coordinate_length
    # Check the total length (this checks the correct dimensionality)
    assert len(actual) == header_length + coordinate_length
    # Check the header
    assert actual[:header_length] == expected[:header_length]
    # Check the coordinates (using numpy.isnan; there are many byte representations for NaN)
    assert np.isnan(struct.unpack("<3d", actual[header_length:])).all()


@pytest.mark.xfail(
    pygeos.geos_version < (3, 8, 0),
    reason="GEOS<3.8 always outputs 3D empty points if output_dimension=3",
)
@pytest.mark.parametrize(
    "geom,expected",
    [
        (empty_point, POINT_NAN_WKB),
        (pygeos.multipoints([empty_point]), MULTIPOINT_NAN_WKB),
        (pygeos.geometrycollections([empty_point]), GEOMETRYCOLLECTION_NAN_WKB),
        (
            pygeos.geometrycollections([pygeos.multipoints([empty_point])]),
            NESTED_COLLECTION_NAN_WKB,
        ),
    ],
)
def test_to_wkb_point_empty_2d_output_dim_3(geom, expected):
    actual = pygeos.to_wkb(geom, output_dimension=3, byte_order=1)
    # Split 'actual' into header and coordinates
    coordinate_length = 16
    header_length = len(expected) - coordinate_length
    # Check the total length (this checks the correct dimensionality)
    assert len(actual) == header_length + coordinate_length
    # Check the header
    assert actual[:header_length] == expected[:header_length]
    # Check the coordinates (using numpy.isnan; there are many byte representations for NaN)
    assert np.isnan(struct.unpack("<2d", actual[header_length:])).all()


@pytest.mark.parametrize(
    "wkb,expected_type,expected_dim",
    [
        (POINT_NAN_WKB, 0, 2),
        (POINTZ_NAN_WKB, 0, 3),
        (MULTIPOINT_NAN_WKB, 4, 2),
        (MULTIPOINTZ_NAN_WKB, 4, 3),
        (GEOMETRYCOLLECTION_NAN_WKB, 7, 2),
        (GEOMETRYCOLLECTIONZ_NAN_WKB, 7, 3),
        (NESTED_COLLECTION_NAN_WKB, 7, 2),
        (NESTED_COLLECTIONZ_NAN_WKB, 7, 3),
    ],
)
def test_from_wkb_point_empty(wkb, expected_type, expected_dim):
    geom = pygeos.from_wkb(wkb)
    # POINT (nan nan) transforms to an empty point
    assert pygeos.is_empty(geom)
    assert pygeos.get_type_id(geom) == expected_type
    # The dimensionality (2D/3D) is only read correctly for GEOS >= 3.9.0
    if pygeos.geos_version >= (3, 9, 0):
        assert pygeos.get_coordinate_dimension(geom) == expected_dim


def test_to_wkb_point_empty_srid():
    expected = pygeos.set_srid(empty_point, 4236)
    wkb = pygeos.to_wkb(expected, include_srid=True)
    actual = pygeos.from_wkb(wkb)
    assert pygeos.get_srid(actual) == 4236


@pytest.mark.parametrize("geom", all_types)
@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", True)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely(geom):
    actual = pygeos.from_shapely(ShapelyGeometryMock(geom))
    assert isinstance(actual, pygeos.Geometry)
    assert_geometries_equal(geom, actual)
    assert geom._ptr != actual._ptr


@pytest.mark.parametrize("geom", all_types)
@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", True)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_prepared(geom):
    actual = pygeos.from_shapely(ShapelyPreparedMock(geom))
    assert isinstance(actual, pygeos.Geometry)
    assert_geometries_equal(geom, actual)
    assert geom._ptr != actual._ptr


@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", True)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_arr():
    actual = pygeos.from_shapely([ShapelyGeometryMock(point), None])
    assert_geometries_equal(point, actual[0])


@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", True)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_none():
    actual = pygeos.from_shapely(None)
    assert actual is None


@pytest.mark.parametrize("geom", [1, 2.3, "x", ShapelyGeometryMock(None)])
@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", True)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_error(geom):
    with pytest.raises(TypeError):
        pygeos.from_shapely(geom)


@pytest.mark.parametrize("geom", all_types)
@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", False)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_incompatible(geom):
    if pygeos.get_type_id(geom) == pygeos.GeometryType.LINEARRING:
        pytest.skip("Linearrings are not preserved in WKB")
    actual = pygeos.from_shapely(ShapelyGeometryMock(geom))
    assert isinstance(actual, pygeos.Geometry)
    assert_geometries_equal(geom, actual)
    assert geom._ptr != actual._ptr


@pytest.mark.parametrize("geom", all_types)
@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", False)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_incompatible_prepared(geom):
    if pygeos.get_type_id(geom) == pygeos.GeometryType.LINEARRING:
        pytest.skip("Linearrings are not preserved in WKB")
    actual = pygeos.from_shapely(ShapelyPreparedMock(geom))
    assert isinstance(actual, pygeos.Geometry)
    assert_geometries_equal(geom, actual)
    assert geom._ptr != actual._ptr


@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", False)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_incompatible_none():
    actual = pygeos.from_shapely(None)
    assert actual is None


@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.ShapelyPreparedGeometry", ShapelyPreparedMock)
@mock.patch("pygeos.io.shapely_compatible", False)
@mock.patch("pygeos.io._shapely_checked", True)
def test_from_shapely_incompatible_array():
    actual = pygeos.from_shapely([ShapelyGeometryMock(point), None])
    assert_geometries_equal(point, actual[0])


@pytest.mark.parametrize("geom", all_types)
@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.shapely_wkb_loads", shapely_wkb_loads_mock)
@mock.patch("pygeos.io.shapely_compatible", False)
@mock.patch("pygeos.io._shapely_checked", True)
def test_to_shapely_incompatible(geom):
    if pygeos.get_type_id(geom) == pygeos.GeometryType.LINEARRING:
        pytest.skip("Linearrings are not preserved in WKB")
    actual = pygeos.to_shapely(geom)
    assert isinstance(actual, ShapelyGeometryMock)
    assert_geometries_equal(geom, actual.g)
    assert geom._ptr != actual.g._ptr


@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.shapely_wkb_loads", shapely_wkb_loads_mock)
@mock.patch("pygeos.io.shapely_compatible", False)
@mock.patch("pygeos.io._shapely_checked", True)
def test_to_shapely_incompatible_none():
    actual = pygeos.to_shapely(None)
    assert actual is None


@mock.patch("pygeos.io.ShapelyGeometry", ShapelyGeometryMock)
@mock.patch("pygeos.io.shapely_wkb_loads", shapely_wkb_loads_mock)
@mock.patch("pygeos.io.shapely_compatible", False)
@mock.patch("pygeos.io._shapely_checked", True)
def test_to_shapely_incompatible_array():
    actual = pygeos.to_shapely([point, None])
    assert_geometries_equal(point, actual[0].g)


@pytest.mark.parametrize("geom", all_types + (point_z, empty_point))
def test_pickle(geom):
    if pygeos.get_type_id(geom) == 2:
        # Linearrings get converted to linestrings
        expected = pygeos.linestrings(pygeos.get_coordinates(geom))
    else:
        expected = geom
    pickled = pickle.dumps(geom)
    assert_geometries_equal(pickle.loads(pickled), expected, tolerance=0)


def test_pickle_with_srid():
    geom = pygeos.set_srid(point, 4326)
    pickled = pickle.dumps(geom)
    assert pygeos.get_srid(pickle.loads(pickled)) == 4326


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 1), reason="GEOS < 3.10.1")
@pytest.mark.parametrize(
    "geojson,expected",
    [
        (GEOJSON_GEOMETRY, GEOJSON_GEOMETRY_EXPECTED),
        (GEOJSON_FEATURE, GEOJSON_GEOMETRY_EXPECTED),
        (
            GEOJSON_FEATURECOLECTION,
            pygeos.geometrycollections(GEOJSON_COLLECTION_EXPECTED),
        ),
        ([GEOJSON_GEOMETRY] * 2, [GEOJSON_GEOMETRY_EXPECTED] * 2),
        (None, None),
        ([GEOJSON_GEOMETRY, None], [GEOJSON_GEOMETRY_EXPECTED, None]),
    ],
)
def test_from_geojson(geojson, expected):
    actual = pygeos.from_geojson(geojson)
    assert_geometries_equal(actual, expected)


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 1), reason="GEOS < 3.10.1")
def test_from_geojson_exceptions():
    with pytest.raises(TypeError, match="Expected bytes or string, got int"):
        pygeos.from_geojson(1)

    with pytest.raises(pygeos.GEOSException, match="Error parsing JSON"):
        pygeos.from_geojson("")

    with pytest.raises(pygeos.GEOSException, match="Unknown geometry type"):
        pygeos.from_geojson('{"type": "NoGeometry", "coordinates": []}')

    with pytest.raises(pygeos.GEOSException, match="type must be array, but is null"):
        pygeos.from_geojson('{"type": "LineString", "coordinates": null}')

    # Note: The two below tests are the reason that from_geojson is disabled for
    # GEOS 3.10.0 See https://trac.osgeo.org/geos/ticket/1138
    with pytest.raises(pygeos.GEOSException, match="key 'type' not found"):
        pygeos.from_geojson('{"geometry": null, "properties": []}')

    with pytest.raises(pygeos.GEOSException, match="key 'type' not found"):
        pygeos.from_geojson('{"no": "geojson"}')


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 1), reason="GEOS < 3.10.1")
def test_from_geojson_warn_on_invalid():
    with pytest.warns(Warning, match="Invalid GeoJSON"):
        assert pygeos.from_geojson("", on_invalid="warn") is None


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 1), reason="GEOS < 3.10.1")
def test_from_geojson_ignore_on_invalid():
    with pytest.warns(None):
        assert pygeos.from_geojson("", on_invalid="ignore") is None


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 1), reason="GEOS < 3.10.1")
def test_from_geojson_on_invalid_unsupported_option():
    with pytest.raises(ValueError, match="not a valid option"):
        pygeos.from_geojson(GEOJSON_GEOMETRY, on_invalid="unsupported_option")


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 0), reason="GEOS < 3.10")
@pytest.mark.parametrize(
    "expected,geometry",
    [
        (GEOJSON_GEOMETRY, GEOJSON_GEOMETRY_EXPECTED),
        ([GEOJSON_GEOMETRY] * 2, [GEOJSON_GEOMETRY_EXPECTED] * 2),
        (None, None),
        ([GEOJSON_GEOMETRY, None], [GEOJSON_GEOMETRY_EXPECTED, None]),
    ],
)
def test_to_geojson(geometry, expected):
    actual = pygeos.to_geojson(geometry, indent=4)
    assert np.all(actual == np.asarray(expected))


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 0), reason="GEOS < 3.10")
@pytest.mark.parametrize("indent", [None, 0, 4])
def test_to_geojson_indent(indent):
    separators = (",", ":") if indent is None else (",", ": ")
    expected = json.dumps(
        json.loads(GEOJSON_GEOMETRY), indent=indent, separators=separators
    )
    actual = pygeos.to_geojson(GEOJSON_GEOMETRY_EXPECTED, indent=indent)
    assert actual == expected


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 0), reason="GEOS < 3.10")
def test_to_geojson_exceptions():
    with pytest.raises(TypeError):
        pygeos.to_geojson(1)


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 0), reason="GEOS < 3.10")
@pytest.mark.parametrize(
    "geom",
    [
        empty_point,
        pygeos.multipoints([empty_point, point]),
        pygeos.geometrycollections([empty_point, point]),
        pygeos.geometrycollections([pygeos.geometrycollections([empty_point]), point]),
    ],
)
def test_to_geojson_point_empty(geom):
    # Pending GEOS ticket: https://trac.osgeo.org/geos/ticket/1139
    with pytest.raises(ValueError):
        assert pygeos.to_geojson(geom)


@pytest.mark.skipif(pygeos.geos_version < (3, 10, 1), reason="GEOS < 3.10.1")
@pytest.mark.parametrize("geom", all_types)
def test_geojson_all_types(geom):
    if pygeos.get_type_id(geom) == pygeos.GeometryType.LINEARRING:
        pytest.skip("Linearrings are not preserved in GeoJSON")
    geojson = pygeos.to_geojson(geom)
    actual = pygeos.from_geojson(geojson)
    assert_geometries_equal(actual, geom)
