import json
from datetime import datetime

from django.test import TestCase
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, override
from model_bakery import baker

import teramap
from teramap.geojson import FeatureCollection, GeoJSON, LineString, MultiPoint, Point, TransformToPoint

from .app.models import DualGeomModel, Location, Observation


class FeatureCollectionTest(TestCase):
    def test_multipoint(self):
        feature_collection = FeatureCollection([MultiPoint([[5, 52], [6, 52]]), Point([6, 52])])

        self.assertEqual(len(feature_collection), 2)

    def test_union(self):
        a = FeatureCollection([LineString([[41, 37], [40, 62], [34, 62]]), Point([4, 52])])

        b = FeatureCollection([Point([6, 52])])

        c = a.union(b)

        self.assertEqual(len(a), 2)
        self.assertEqual(len(b), 1)
        self.assertEqual(len(c), 3)

    def test_union_with_or_operation(self):
        a = FeatureCollection([Point([1, 2]), Point([3, 4])])
        b = FeatureCollection([Point([1, 6]), LineString([[2, 8], [4, 9], [9, 3]])])
        c = FeatureCollection([Point([1, 5]), Point([2, 4])])

        d = a + b + c

        def get_coords(obj):
            return [feature.to_geojson()["geometry"]["coordinates"] for feature in obj.features]

        self.assertEqual(len(d), 6)
        self.assertEqual(get_coords(d), [[1, 2], [3, 4], [1, 6], [[2, 8], [4, 9], [9, 3]], [1, 5], [2, 4]])

        e = c + b + a
        self.assertEqual(len(e), 6)
        self.assertEqual(get_coords(e), [[1, 5], [2, 4], [1, 6], [[2, 8], [4, 9], [9, 3]], [1, 2], [3, 4]])


class GeoJSONTest(TestCase):
    def test_point(self):
        location = baker.make(Location, geom="POINT(4 2)")
        geojson = teramap.GeoJSON([location])

        data = geojson.to_geojson()
        self.assertEqual(
            data,
            {
                "type": "FeatureCollection",
                "features": [
                    {
                        "type": "Feature",
                        "geometry": {"type": "Point", "coordinates": [4.0, 2.0]},
                        "properties": {"id": location.id},
                    }
                ],
            },
        )

    def test_multipoint(self):
        location = baker.make(Location, geom="MULTIPOINT(4 2, 5 2)")
        geojson = teramap.GeoJSON([location])

        data = geojson.to_geojson()
        self.assertEqual(
            data,
            {
                "type": "FeatureCollection",
                "features": [
                    {
                        "type": "Feature",
                        "geometry": {"type": "MultiPoint", "coordinates": [[4.0, 2.0], [5.0, 2.0]]},
                        "properties": {"id": location.id},
                    }
                ],
            },
        )


class GeoJSONPropertiesTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        baker.make(Location, name="puntje", geom="POINT(4 2)", description="foo")
        baker.make(Observation, geom="POINT(4 2)", description="foo", date=datetime.now())

    def test_extra_properties(self):
        class GeoJSON(teramap.GeoJSON):
            properties = {"name": "name"}

        geojson = GeoJSON(Location.objects.all(), extra_properties={"description": "description"})

        data = geojson.to_geojson()

        self.assertEqual(data["features"][0]["properties"], {"name": "puntje", "description": "foo"})

    def test_callable_properties(self):
        class GeoJSON(teramap.GeoJSON):
            properties = {"name": lambda o: o.name.upper()}

        geojson = GeoJSON(Location.objects.all())
        data = geojson.to_geojson()

        self.assertEqual(data["features"][0]["properties"], {"name": "PUNTJE"})

    def test_properties_xss_value(self):
        class GeoJSON(teramap.GeoJSON):
            properties = {"content": "name"}

        location = Location.objects.create(name="<img src onerror=alert(origin)>", geom="POINT(4 52)")
        data = GeoJSON([location]).to_geojson()
        self.assertEqual(data["features"][0]["properties"], {"content": "&lt;img src onerror=alert(origin)&gt;"})

    def test_properties_xss_key(self):
        class GeoJSON(teramap.GeoJSON):
            properties = {"<img src onerror=alert(origin)>": "name"}

        location = Location.objects.create(name="test", geom="POINT(4 52)")
        data = GeoJSON([location]).to_geojson()
        self.assertEqual(data["features"][0]["properties"], {"&lt;img src onerror=alert(origin)&gt;": "test"})

    def test_properties_value_safe_html(self):
        class GeoJSON(teramap.GeoJSON):
            properties = {"content": lambda x: format_html('<a href="{}">{}</a>', x.name, x.name)}

        location = Location.objects.create(name="https://example.com", geom="POINT(4 52)")
        data = GeoJSON([location]).to_geojson()
        expected = {"content": '<a href="https://example.com">https://example.com</a>'}
        self.assertEqual(data["features"][0]["properties"], expected)

    def test_boolean_properties(self):
        """If the lookup of a property is a boolean, just use that boolean."""

        class GeoJSON(teramap.GeoJSON):
            properties = {"name": "name", "no_popup": True, "tooltip": False}

        geojson = GeoJSON(Location.objects.all())
        data = geojson.to_geojson()

        self.assertEqual(
            set(data["features"][0]["properties"].items()),
            set((("name", "puntje"), ("no_popup", True), ("tooltip", False))),
        )
        self.assertNotIn("properties", data)

    def test_collection_properties(self):
        expected = {"legend": [{"color": "red", "legend": "active"}, {"color": "blue", "legend": "not active"}]}

        class GeoJSON(teramap.GeoJSON):
            properties = {"name": "name", "no_popup": True, "tooltip": False}

            collection_properties = expected

        geojson = GeoJSON(Location.objects.all())
        data = geojson.to_geojson()

        self.assertEqual(data["properties"], expected)

    def test_collection_properties_under_addition(self):
        class A(teramap.GeoJSON):
            collection_properties = {"a": "a"}

        class B(teramap.GeoJSON):
            collection_properties = {"b": "b"}

        geojson = A(Location.objects.all()) + B(Location.objects.all())
        data = geojson.to_geojson()

        self.assertEqual(data["properties"], {"a": "a", "b": "b"})

        # order should not matter
        B(Location.objects.all()) + A(Location.objects.all())
        other_data = geojson.to_geojson()
        self.assertEqual(data["properties"], other_data["properties"])

    def test_collection_properties_under_addition_key_collision(self):
        class A(teramap.GeoJSON):
            collection_properties = {"key": "a"}

        class B(teramap.GeoJSON):
            collection_properties = {"key": "b"}

        geojson = A(Location.objects.all()) + B(Location.objects.all())
        data = geojson.to_geojson()
        self.assertEqual(data["properties"], {"key": "b"})

        geojson = B(Location.objects.all()) + A(Location.objects.all())
        data = geojson.to_geojson()
        self.assertEqual(data["properties"], {"key": "a"})

    def test_collection_properties_under_addition_Feature(self):
        class A(teramap.GeoJSON):
            collection_properties = {"a": "a"}

        geojson = A(Location.objects.all()) + Point([1, 2])
        data = geojson.to_geojson()

        self.assertEqual(data["properties"], {"a": "a"})

    def test_empty_collection_properties_no_properties_key(self):
        geojson = Point([1, 3]) + Point([1, 2])
        data = geojson.to_geojson()

        self.assertNotIn("properties", data)

    def test_conversion_to_string(self):
        class GeoJSON(teramap.GeoJSON):
            properties = {"name": "name"}

        class ObservationGeoJSON(teramap.GeoJSON):
            properties = {"name": "date"}

        geojson = ObservationGeoJSON(Observation.objects.all()) + GeoJSON(Location.objects.all())

        self.assertEqual(len(geojson), 2)
        data = geojson.to_geojson_str()

        self.assertEqual(data, str(geojson))
        self.assertIn("puntje", data)

    def test_geom_tansform_centroid(self):
        Location.objects.create(name="polygon 1", geom="POLYGON ((-5 50, 10 50, 10 55, -5 55, -5 50))")
        Location.objects.create(name="polygon 2", geom="POLYGON ((-15 60, 10 60, 10 70, -15 70, -15 60))")
        Location.objects.create(name="line 2", geom="LINESTRING (14 60, -6 56, -14 47, -1 38)")

        class GeoJSON(TransformToPoint, teramap.GeoJSON):
            properties = {"name": "name"}

        geojson = GeoJSON(Location.objects.all()).to_geojson()
        for feature in geojson["features"]:
            self.assertEqual(feature["geometry"]["type"], "Point")

    def test_properties_property_decorator(self):
        """
        Passing extra_properties should also be possible with a class with a method named
        properties decorated with @property.
        """

        class GeoJSON(teramap.GeoJSON):
            @property
            def properties(self):
                return {"foo": False}

        geojson = GeoJSON(Location.objects.all(), extra_properties={"bar": True}).to_geojson()

        for feature in geojson["features"]:
            self.assertEqual(feature["properties"], {"foo": False, "bar": True})

    def test_geom_field_constructor_argument(self):
        baker.make(DualGeomModel, geom="POINT(0 0)", track_geom="LINESTRING (1 0, 1 6, 4 4, 9 8)", _quantity=4)

        class GeoJSON(teramap.GeoJSON):
            pass

        geojson = GeoJSON(DualGeomModel.objects.all()).to_geojson()
        self.assertEqual(geojson["features"][0]["geometry"]["type"], "Point")

        geojson = GeoJSON(DualGeomModel.objects.all(), geom_field="track_geom").to_geojson()
        self.assertEqual(geojson["features"][0]["geometry"]["type"], "LineString")

    def test_lazy_properties(self):
        """Lazy values should not make the conversion to json fail."""

        class GeoJSON(teramap.GeoJSON):
            collection_properties = {"legend-title": _("Yes"), "legend": [{"color": "red", "label": _("No")}]}

        with override("en"):
            data = json.loads(GeoJSON(Location.objects.all()).to_geojson_str())
            self.assertEqual(data["properties"]["legend-title"], "Yes")
            self.assertEqual(data["properties"]["legend"][0]["label"], "No")

        with override("nl"):
            data = json.loads(GeoJSON(Location.objects.all()).to_geojson_str())
            self.assertEqual(data["properties"]["legend-title"], "Ja")
            self.assertEqual(data["properties"]["legend"][0]["label"], "Nee")

    def test_properties_dict_is_copied(self):
        class A(GeoJSON):
            properties = {"id": "id"}

        class B(A):
            @property
            def properties(self):
                return {"name": "name"}

        a1 = A([])
        a2 = A([])
        self.assertNotEqual(id(a1.properties), id(a2.properties))
