require "rails_helper"

describe GeojsonFormatValidator do
  before do
    dummy_model = Class.new do
      include ActiveModel::Model

      attr_accessor :geojson
      validates :geojson, geojson_format: true
    end

    stub_const("DummyModel", dummy_model)
  end

  let(:record) { DummyModel.new }

  it "is not valid with an empty hash" do
    record.geojson = "{}"

    expect(record).not_to be_valid
  end

  it "is not valid with arbitrary keys" do
    record.geojson = '{ "invalid": "yes" }'

    expect(record).not_to be_valid
  end

  it "is not valid without a type" do
    record.geojson = '{ "coordinates": [1.23, 4.56] }'

    expect(record).not_to be_valid
  end

  it "is not valid without a type but a geometry" do
    record.geojson = '{ "geometry": { "type": "Point", "coordinates": [1.23, 4.56] } }'

    expect(record).not_to be_valid
  end

  context "Point geometry" do
    it "is not valid without coordinates" do
      record.geojson = '{ "type": "Point" }'

      expect(record).not_to be_valid
    end

    it "is not valid with only one the longitude" do
      record.geojson = '{ "type": "Point", "coordinates": 1.23 }'

      expect(record).not_to be_valid
    end

    it "is not valid with non-numerical coordinates" do
      record.geojson = '{ "type": "Point", "coordinates": ["1.23", "4.56"] }'

      expect(record).not_to be_valid
    end

    it "is not valid with 3-dimensional coordinates" do
      record.geojson = '{ "type": "Point", "coordinates": [1.23, 4.56, 7.89] }'

      expect(record).not_to be_valid
    end

    it "is not valid with multiple coordinates" do
      record.geojson = '{ "type": "Point", "coordinates": [[1.23, 4.56], [7.89, 10.11]] }'

      expect(record).not_to be_valid
    end

    it "is not valid with a longitude above 180" do
      record.geojson = '{ "type": "Point", "coordinates": [180.01, 4.56] }'

      expect(record).not_to be_valid
    end

    it "is not valid with a longitude below -180" do
      record.geojson = '{ "type": "Point", "coordinates": [-180.01, 4.56] }'

      expect(record).not_to be_valid
    end

    it "is not valid with a latitude above 90" do
      record.geojson = '{ "type": "Point", "coordinates": [1.23, 90.01] }'

      expect(record).not_to be_valid
    end

    it "is not valid with a latitude below -90" do
      record.geojson = '{ "type": "Point", "coordinates": [1.23, -90.01] }'

      expect(record).not_to be_valid
    end

    it "is valid with coordinates in the valid range" do
      record.geojson = '{ "type": "Point", "coordinates": [1.23, 4.56] }'

      expect(record).to be_valid
    end

    it "is valid with coordinates at the positive end of the range" do
      record.geojson = '{ "type": "Point", "coordinates": [180.0, 90.0] }'

      expect(record).to be_valid
    end

    it "is valid with coordinates at the negative end of the range" do
      record.geojson = '{ "type": "Point", "coordinates": [-180.0, -90.0] }'

      expect(record).to be_valid
    end
  end

  context "LineString or MultiPoint geometry" do
    it "is not valid with a one-dimensional array of coordinates" do
      record.geojson = '{ "type": "LineString", "coordinates": [1.23, 4.56] }'

      expect(record).not_to be_valid

      record.geojson = '{ "type": "MultiPoint", "coordinates": [1.23, 4.56] }'

      expect(record).not_to be_valid
    end

    it "is not valid when some coordinates are invalid" do
      record.geojson = '{ "type": "LineString", "coordinates": [[1.23, 4.56], [180.01, 4.56]] }'

      expect(record).not_to be_valid

      record.geojson = '{ "type": "MultiPoint", "coordinates": [[1.23, 4.56], [180.01, 4.56]] }'

      expect(record).not_to be_valid
    end

    it "is valid when all the coordinates are valid" do
      record.geojson = '{ "type": "LineString", "coordinates": [[1.23, 4.56], [7.89, 4.56]] }'

      expect(record).to be_valid

      record.geojson = '{ "type": "MultiPoint", "coordinates": [[1.23, 4.56], [7.89, 4.56]] }'

      expect(record).to be_valid
    end
  end

  context "LineString geometry" do
    it "is not valid with only one point" do
      record.geojson = '{ "type": "LineString", "coordinates": [[1.23, 4.56]] }'

      expect(record).not_to be_valid
    end
  end

  context "MultiPoint geometry" do
    it "is valid with only one point" do
      record.geojson = '{ "type": "MultiPoint", "coordinates": [[1.23, 4.56]] }'

      expect(record).to be_valid
    end
  end

  context "Polygon or MultiLineString geometry" do
    it "is not valid with a one-dimensional array of coordinates" do
      record.geojson = '{ "type": "MultiLineString", "coordinates": [1.23, 4.56] }'

      expect(record).not_to be_valid

      record.geojson = '{ "type": "Polygon", "coordinates": [1.23, 4.56] }'

      expect(record).not_to be_valid
    end

    it "is not valid with a two-dimensional array of coordinates" do
      record.geojson = '{ "type": "MultiLineString", "coordinates": [[1.23, 4.56], [7.89, 4.56]] }'

      expect(record).not_to be_valid

      record.geojson = '{ "type": "Polygon", "coordinates": [[1.23, 4.56], [7.89, 4.56]] }'

      expect(record).not_to be_valid
    end
  end

  context "MultiLineString geometry" do
    it "is valid with just one line" do
      record.geojson = '{ "type": "MultiLineString", "coordinates": [[[1.23, 4.56], [7.89, 4.56]]] }'

      expect(record).to be_valid
    end

    it "is valid with multiple valid lines" do
      record.geojson = <<~JSON
        {
          "type": "MultiLineString",
          "coordinates": [
            [[1.23, 4.56], [7.89, 4.56]],
            [[10.11, 12.13], [14.15, 16.17]]
          ]
        }
      JSON

      expect(record).to be_valid
    end

    it "is not valid if some lines are invalid" do
      record.geojson = <<~JSON
        {
          "type": "MultiLineString",
          "coordinates": [
            [[1.23, 4.56], [7.89, 4.56]],
            [[10.11, 12.13]]
          ]
        }
      JSON

      expect(record).not_to be_valid
    end
  end

  context "Polygon geometry" do
    it "is not valid with a ring having less than four elements" do
      record.geojson = <<~JSON
        {
          "type": "Polygon",
          "coordinates": [[
            [1.23, 4.56],
            [7.89, 10.11],
            [1.23, 4.56]
          ]]
        }
      JSON

      expect(record).not_to be_valid
    end

    it "is not valid with a ring which with different starting and end points" do
      record.geojson = <<~JSON
        {
          "type": "Polygon",
          "coordinates": [[
            [1.23, 4.56],
            [7.89, 10.11],
            [12.13, 14.15],
            [16.17, 18.19]
          ]]
        }
      JSON

      expect(record).not_to be_valid
    end

    it "is valid with one valid ring" do
      record.geojson = <<~JSON
        {
          "type": "Polygon",
          "coordinates": [[
            [1.23, 4.56],
            [7.89, 10.11],
            [12.13, 14.15],
            [1.23, 4.56]
          ]]
        }
      JSON

      expect(record).to be_valid
    end

    it "is valid with multiple valid rings" do
      record.geojson = <<~JSON
        {
          "type": "Polygon",
          "coordinates": [
            [
              [100.0, 0.0],
              [101.0, 0.0],
              [101.0, 1.0],
              [100.0, 1.0],
              [100.0, 0.0]
            ],
            [
              [100.8, 0.8],
              [100.8, 0.2],
              [100.2, 0.2],
              [100.2, 0.8],
              [100.8, 0.8]
            ]
          ]
        }
      JSON

      expect(record).to be_valid
    end

    it "is not valid with multiple rings if some rings are invalid" do
      record.geojson = <<~JSON
        {
          "type": "Polygon",
          "coordinates": [
            [
              [100.0, 0.0],
              [101.0, 0.0],
              [101.0, 1.0],
              [100.0, 1.0],
              [100.0, 0.0]
            ],
            [
              [100.8, 0.8],
              [100.8, 0.2],
              [100.2, 0.2]
            ]
          ]
        }
      JSON

      expect(record).not_to be_valid
    end
  end

  context "MultiPolygon geometry" do
    it "is not valid with a one-dimensional array of coordinates" do
      record.geojson = '{ "type": "MultiPolygon", "coordinates": [1.23, 4.56] }'

      expect(record).not_to be_valid
    end

    it "is not valid with a two-dimensional array of coordinates" do
      record.geojson = '{ "type": "MultiPolygon", "coordinates": [[1.23, 4.56], [7.89, 4.56]] }'

      expect(record).not_to be_valid
    end

    it "is not valid with a three-dimensional polygon coordinates array" do
      record.geojson = <<~JSON
        {
          "type": "MultiPolygon",
          "coordinates": [[
            [1.23, 4.56],
            [7.89, 10.11],
            [12.13, 14.15],
            [1.23, 4.56]
          ]]
        }
      JSON

      expect(record).not_to be_valid
    end

    it "is valid with a valid polygon" do
      record.geojson = <<~JSON
        {
          "type": "MultiPolygon",
          "coordinates": [[[
            [1.23, 4.56],
            [7.89, 10.11],
            [12.13, 14.15],
            [1.23, 4.56]
          ]]]
        }
      JSON

      expect(record).to be_valid
    end

    it "is valid with multiple valid polygons" do
      record.geojson = <<~JSON
        {
          "type": "MultiPolygon",
          "coordinates": [
            [
              [
                [1.23, 4.56],
                [7.89, 10.11],
                [12.13, 14.15],
                [1.23, 4.56]
              ]
            ],
            [
              [
                [100.0, 0.0],
                [101.0, 0.0],
                [101.0, 1.0],
                [100.0, 1.0],
                [100.0, 0.0]
              ],
              [
                [100.8, 0.8],
                [100.8, 0.2],
                [100.2, 0.2],
                [100.2, 0.8],
                [100.8, 0.8]
              ]
            ]
          ]
        }
      JSON

      expect(record).to be_valid
    end

    it "is not valid with multiple polygons if some polygons are invalid" do
      record.geojson = <<~JSON
        {
          "type": "MultiPolygon",
          "coordinates": [
            [
              [
                [1.23, 4.56],
                [7.89, 10.11],
                [12.13, 14.15],
                [1.23, 4.56]
              ]
            ],
            [
              [
                [100.0, 0.0],
                [101.0, 0.0],
                [101.0, 1.0],
                [100.0, 1.0],
                [100.0, 0.0]
              ],
              [
                [100.8, 0.8],
                [100.8, 0.2],
                [100.2, 0.2]
              ]
            ]
          ]
        }
      JSON

      expect(record).not_to be_valid
    end
  end

  context "GeometryCollection" do
    it "is not valid if it doesn't contain geometries" do
      record.geojson = '{ "type": "GeometryCollection" }'

      expect(record).not_to be_valid
    end

    it "is not valid if geometries is not an array" do
      record.geojson = <<~JSON
        {
          "type": "GeometryCollection",
          "geometries": { "type": "Point", "coordinates": [1.23, 4.56] }
        }
      JSON

      expect(record).not_to be_valid
    end

    it "is valid if the array of geometries is empty" do
      record.geojson = '{ "type": "GeometryCollection", "geometries": [] }'

      expect(record).to be_valid
    end

    it "is valid if all geometries are valid" do
      record.geojson = <<~JSON
        {
          "type": "GeometryCollection",
          "geometries": [
            {
              "type": "Point",
              "coordinates": [100.0, 0.0]
            },
            {
              "type": "LineString",
              "coordinates": [
                [101.0, 0.0],
                [102.0, 1.0]
              ]
            }
          ]
        }
      JSON

      expect(record).to be_valid
    end

    it "is not valid if some geometries are invalid" do
      record.geojson = <<~JSON
        {
          "type": "GeometryCollection",
          "geometries": [
            {
              "type": "Point",
              "coordinates": [100.0, 0.0]
            },
            {
              "type": "LineString",
              "coordinates": [101.0, 0.0]
            }
          ]
        }
      JSON

      expect(record).not_to be_valid
    end
  end

  context "Feature" do
    it "is valid with a valid geometry" do
      record.geojson = <<~JSON
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [1.23, 4.56]
          }
        }
      JSON

      expect(record).to be_valid
    end

    it "is not valid with a valid geometry" do
      record.geojson = <<~JSON
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [1.23]
          }
        }
      JSON

      expect(record).not_to be_valid
    end
  end

  context "FeatureCollection" do
    it "is not valid without features" do
      record.geojson = '{ "type": "FeatureCollection" }'
    end

    it "is not valid if features is not an array" do
      record.geojson = <<~JSON
        {
          "type": "FeatureCollection",
          "features": {
            "type": "Feature",
            "geometry": {
              "type": "Point",
              "coordinates": [1.23, 4.56]
            }
          }
        }
      JSON
    end

    it "is valid if the array of features is empty" do
      record.geojson = '{ "type": "FeatureCollection", "features": [] }'

      expect(record).to be_valid
    end

    it "is valid if all features are valid" do
      record.geojson = <<~JSON
        {
          "type": "FeatureCollection",
          "features": [
            {
              "type": "Feature",
              "geometry": {
                "type": "Point",
                "coordinates": [1.23, 4.56]
              }
            },
            {
              "type": "Feature",
              "geometry": {
                "type": "LineString",
                "coordinates": [[101.0, 0.0], [102.0, 1.0]]
              }
            }
          ]
        }
      JSON

      expect(record).to be_valid
    end

    it "is not valid if some features are invalid" do
      record.geojson = <<~JSON
        {
          "type": "FeatureCollection",
          "features": [
            {
              "type": "Feature",
              "geometry": {
                "type": "Point",
                "coordinates": [1.23, 4.56]
              }
            },
            {
              "type": "LineString",
              "coordinates": [[101.0, 0.0], [102.0, 1.0]]
            }
          ]
        }
      JSON

      expect(record).not_to be_valid
    end
  end
end
