import Circle from '../../../../../src/ol/geom/Circle.js';
import CircleStyle from '../../../../../src/ol/style/Circle.js';
import Collection from '../../../../../src/ol/Collection.js';
import Event from '../../../../../src/ol/events/Event.js';
import Feature from '../../../../../src/ol/Feature.js';
import GeometryCollection from '../../../../../src/ol/geom/GeometryCollection.js';
import LineString from '../../../../../src/ol/geom/LineString.js';
import Map from '../../../../../src/ol/Map.js';
import MapBrowserEvent from '../../../../../src/ol/MapBrowserEvent.js';
import Modify, {ModifyEvent} from '../../../../../src/ol/interaction/Modify.js';
import Point from '../../../../../src/ol/geom/Point.js';
import Polygon, {fromExtent} from '../../../../../src/ol/geom/Polygon.js';
import Snap from '../../../../../src/ol/interaction/Snap.js';
import VectorLayer from '../../../../../src/ol/layer/Vector.js';
import VectorSource from '../../../../../src/ol/source/Vector.js';
import View from '../../../../../src/ol/View.js';
import {Fill, Style} from '../../../../../src/ol/style.js';
import {MultiPoint} from '../../../../../src/ol/geom.js';
import {
  clearUserProjection,
  setUserProjection,
} from '../../../../../src/ol/proj.js';
import {
  click,
  doubleClick,
  never,
} from '../../../../../src/ol/events/condition.js';
import {getValues} from '../../../../../src/ol/obj.js';

