/* eslint-env jest*/

import OlGeomCircle from 'ol/geom/Circle';
import OlGeomLineString from 'ol/geom/LineString';
import OlGeomPolygon from 'ol/geom/Polygon';
import OlMap from 'ol/Map';
import OlView from 'ol/View';

import {
  MeasureUtil,
} from '../index';
import TestUtil from '../TestUtil';

describe('MeasureUtil', () => {

  const smallPolyCoords = [
    [0, 0],
    [0, 10],
    [10, 10],
    [10, 0],
    [0, 0]
  ];
  let map: OlMap;

  describe('Basics', () => {
    it('is defined', () => {
      expect(MeasureUtil).toBeDefined();
    });
  });

  describe('Static methods', () => {

    describe('#getLength', () => {
      it('is defined', () => {
        expect(MeasureUtil.getLength).toBeDefined();
      });
      it('get the length of a line as expected', () => {
        const start = [0, 0];
        const end = [0, 100];
        const end2 = [0, 100550];

        const shortLine = new OlGeomLineString([start, end]);
        const longLine = new OlGeomLineString([start, end2]);

        map = TestUtil.createMap();

        const expectedShortLength = MeasureUtil.getLength(shortLine, map);
        const expectedLongLength = MeasureUtil.getLength(longLine, map);

        expect(expectedShortLength).toBeCloseTo(99.88824008937313, 4);
        expect(expectedLongLength).toBeCloseTo(100433.46540039503, 4);

        TestUtil.removeMap(map);
      });
    });

    describe('#formatLength', () => {
      it('is defined', () => {
        expect(MeasureUtil.formatLength).toBeDefined();
      });
      it('formats the length of a line as expected', () => {
        const start = [0, 0];
        const end = [0, 100];
        const end2 = [0, 100550];
        const end3 = [0, 0.1];

        const shortLine = new OlGeomLineString([start, end]);
        const longLine = new OlGeomLineString([start, end2]);
        const veryShortLine = new OlGeomLineString([start, end3]);

        map = TestUtil.createMap();

        const expectedShortLength = MeasureUtil.formatLength(shortLine, map, 2);
        const expectedLongLength = MeasureUtil.formatLength(longLine, map, 2);
        const expectedVeryShortLength = MeasureUtil.formatLength(veryShortLine, map, 2);

        expect(expectedShortLength).toBe('99.89 m');
        expect(expectedLongLength).toBe('100.43 km');
        expect(expectedVeryShortLength).toBe('99.89 mm');

        TestUtil.removeMap(map);
      });
    });

    describe('#getArea', () => {
      it('is defined', () => {
        expect(MeasureUtil.getArea).toBeDefined();
      });
      it('get the area of a polygon as expected', () => {
        const bigPolyCoords = smallPolyCoords.map(coord => [coord[0] * 100, coord[1] * 100]);

        const smallPoly = new OlGeomPolygon([smallPolyCoords]);
        const bigPoly = new OlGeomPolygon([bigPolyCoords]);

        map = TestUtil.createMap();

        const expectedSmallArea = MeasureUtil.getArea(smallPoly, map);
        const expectedBigArea = MeasureUtil.getArea(bigPoly, map);

        expect(expectedSmallArea).toBe(99.7766050826797);
        expect(expectedBigArea).toBe(997766.042705488);

        TestUtil.removeMap(map);
      });
    });

    describe('#formatArea', () => {
      it('is defined', () => {
        expect(MeasureUtil.formatArea).toBeDefined();
      });
      it('formats the area of a polygon as expected', () => {
        const bigPolyCoords = smallPolyCoords.map(coord => [coord[0] * 100, coord[1] * 100]);
        const verySmallPolyCoords = smallPolyCoords.map(coord => [coord[0] / 100, coord[1] / 100]);

        const smallPoly = new OlGeomPolygon([smallPolyCoords]);
        const bigPoly = new OlGeomPolygon([bigPolyCoords]);
        const verySmallPoly = new OlGeomPolygon([verySmallPolyCoords]);

        map = TestUtil.createMap();

        const expectedSmallArea = MeasureUtil.formatArea(smallPoly, map, 2);
        const expectedBigArea = MeasureUtil.formatArea(bigPoly, map, 2);
        const expectedVerySmallArea = MeasureUtil.formatArea(verySmallPoly, map, 2);

        expect(expectedSmallArea).toBe('99.78 m<sup>2</sup>');
        expect(expectedBigArea).toBe('1 km<sup>2</sup>');
        expect(expectedVerySmallArea).toBe('9977.66 mm<sup>2</sup>');

        TestUtil.removeMap(map);
      });
    });

    describe('#angle', () => {
      it('is defined', () => {
        expect(MeasureUtil.angle).toBeDefined();
      });
      it('calculates the angle in deegrees', function() {
        const start = [0, 0];
        const ends = [
          [1, 0], // east, 3 o'clock
          [1, -1], // south-east, between 4 and 5 o'clock
          [0, -1], // south, 6 o'clock
          [-1, -1], // south-west, between 7 and 8 o'clock
          [-1, 0], // west, 9 o'clock
          [-1, 1], // north-west, between 10 and 11 o'clock
          [0, 1], // north, 12 o'clock
          [1, 1] // north-east, between 1 and 2 o'clock
        ];
        const expectedAngles = [
          180,
          135,
          90,
          45,
          0,
          -45,
          -90,
          -135
        ];

        expect(ends.length).toBe(expectedAngles.length);

        ends.forEach((end, index) => {
          const got = MeasureUtil.angle(start, end);
          expect(got).toBe(expectedAngles[index]);
        });
      });
    });

    describe('#angle360', () => {
      it('is defined', () => {
        expect(MeasureUtil.angle360).toBeDefined();
      });
      it('calculates the angle in deegrees ranged from 0° and 360°', function() {
        const start = [0, 0];
        const ends = [
          [1, 0], // east, 3 o'clock
          [1, -1], // south-east, between 4 and 5 o'clock
          [0, -1], // south, 6 o'clock
          [-1, -1], // south-west, between 7 and 8 o'clock
          [-1, 0], // west, 9 o'clock
          [-1, 1], // north-west, between 10 and 11 o'clock
          [0, 1], // north, 12 o'clock
          [1, 1] // north-east, between 1 and 2 o'clock
        ];
        const expectedAngles = [
          180,
          135,
          90,
          45,
          0,
          315,
          270,
          225
        ];

        expect(ends.length).toBe(expectedAngles.length);

        ends.forEach((end, index) => {
          const got = MeasureUtil.angle360(start, end);
          expect(got).toBe(expectedAngles[index]);
        });
      });
    });

    describe('#makeClockwise', () => {
      it('is defined', () => {
        expect(MeasureUtil.makeClockwise).toBeDefined();
      });
      it('returns a clockwised version of an angle', function() {
        expect(MeasureUtil.makeClockwise(0)).toBe(360);
        expect(MeasureUtil.makeClockwise(45)).toBe(315);
        expect(MeasureUtil.makeClockwise(90)).toBe(270);
        expect(MeasureUtil.makeClockwise(135)).toBe(225);
        expect(MeasureUtil.makeClockwise(180)).toBe(180);
        expect(MeasureUtil.makeClockwise(225)).toBe(135);
        expect(MeasureUtil.makeClockwise(270)).toBe(90);
        expect(MeasureUtil.makeClockwise(315)).toBe(45);
        expect(MeasureUtil.makeClockwise(360)).toBe(0);
      });
    });

    describe('#makeZeroDegreesAtNorth', () => {
      it('is defined', () => {
        expect(MeasureUtil.makeZeroDegreesAtNorth).toBeDefined();
      });
      it('shifts a calculates the angle so 0° is in the north', function() {
        const start = [0, 0];
        const ends = [
          [1, 0], // east, 3 o'clock
          [1, -1], // south-east, between 4 and 5 o'clock
          [0, -1], // south, 6 o'clock
          [-1, -1], // south-west, between 7 and 8 o'clock
          [-1, 0], // west, 9 o'clock
          [-1, 1], // north-west, between 10 and 11 o'clock
          [0, 1], // north, 12 o'clock
          [1, 1] // north-east, between 1 and 2 o'clock
        ];
        const expectedAngles = [
          270,
          225,
          180,
          135,
          90,
          45,
          360, // also 0
          315
        ];

        expect(ends.length).toBe(expectedAngles.length);

        ends.forEach((end, index) => {
          const angle = MeasureUtil.angle360(start, end);
          const got = MeasureUtil.makeZeroDegreesAtNorth(angle);
          expect(got).toBe(expectedAngles[index]);
        });
      });
    });

    describe('#formatAngle', () => {
      it('is defined', () => {
        expect(MeasureUtil.formatAngle).toBeDefined();
      });
      it('formats the angle of a multiline as expected', () => {
        const start = [0, 0];
        const ends = [
          [1, 0], // east, 3 o'clock
          [1, -1], // south-east, between 4 and 5 o'clock
          [0, -1], // south, 6 o'clock
          [-1, -1], // south-west, between 7 and 8 o'clock
          [-1, 0], // west, 9 o'clock
          [-1, 1], // north-west, between 10 and 11 o'clock
          [0, 1], // north, 12 o'clock
          [1, 1] // north-east, between 1 and 2 o'clock
        ];

        const lines = ends.map(end => new OlGeomLineString([start, end]));

        const expectedAngles = [
          '90.00°',
          '135.00°',
          '180.00°',
          '225.00°',
          '270.00°',
          '315.00°',
          '0.00°', // also 0°
          '45.00°'
        ];
        expect(ends.length).toBe(expectedAngles.length);
        expect(lines.length).toBe(expectedAngles.length);

        lines.forEach((line, index) => {
          const angle = MeasureUtil.formatAngle(line);
          expect(angle).toBe(expectedAngles[index]);
        });
      });
    });

    describe('#getAreaOfCircle - calculates the area of a circle correctly', () => {
      it('for metrical units', () => {
        const map3857 = TestUtil.createMap({
          view: new OlView({
            projection: 'EPSG:3857'
          }),
          resolutions: []
        });
        const radius = 100;
        const expectedArea = Math.PI * Math.pow(radius, 2);
        const circle = new OlGeomCircle([0, 0], radius);
        const result = MeasureUtil.getAreaOfCircle(circle, map3857);
        expect(result).toBeCloseTo(expectedArea, 6);
      });

      it('for spherical units', () => {
        const map4326 = TestUtil.createMap({
          view: new OlView({
            projection: 'EPSG:4326'
          }),
          resolutions: []
        });
        const radius = 100;
        const expectedArea = Math.PI * Math.pow(radius, 2);
        const circle = new OlGeomCircle([1, 1], radius);
        const result = MeasureUtil.getAreaOfCircle(circle, map4326);
        expect(result).toBeCloseTo(expectedArea);
      });
    });

  });
});