describe('ol.interaction.Modify', function () {
  let target, map, layer, source, features;

  const width = 360;
  const height = 180;

  beforeEach(function (done) {
    target = document.createElement('div');

    const style = target.style;
    style.position = 'absolute';
    style.left = '-1000px';
    style.top = '-1000px';
    style.width = width + 'px';
    style.height = height + 'px';
    document.body.appendChild(target);

    features = [
      new Feature({
        geometry: new Polygon([
          [
            [0, 0],
            [10, 20],
            [0, 40],
            [40, 40],
            [40, 0],
          ],
        ]),
      }),
    ];

    source = new VectorSource({
      features: features,
    });

    layer = new VectorLayer({source: source});

    map = new Map({
      target: target,
      layers: [layer],
      view: new View({
        projection: 'EPSG:4326',
        center: [0, 0],
        resolution: 1,
      }),
    });

    map.once('postrender', function () {
      done();
    });
  });

  afterEach(function () {
    map.dispose();
    document.body.removeChild(target);
    clearUserProjection();
  });

  /**
   * Simulates a browser event on the map viewport.  The client x/y location
   * will be adjusted as if the map were centered at 0,0.
   * @param {string} type Event type.
   * @param {number} x Horizontal offset from map center.
   * @param {number} y Vertical offset from map center.
   * @param {Object} modifiers Lookup of modifier keys.
   * @param {number} button The mouse button.
   */
  function simulateEvent(type, x, y, modifiers, button) {
    modifiers = modifiers || {};
    const viewport = map.getViewport();
    // calculated in case body has top < 0 (test runner with small window)
    const position = viewport.getBoundingClientRect();
    const pointerEvent = new Event();
    pointerEvent.type = type;
    pointerEvent.target = viewport.firstChild;
    pointerEvent.clientX = position.left + x + width / 2;
    pointerEvent.clientY = position.top + y + height / 2;
    pointerEvent.shiftKey = modifiers.shift || false;
    pointerEvent.altKey = modifiers.alt || false;
    pointerEvent.pointerId = 1;
    pointerEvent.preventDefault = function () {};
    pointerEvent.button = button;
    pointerEvent.isPrimary = true;
    const event = new MapBrowserEvent(type, map, pointerEvent);
    map.handleMapBrowserEvent(event);
  }

  /**
   * Tracks events triggered by the interaction as well as feature
   * modifications. Helper function to
   * @param {ol.Feature} feature Modified feature.
   * @param {ol.interaction.Modify} interaction The interaction.
   * @return {Array<ModifyEvent|string>} events
   */
  function trackEvents(feature, interaction) {
    const events = [];
    feature.on('change', function (event) {
      events.push('change');
    });
    interaction.on('modifystart', function (event) {
      events.push(event);
    });
    interaction.on('modifyend', function (event) {
      events.push(event);
    });
    return events;
  }

  /**
   * Validates the event array to verify proper event sequence. Checks
   * that first and last event are correct ModifyEvents and that feature
   * modifications event are in between.
   * @param {Array<ModifyEvent|string>} events The events.
   * @param {Array<ol.Feature>} features The features.
   */
  function validateEvents(events, features) {
    const startevent = events[0];
    const endevent = events[events.length - 1];

    // first event should be modifystart
    expect(startevent).to.be.a(ModifyEvent);
    expect(startevent.type).to.eql('modifystart');

    // last event should be modifyend
    expect(endevent).to.be.a(ModifyEvent);
    expect(endevent.type).to.eql('modifyend');

    // make sure we get change events to events array
    expect(events.length > 2).to.be(true);
    // middle events should be feature modification events
    for (let i = 1; i < events.length - 1; i++) {
      expect(events[i]).to.equal('change');
    }

    // ModifyEvents should include the expected features
    expect(startevent.features.getArray()).to.eql(features);
    expect(endevent.features.getArray()).to.eql(features);
  }

  describe('constructor', function () {
    it('adds features to the RTree', function () {
      const feature = new Feature(new Point([0, 0]));
      const features = new Collection([feature]);
      const modify = new Modify({
        features: features,
      });
      const rbushEntries = modify.rBush_.getAll();
      expect(rbushEntries.length).to.be(1);
      expect(rbushEntries[0].feature).to.be(feature);
    });

    it('accepts feature without geometry', function () {
      const feature = new Feature();
      const features = new Collection([feature]);
      const modify = new Modify({
        features: features,
      });
      let rbushEntries = modify.rBush_.getAll();
      expect(rbushEntries.length).to.be(0);

      feature.setGeometry(new Point([0, 10]));
      rbushEntries = modify.rBush_.getAll();
      expect(rbushEntries.length).to.be(1);
      expect(rbushEntries[0].feature).to.be(feature);
    });

    it('accepts a source', function () {
      const feature = new Feature(new Point([0, 0]));
      const source = new VectorSource({features: [feature]});
      const modify = new Modify({source: source});
      const rbushEntries = modify.rBush_.getAll();
      expect(rbushEntries.length).to.be(1);
      expect(rbushEntries[0].feature).to.be(feature);
    });

    it('accepts a hitDetection option', function () {
      const feature = new Feature(new Point([0, 0]));
      const source = new VectorSource({features: [feature]});
      const layer = new VectorLayer({source: source});
      const modify = new Modify({hitDetection: layer, source: source});
      const rbushEntries = modify.rBush_.getAll();
      expect(rbushEntries.length).to.be(1);
      expect(rbushEntries[0].feature).to.be(feature);
      expect(modify.hitDetection_).to.be(layer);
    });

    it('accepts a snapToPointer option', function () {
      const modify = new Modify({source: source, snapToPointer: true});
      expect(modify.snapToPointer_).to.be(true);
    });
  });

  describe('vertex deletion', function () {
    it('works when clicking on a shared vertex', function () {
      features.push(features[0].clone());

      const first = features[0];
      const firstRevision = first.getGeometry().getRevision();
      const second = features[1];
      const secondRevision = second.getGeometry().getRevision();

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      const events = trackEvents(first, modify);

      expect(first.getGeometry().getRevision()).to.equal(firstRevision);
      expect(first.getGeometry().getCoordinates()[0]).to.have.length(5);
      expect(second.getGeometry().getRevision()).to.equal(secondRevision);
      expect(second.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointerdown', 10, -20, {alt: true}, 0);
      simulateEvent('pointerup', 10, -20, {alt: true}, 0);
      simulateEvent('click', 10, -20, {alt: true}, 0);
      simulateEvent('singleclick', 10, -20, {alt: true}, 0);

      expect(first.getGeometry().getRevision()).to.equal(firstRevision + 1);
      expect(first.getGeometry().getCoordinates()[0]).to.have.length(4);
      expect(second.getGeometry().getRevision()).to.equal(secondRevision + 1);
      expect(second.getGeometry().getCoordinates()[0]).to.have.length(4);

      validateEvents(events, features);
    });

    it('deletes first vertex of a LineString', function () {
      const lineFeature = new Feature({
        geometry: new LineString([
          [0, 0],
          [10, 20],
          [0, 40],
          [40, 40],
          [40, 0],
        ]),
      });
      features.length = 0;
      features.push(lineFeature);
      features.push(lineFeature.clone());

      const first = features[0];
      const firstRevision = first.getGeometry().getRevision();

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      const events = trackEvents(first, modify);

      expect(first.getGeometry().getRevision()).to.equal(firstRevision);
      expect(first.getGeometry().getCoordinates()).to.have.length(5);

      simulateEvent('pointerdown', 0, 0, {alt: true}, 0);
      simulateEvent('pointerup', 0, 0, {alt: true}, 0);
      simulateEvent('click', 0, 0, {alt: true}, 0);
      simulateEvent('singleclick', 0, 0, {alt: true}, 0);

      expect(first.getGeometry().getRevision()).to.equal(firstRevision + 1);
      expect(first.getGeometry().getCoordinates()).to.have.length(4);
      expect(first.getGeometry().getCoordinates()[0][0]).to.equal(10);
      expect(first.getGeometry().getCoordinates()[0][1]).to.equal(20);

      validateEvents(events, features);
    });

    it('deletes last vertex of a LineString', function () {
      const lineFeature = new Feature({
        geometry: new LineString([
          [0, 0],
          [10, 20],
          [0, 40],
          [40, 40],
          [40, 0],
        ]),
      });
      features.length = 0;
      features.push(lineFeature);
      features.push(lineFeature.clone());

      const first = features[0];
      const firstRevision = first.getGeometry().getRevision();

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      const events = trackEvents(first, modify);

      expect(first.getGeometry().getRevision()).to.equal(firstRevision);
      expect(first.getGeometry().getCoordinates()).to.have.length(5);

      simulateEvent('pointerdown', 40, 0, {alt: true}, 0);
      simulateEvent('pointerup', 40, 0, {alt: true}, 0);
      simulateEvent('click', 40, 0, {alt: true}, 0);
      simulateEvent('singleclick', 40, 0, {alt: true}, 0);

      expect(first.getGeometry().getRevision()).to.equal(firstRevision + 1);
      expect(first.getGeometry().getCoordinates()).to.have.length(4);
      expect(first.getGeometry().getCoordinates()[3][0]).to.equal(40);
      expect(first.getGeometry().getCoordinates()[3][1]).to.equal(40);

      validateEvents(events, features);
    });

    it('deletes vertex of a LineString programmatically', function () {
      const lineFeature = new Feature({
        geometry: new LineString([
          [0, 0],
          [10, 20],
          [0, 40],
          [40, 40],
          [40, 0],
        ]),
      });
      features.length = 0;
      features.push(lineFeature);
      features.push(lineFeature.clone());

      const first = features[0];
      const firstRevision = first.getGeometry().getRevision();

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      const events = trackEvents(first, modify);

      expect(first.getGeometry().getRevision()).to.equal(firstRevision);
      expect(first.getGeometry().getCoordinates()).to.have.length(5);

      simulateEvent('pointerdown', 40, 0, null, 0);
      simulateEvent('pointerup', 40, 0, null, 0);

      const removed = modify.removePoint();

      expect(removed).to.be(true);
      expect(first.getGeometry().getRevision()).to.equal(firstRevision + 1);
      expect(first.getGeometry().getCoordinates()).to.have.length(4);
      expect(first.getGeometry().getCoordinates()[3][0]).to.equal(40);
      expect(first.getGeometry().getCoordinates()[3][1]).to.equal(40);

      validateEvents(events, features);
    });
  });

  describe('vertex modification', function () {
    it('keeps the third dimension', function () {
      const lineFeature = new Feature({
        geometry: new LineString([
          [0, 0, 10],
          [10, 20, 20],
          [0, 40, 30],
          [40, 40, 40],
          [40, 0, 50],
        ]),
      });
      features.length = 0;
      features.push(lineFeature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      // Move first vertex
      simulateEvent('pointermove', 0, 0, null, 0);
      simulateEvent('pointerdown', 0, 0, null, 0);
      simulateEvent('pointermove', -10, -10, null, 0);
      simulateEvent('pointerdrag', -10, -10, null, 0);
      simulateEvent('pointerup', -10, -10, null, 0);

      // Move middle vertex
      simulateEvent('pointermove', 0, -40, null, 0);
      simulateEvent('pointerdown', 0, -40, null, 0);
      simulateEvent('pointermove', 10, -30, null, 0);
      simulateEvent('pointerdrag', 10, -30, null, 0);
      simulateEvent('pointerup', 10, -30, null, 0);

      // Move last vertex
      simulateEvent('pointermove', 40, 0, null, 0);
      simulateEvent('pointerdown', 40, 0, null, 0);
      simulateEvent('pointermove', 50, -10, null, 0);
      simulateEvent('pointerdrag', 50, -10, null, 0);
      simulateEvent('pointerup', 50, -10, null, 0);

      expect(lineFeature.getGeometry().getCoordinates()[0][2]).to.equal(10);
      expect(lineFeature.getGeometry().getCoordinates()[2][2]).to.equal(30);
      expect(lineFeature.getGeometry().getCoordinates()[4][2]).to.equal(50);
    });
  });

  describe('vertex insertion', function () {
    it('only inserts one vertex per geometry', function () {
      const lineFeature = new Feature({
        geometry: new LineString([
          [-10, -10],
          [10, 10],
          [-10, -10],
          [10, 10],
        ]),
      });
      features.length = 0;
      features.push(lineFeature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      // Click on line
      simulateEvent('pointermove', 0, 0, null, 0);
      simulateEvent('pointerdown', 0, 0, null, 0);
      simulateEvent('pointerup', 0, 0, null, 0);

      expect(lineFeature.getGeometry().getCoordinates().length).to.equal(5);
    });
    it('inserts one vertex into both linestrings with duplicate segments each', function () {
      const lineFeature1 = new Feature(
        new LineString([
          [-10, -10],
          [10, 10],
          [-10, -10],
        ])
      );
      const lineFeature2 = new Feature(
        new LineString([
          [10, 10],
          [-10, -10],
          [10, 10],
        ])
      );
      features.length = 0;
      features.push(lineFeature1, lineFeature2);

      const modify = new Modify({
        features: new Collection(features),
      });
      let modifiedFeatures;

      const onModifyStart = function (evt) {
        modifiedFeatures = evt.features;
      };
      map.addInteraction(modify);

      modify.on('modifystart', onModifyStart);
      // Click on line
      simulateEvent('pointermove', 0, 0, null, 0);
      simulateEvent('pointerdown', 0, 0, null, 0);
      simulateEvent('pointerup', 0, 0, null, 0);
      modify.un('modifystart', onModifyStart);

      expect(lineFeature1.getGeometry().getCoordinates().length).to.be(4);
      expect(lineFeature2.getGeometry().getCoordinates().length).to.be(4);
      expect(modifiedFeatures.getArray()).to.eql([lineFeature1, lineFeature2]);
    });
  });

  describe('circle modification', function () {
    it('changes the circle radius and center', function () {
      const circleFeature = new Feature(new Circle([10, 10], 20));
      features.length = 0;
      features.push(circleFeature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      // Change center
      simulateEvent('pointermove', 10, -10, null, 0);
      simulateEvent('pointerdown', 10, -10, null, 0);
      simulateEvent('pointermove', 5, -5, null, 0);
      simulateEvent('pointerdrag', 5, -5, null, 0);
      simulateEvent('pointerup', 5, -5, null, 0);

      expect(circleFeature.getGeometry().getRadius()).to.equal(20);
      expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]);

      // Increase radius along x axis
      simulateEvent('pointermove', 25, -4, null, 0);
      simulateEvent('pointerdown', 25, -4, null, 0);
      simulateEvent('pointermove', 30, -5, null, 0);
      simulateEvent('pointerdrag', 30, -5, null, 0);
      simulateEvent('pointerup', 30, -5, null, 0);

      expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(25, 0.1);
      expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]);

      // Increase radius along y axis
      simulateEvent('pointermove', 4, -30, null, 0);
      simulateEvent('pointerdown', 4, -30, null, 0);
      simulateEvent('pointermove', 5, -35, null, 0);
      simulateEvent('pointerdrag', 5, -35, null, 0);
      simulateEvent('pointerup', 5, -35, null, 0);

      expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(30, 0.1);
      expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]);
    });

    it('changes the circle radius and center in a user projection', function () {
      const userProjection = 'EPSG:3857';
      setUserProjection(userProjection);
      const viewProjection = map.getView().getProjection();

      const circleFeature = new Feature(
        new Circle([10, 10], 20).transform(viewProjection, userProjection)
      );
      features.length = 0;
      features.push(circleFeature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      // Change center
      simulateEvent('pointermove', 10, -10, null, 0);
      simulateEvent('pointerdown', 10, -10, null, 0);
      simulateEvent('pointermove', 5, -5, null, 0);
      simulateEvent('pointerdrag', 5, -5, null, 0);
      simulateEvent('pointerup', 5, -5, null, 0);

      const geometry1 = circleFeature
        .getGeometry()
        .clone()
        .transform(userProjection, viewProjection);
      expect(geometry1.getRadius()).to.roughlyEqual(20, 1e-9);
      expect(geometry1.getCenter()).to.eql([5, 5]);

      // Increase radius along x axis
      simulateEvent('pointermove', 25, -4, null, 0);
      simulateEvent('pointerdown', 25, -4, null, 0);
      simulateEvent('pointermove', 30, -5, null, 0);
      simulateEvent('pointerdrag', 30, -5, null, 0);
      simulateEvent('pointerup', 30, -5, null, 0);

      const geometry2 = circleFeature
        .getGeometry()
        .clone()
        .transform(userProjection, viewProjection);
      expect(geometry2.getRadius()).to.roughlyEqual(25, 0.1);
      expect(geometry2.getCenter()).to.eql([5, 5]);

      // Increase radius along y axis
      simulateEvent('pointermove', 4, -30, null, 0);
      simulateEvent('pointerdown', 4, -30, null, 0);
      simulateEvent('pointermove', 5, -35, null, 0);
      simulateEvent('pointerdrag', 5, -35, null, 0);
      simulateEvent('pointerup', 5, -35, null, 0);

      const geometry3 = circleFeature
        .getGeometry()
        .clone()
        .transform(userProjection, viewProjection);
      expect(geometry3.getRadius()).to.roughlyEqual(30, 0.1);
      expect(geometry3.getCenter()).to.eql([5, 5]);
    });
  });

  describe('boundary modification', function () {
    let modify, feature, events;

    beforeEach(function () {
      features.push(new Feature(new Point([12, 34])));
      modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      feature = features[0];

      events = trackEvents(feature, modify);
    });

    it('clicking vertex should delete it and +r1', function () {
      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointerdown', 10, -20, {alt: true}, 0);
      simulateEvent('pointerup', 10, -20, {alt: true}, 0);
      simulateEvent('click', 10, -20, {alt: true}, 0);
      simulateEvent('singleclick', 10, -20, {alt: true}, 0);

      expect(feature.getGeometry().getRevision()).to.equal(2);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(4);

      validateEvents(events, [feature]);
    });

    it('single clicking boundary should add vertex and +r1', function () {
      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointerdown', 40, -20, null, 0);
      simulateEvent('pointerup', 40, -20, null, 0);
      simulateEvent('click', 40, -20, null, 0);
      simulateEvent('singleclick', 40, -20, null, 0);

      expect(feature.getGeometry().getRevision()).to.equal(2);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(6);

      validateEvents(events, [feature]);
    });

    it('single clicking on created vertex should delete it again', function () {
      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointerdown', 40, -20, null, 0);
      simulateEvent('pointerup', 40, -20, null, 0);
      simulateEvent('click', 40, -20, null, 0);
      simulateEvent('singleclick', 40, -20, null, 0);

      expect(feature.getGeometry().getRevision()).to.equal(2);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(6);

      validateEvents(events, [feature]);
      events.length = 0;

      simulateEvent('pointerdown', 40, -20, {alt: true}, 0);
      simulateEvent('pointerup', 40, -20, {alt: true}, 0);
      simulateEvent('click', 40, -20, {alt: true}, 0);
      simulateEvent('singleclick', 40, -20, {alt: true}, 0);

      expect(feature.getGeometry().getRevision()).to.equal(3);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      validateEvents(events, [feature]);
    });

    it('clicking with drag should add vertex and +r3', function () {
      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointermove', 40, -20, null, 0);
      simulateEvent('pointerdown', 40, -20, null, 0);
      simulateEvent('pointermove', 30, -20, null, 0);
      simulateEvent('pointerdrag', 30, -20, null, 0);
      simulateEvent('pointerup', 30, -20, null, 0);

      expect(feature.getGeometry().getRevision()).to.equal(4);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(6);

      validateEvents(events, [feature]);
    });

    it('clicking with right button should not add a vertex', function () {
      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointermove', 40, -20, null, 0);
      // right click
      simulateEvent('pointerdown', 40, -20, null, 1);
      simulateEvent('pointermove', 30, -20, null, 1);
      simulateEvent('pointerdrag', 30, -20, null, 1);
      simulateEvent('pointerup', 30, -20, null, 1);

      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);
      expect(events).to.have.length(0);
    });
  });

  describe('geometry collection modification', function () {
    it('all geometries should be modified', function () {
      const firstPolygon = new Polygon([
        [
          [0, 0],
          [1, 0],
          [1, 1],
          [0, 1],
          [0, 0],
        ],
      ]);
      const secondPolygon = firstPolygon.clone();

      const firstLineString = new LineString([
        [-2, 0],
        [0, 0],
        [2, 0],
      ]);
      const secondLineString = new LineString([
        [0, 2],
        [0, 0],
        [0, -2],
      ]);

      const point = new Point([0, 0]);

      const circle = new Circle([0, 0], 1);

      const geometryCollection = new GeometryCollection([
        firstPolygon,
        secondPolygon,
        firstLineString,
        secondLineString,
        point,
        circle,
      ]);

      const feature = new Feature({
        geometry: geometryCollection,
      });

      features.length = 0;
      features.push(feature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      // Move vertex
      simulateEvent('pointermove', 0, 0, null, 0);
      simulateEvent('pointerdown', 0, 0, null, 0);
      simulateEvent('pointermove', -1, 0, null, 0);
      simulateEvent('pointerdrag', -1, 0, null, 0);
      simulateEvent('pointerup', -1, 0, null, 0);

      let geomCoords;
      geomCoords = firstPolygon.getCoordinates()[0];
      expect(geomCoords[0][0]).to.equal(-1);
      expect(geomCoords[0][1]).to.equal(0);

      geomCoords = secondPolygon.getCoordinates()[0];
      expect(geomCoords[0][0]).to.equal(-1);
      expect(geomCoords[0][1]).to.equal(0);

      geomCoords = firstLineString.getCoordinates();
      expect(geomCoords[1][0]).to.equal(-1);
      expect(geomCoords[1][1]).to.equal(0);

      geomCoords = secondLineString.getCoordinates();
      expect(geomCoords[1][0]).to.equal(-1);
      expect(geomCoords[1][1]).to.equal(0);

      geomCoords = point.getCoordinates();
      expect(geomCoords[0]).to.equal(-1);
      expect(geomCoords[1]).to.equal(0);

      geomCoords = circle.getCenter();
      expect(geomCoords[0]).to.equal(-1);
      expect(geomCoords[1]).to.equal(0);
    });
  });

  describe('double click deleteCondition', function () {
    let modify, feature, events;

    beforeEach(function () {
      modify = new Modify({
        features: new Collection(features),
        deleteCondition: doubleClick,
      });
      map.addInteraction(modify);

      feature = features[0];

      events = trackEvents(feature, modify);
    });

    it('should delete vertex on double click', function () {
      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointerdown', 10, -20, null, 0);
      simulateEvent('pointerup', 10, -20, null, 0);
      simulateEvent('click', 10, -20, null, 0);
      simulateEvent('pointerdown', 10, -20, null, 0);
      simulateEvent('pointerup', 10, -20, null, 0);
      simulateEvent('click', 10, -20, null, 0);
      simulateEvent('dblclick', 10, -20, null, 0);

      expect(feature.getGeometry().getRevision()).to.equal(2);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(4);

      validateEvents(events, features);
    });

    it('should do nothing on single click', function () {
      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      simulateEvent('pointerdown', 10, -20, null, 0);
      simulateEvent('pointerup', 10, -20, null, 0);
      simulateEvent('click', 10, -20, null, 0);
      simulateEvent('singleclick', 10, -20, null, 0);

      expect(feature.getGeometry().getRevision()).to.equal(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      expect(events.length).to.eql(0);
    });
  });

  describe('insertVertexCondition', function () {
    it('calls the callback function', function () {
      const listenerSpy = sinon.spy(function (event) {
        return false;
      });

      const modify = new Modify({
        features: new Collection(features),
        insertVertexCondition: listenerSpy,
      });
      map.addInteraction(modify);
      const feature = features[0];

      // move first vertex
      simulateEvent('pointermove', 0, 0, null, 0);
      simulateEvent('pointerdown', 0, 0, null, 0);
      simulateEvent('pointermove', -10, -10, null, 0);
      simulateEvent('pointerdrag', -10, -10, null, 0);
      simulateEvent('pointerup', -10, -10, null, 0);

      expect(listenerSpy.callCount).to.be(0);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);

      // try to add vertex
      simulateEvent('pointerdown', 40, -20, null, 0);
      simulateEvent('pointerup', 40, -20, null, 0);
      simulateEvent('click', 40, -20, null, 0);
      simulateEvent('singleclick', 40, -20, null, 0);

      expect(listenerSpy.callCount).to.be(1);
      expect(feature.getGeometry().getCoordinates()[0]).to.have.length(5);
    });

    it('does not fire `modifystart` when nothing is modified', function (done) {
      const modify = new Modify({
        features: new Collection(features),
        insertVertexCondition: never,
      });
      map.addInteraction(modify);

      let modifystart = false;
      modify.on('modifystart', function () {
        modifystart = true;
      });

      // try to add vertex
      simulateEvent('pointermove', 40, -20, null, 0);
      simulateEvent('pointerdown', 40, -20, null, 0);
      simulateEvent('pointermove', 42, -30, null, 0);
      simulateEvent('pointerdrag', 42, -30, null, 0);
      simulateEvent('pointerup', 42, -30, null, 0);
      simulateEvent('click', 42, -30, null, 0);
      simulateEvent('singleclick', 42, -30, null, 0);

      setTimeout(function () {
        expect(modifystart).to.be(false);
        done();
      }, 0);
    });

    it('does not fire `modifyend` when nothing is modified', function (done) {
      const modify = new Modify({
        features: new Collection(features),
        deleteCondition: click,
        insertVertexCondition: never,
      });
      map.addInteraction(modify);

      let modifyend = false;
      modify.on('modifyend', function (e) {
        modifyend = true;
      });

      // try to add vertex
      simulateEvent('pointermove', 40, -20, null, 0);
      simulateEvent('pointerdown', 40, -20, null, 0);
      simulateEvent('pointerdrag', 42, -30, null, 0);
      simulateEvent('pointerup', 42, -30, null, 0);
      simulateEvent('click', 42, -30, null, 0);
      simulateEvent('singleclick', 42, -30, null, 0);

      setTimeout(function () {
        expect(modifyend).to.be(false);
        done();
      }, 0);
    });
  });

  describe('handle feature change', function () {
    let getModifyListeners;

    beforeEach(function () {
      getModifyListeners = function (feature, modify) {
        const listeners = feature.listeners_['change'];
        const candidates = getValues(modify);
        return listeners.filter(function (listener) {
          return candidates.indexOf(listener) !== -1;
        });
      };
    });

    it('updates circle segment data', function () {
      const feature = new Feature(new Circle([10, 10], 20));
      features.length = 0;
      features.push(feature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      let listeners;

      listeners = getModifyListeners(feature, modify);
      expect(listeners).to.have.length(1);

      let firstSegmentData;

      firstSegmentData = modify.rBush_.forEachInExtent(
        [0, 0, 5, 5],
        function (node) {
          return node;
        }
      );
      expect(firstSegmentData.segment[0]).to.eql([10, 10]);
      expect(firstSegmentData.segment[1]).to.eql([10, 10]);

      const center = feature.getGeometry().getCenter();
      center[0] = 1;
      center[1] = 1;
      feature.getGeometry().setCenter(center);

      firstSegmentData = modify.rBush_.forEachInExtent(
        [0, 0, 5, 5],
        function (node) {
          return node;
        }
      );
      expect(firstSegmentData.segment[0]).to.eql([1, 1]);
      expect(firstSegmentData.segment[1]).to.eql([1, 1]);

      listeners = getModifyListeners(feature, modify);
      expect(listeners).to.have.length(1);
    });

    it('updates polygon segment data', function () {
      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      const feature = features[0];
      let listeners;

      listeners = getModifyListeners(feature, modify);
      expect(listeners).to.have.length(1);

      let firstSegmentData;

      firstSegmentData = modify.rBush_.forEachInExtent(
        [0, 0, 5, 5],
        function (node) {
          return node;
        }
      );
      expect(firstSegmentData.segment[0]).to.eql([0, 0]);
      expect(firstSegmentData.segment[1]).to.eql([10, 20]);

      const coordinates = feature.getGeometry().getCoordinates();
      const firstVertex = coordinates[0][0];
      firstVertex[0] = 1;
      firstVertex[1] = 1;
      feature.getGeometry().setCoordinates(coordinates);

      firstSegmentData = modify.rBush_.forEachInExtent(
        [0, 0, 5, 5],
        function (node) {
          return node;
        }
      );
      expect(firstSegmentData.segment[0]).to.eql([1, 1]);
      expect(firstSegmentData.segment[1]).to.eql([10, 20]);

      listeners = getModifyListeners(feature, modify);
      expect(listeners).to.have.length(1);
    });
  });

  describe('handle feature removal during down-up sequence', function () {
    it('removes segment data of removed features from dragSegments_', function () {
      const collection = new Collection(features);
      const modify = new Modify({
        features: collection,
      });
      map.addInteraction(modify);
      simulateEvent('pointermove', 0, 0, null, 0);
      simulateEvent('pointerdown', 0, 0, null, 0);
      simulateEvent('pointermove', -10, -10, null, 0);
      simulateEvent('pointerdrag', -10, -10, null, 0);
      collection.remove(features[0]);
      expect(function () {
        simulateEvent('pointerup', -10, -10, null, 0);
      }).to.not.throwException();
    });
  });

  describe('Vertex feature', function () {
    it('tracks features and geometries and removes the vertexFeature on deactivation', function () {
      const collection = new Collection(features);
      const modify = new Modify({
        features: collection,
      });
      map.addInteraction(modify);
      expect(modify.vertexFeature_).to.be(null);

      simulateEvent('pointermove', 10, -20, null, 0);
      expect(modify.vertexFeature_).to.not.be(null);
      expect(modify.vertexFeature_.get('features').length).to.be(1);
      expect(modify.vertexFeature_.get('geometries').length).to.be(1);

      modify.setActive(false);
      expect(modify.vertexFeature_).to.be(null);
      map.removeInteraction(modify);
    });

    it('tracks features and geometries - multi geometry', function () {
      const collection = new Collection();
      const modify = new Modify({
        features: collection,
      });
      map.addInteraction(modify);
      const feature = new Feature(
        new MultiPoint([
          [10, 10],
          [10, 20],
        ])
      );
      collection.push(feature);
      simulateEvent('pointermove', 10, -20, null, 0);
      expect(modify.vertexFeature_.get('features')[0]).to.eql(feature);
      expect(modify.vertexFeature_.get('geometries')[0]).to.eql(
        feature.getGeometry()
      );
      map.removeInteraction(modify);
    });

    it('tracks features and geometries - geometry collection', function () {
      const collection = new Collection();
      const modify = new Modify({
        features: collection,
      });
      map.addInteraction(modify);
      const feature = new Feature(
        new GeometryCollection([fromExtent([0, 0, 10, 10]), new Point([5, 5])])
      );
      collection.push(feature);
      simulateEvent('pointermove', 5, -5, null, 0);
      expect(modify.vertexFeature_.get('features')[0]).to.eql(feature);
      expect(modify.vertexFeature_.get('geometries')[0]).to.eql(
        feature.getGeometry().getGeometriesArray()[1]
      );
    });

    it('works with hit detection of point features', function () {
      const modify = new Modify({
        hitDetection: layer,
        source: source,
      });
      map.addInteraction(modify);
      source.clear();
      const pointFeature = new Feature(new Point([0, 0]));
      source.addFeature(pointFeature);
      layer.setStyle(
        new Style({
          image: new CircleStyle({
            radius: 30,
            fill: new Fill({
              color: 'fuchsia',
            }),
          }),
        })
      );
      map.renderSync();
      simulateEvent('pointermove', 10, -10, null, 0);
      expect(modify.vertexFeature_.get('features')[0]).to.eql(pointFeature);
      expect(modify.vertexFeature_.get('geometries')[0]).to.eql(
        pointFeature.getGeometry()
      );
    });

    it('snaps to pointer by default', function () {
      const modify = new Modify({
        source: source,
      });
      map.addInteraction(modify);
      source.clear();
      const pointFeature = new Feature(new Point([0, 0]));
      source.addFeature(pointFeature);
      map.renderSync();
      simulateEvent('pointerdown', 2, 2, null, 0);
      simulateEvent('pointerdrag', 2, 2, null, 0);
      simulateEvent('pointerup', 2, 2, null, 0);
      expect(pointFeature.getGeometry().getCoordinates()).to.eql([2, -2]);
    });

    it('does not snap to pointer when snapToPointer is false', function () {
      const modify = new Modify({
        source: source,
        snapToPointer: false,
      });
      map.addInteraction(modify);
      source.clear();
      const pointFeature = new Feature(new Point([0, 0]));
      source.addFeature(pointFeature);
      map.renderSync();
      simulateEvent('pointerdown', 2, 2, null, 0);
      simulateEvent('pointerdrag', 2, 2, null, 0);
      simulateEvent('pointerup', 2, 2, null, 0);
      expect(pointFeature.getGeometry().getCoordinates()).to.eql([0, 0]);
    });
  });

  describe('#getOverlay', function () {
    it('returns the feature overlay layer', function () {
      const modify = new Modify({
        features: new Collection(),
      });
      expect(modify.getOverlay()).to.eql(modify.overlay_);
    });
  });

  describe('circle modification with snap', function () {
    it('changes the circle radius and center', function () {
      const circleFeature = new Feature(new Circle([10, 10], 20));
      features.length = 0;
      features.push(circleFeature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      const snap = new Snap({
        features: new Collection(features),
        pixelTolerance: 1,
      });
      map.addInteraction(snap);

      // Change center
      simulateEvent('pointermove', 10, -10, null, 0);
      simulateEvent('pointerdown', 10, -10, null, 0);
      simulateEvent('pointermove', 5, -5, null, 0);
      simulateEvent('pointerdrag', 5, -5, null, 0);
      simulateEvent('pointerup', 5, -5, null, 0);

      expect(circleFeature.getGeometry().getRadius()).to.equal(20);
      expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]);

      // Increase radius along x axis
      simulateEvent('pointermove', 25, -4, null, 0);
      simulateEvent('pointerdown', 25, -4, null, 0);
      simulateEvent('pointermove', 30, -5, null, 0);
      simulateEvent('pointerdrag', 30, -5, null, 0);
      simulateEvent('pointerup', 30, -5, null, 0);

      expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(25, 1e-9);
      expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]);

      // Increase radius along y axis
      simulateEvent('pointermove', 4, -30, null, 0);
      simulateEvent('pointerdown', 4, -30, null, 0);
      simulateEvent('pointermove', 5, -35, null, 0);
      simulateEvent('pointerdrag', 5, -35, null, 0);
      simulateEvent('pointerup', 5, -35, null, 0);

      expect(circleFeature.getGeometry().getRadius()).to.equal(30);
      expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]);
    });

    it('changes the circle radius and center in a user projection', function () {
      const userProjection = 'EPSG:3857';
      setUserProjection(userProjection);
      const viewProjection = map.getView().getProjection();

      const circleFeature = new Feature(
        new Circle([10, 10], 20).transform(viewProjection, userProjection)
      );
      features.length = 0;
      features.push(circleFeature);

      const modify = new Modify({
        features: new Collection(features),
      });
      map.addInteraction(modify);

      const snap = new Snap({
        features: new Collection(features),
        pixelTolerance: 1,
      });
      map.addInteraction(snap);

      // Change center
      simulateEvent('pointermove', 10, -10, null, 0);
      simulateEvent('pointerdown', 10, -10, null, 0);
      simulateEvent('pointermove', 5, -5, null, 0);
      simulateEvent('pointerdrag', 5, -5, null, 0);
      simulateEvent('pointerup', 5, -5, null, 0);

      const geometry1 = circleFeature
        .getGeometry()
        .clone()
        .transform(userProjection, viewProjection);
      expect(geometry1.getRadius()).to.roughlyEqual(20, 1e-9);
      expect(geometry1.getCenter()).to.eql([5, 5]);

      // Increase radius along x axis
      simulateEvent('pointermove', 25, -4, null, 0);
      simulateEvent('pointerdown', 25, -4, null, 0);
      simulateEvent('pointermove', 30, -5, null, 0);
      simulateEvent('pointerdrag', 30, -5, null, 0);
      simulateEvent('pointerup', 30, -5, null, 0);

      const geometry2 = circleFeature
        .getGeometry()
        .clone()
        .transform(userProjection, viewProjection);
      expect(geometry2.getRadius()).to.roughlyEqual(25, 1e-9);
      expect(geometry2.getCenter()).to.eql([5, 5]);

      // Increase radius along y axis
      simulateEvent('pointermove', 4, -30, null, 0);
      simulateEvent('pointerdown', 4, -30, null, 0);
      simulateEvent('pointermove', 5, -35, null, 0);
      simulateEvent('pointerdrag', 5, -35, null, 0);
      simulateEvent('pointerup', 5, -35, null, 0);

      const geometry3 = circleFeature
        .getGeometry()
        .clone()
        .transform(userProjection, viewProjection);
      expect(geometry3.getRadius()).to.roughlyEqual(30, 1e-9);
      expect(geometry3.getCenter()).to.eql([5, 5]);
    });
  });
});
