import Projection from '../../../../../src/ol/proj/Projection.js';
import WMTS, {
  optionsFromCapabilities,
} from '../../../../../src/ol/source/WMTS.js';
import WMTSCapabilities from '../../../../../src/ol/format/WMTSCapabilities.js';
import WMTSTileGrid from '../../../../../src/ol/tilegrid/WMTS.js';
import {getBottomLeft, getTopRight} from '../../../../../src/ol/extent.js';
import {get as getProjection} from '../../../../../src/ol/proj.js';

describe('ol/source/WMTS', function () {
  describe('when creating options from capabilities', function () {
    const parser = new WMTSCapabilities();
    let capabilities, content;
    before(function (done) {
      afterLoadText('spec/ol/format/wmts/ogcsample.xml', function (xml) {
        try {
          content = xml;
          capabilities = parser.read(xml);
        } catch (e) {
          done(e);
        }
        done();
      });
    });

    it('returns null if the layer was not found in the capabilities', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'invalid',
      });

      expect(options).to.be(null);
    });

    it('passes the crossOrigin option', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        matrixSet: 'google3857',
        crossOrigin: '',
      });

      expect(options.crossOrigin).to.be.eql('');
    });

    it('can create KVP options from spec/ol/format/wmts/ogcsample.xml', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        matrixSet: 'google3857',
      });

      expect(options.urls).to.be.an('array');
      expect(options.urls).to.have.length(1);
      expect(options.urls[0]).to.be.eql(
        'http://www.maps.bob/cgi-bin/MiraMon5_0.cgi?'
      );

      expect(options.layer).to.be.eql('BlueMarbleNextGeneration');

      expect(options.matrixSet).to.be.eql('google3857');

      expect(options.format).to.be.eql('image/jpeg');

      expect(options.projection).to.be.a(Projection);
      expect(options.projection).to.be.eql(getProjection('EPSG:3857'));

      expect(options.requestEncoding).to.be.eql('KVP');

      expect(options.tileGrid).to.be.a(WMTSTileGrid);

      expect(options.style).to.be.eql('DarkBlue');

      expect(options.dimensions).to.eql({Time: '20110805'});

      expect(options.crossOrigin).to.be(undefined);
    });

    it('can create REST options from spec/ol/format/wmts/ogcsample.xml', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        matrixSet: 'google3857',
        requestEncoding: 'REST',
      });

      expect(options.urls).to.be.an('array');
      expect(options.urls).to.have.length(1);
      expect(options.urls[0]).to.be.eql(
        'http://www.example.com/wmts/coastlines/{TileMatrix}/{TileRow}/{TileCol}.png'
      );

      expect(options.layer).to.be.eql('BlueMarbleNextGeneration');

      expect(options.matrixSet).to.be.eql('google3857');

      expect(options.format).to.be.eql('image/png');

      expect(options.projection).to.be.a(Projection);
      expect(options.projection).to.be.eql(getProjection('EPSG:3857'));

      expect(options.requestEncoding).to.be.eql('REST');

      expect(options.tileGrid).to.be.a(WMTSTileGrid);

      expect(options.style).to.be.eql('DarkBlue');

      expect(options.dimensions).to.eql({Time: '20110805'});
    });

    it('can find a MatrixSet by SRS identifier', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        projection: 'EPSG:3857',
        requestEncoding: 'REST',
      });

      expect(options.matrixSet).to.be.eql('google3857');
      expect(options.projection.getCode()).to.be.eql('EPSG:3857');
    });

    it('can find a MatrixSet by equivalent SRS identifier', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        projection: 'EPSG:900913',
        requestEncoding: 'REST',
      });

      expect(options.matrixSet).to.be.eql('google3857');
      expect(options.projection.getCode()).to.be.eql('EPSG:900913');
    });

    it('can find the default MatrixSet', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        requestEncoding: 'REST',
      });

      expect(options.matrixSet).to.be.eql('BigWorldPixel');
      expect(options.projection.getCode()).to.be.eql(
        'urn:ogc:def:crs:OGC:1.3:CRS84'
      );
    });

    it("uses the projection of the default MatrixSet if the config's projection is not supported", function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        projection: new Projection({
          code: 'EPSG:2056',
          units: 'm',
        }),
      });

      expect(options.matrixSet).to.be.eql('BigWorldPixel');
      expect(options.projection.getCode()).to.be.eql(
        'urn:ogc:def:crs:OGC:1.3:CRS84'
      );
    });

    it('uses extent of tile matrix instead of projection extent', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'BlueMarbleNextGeneration',
        matrixSet: 'google3857subset',
      });

      // Since google3857subset defines subset of space defined by the google3857 matrix set:
      // - top left corner: -10000000, 10000000
      // - calculated grid extent: [-10000000, 9999694.25188686, -9999694.25188686, 10000000]
      // then the tile grid extent is only a part of the full projection extent.

      const gridExtent = options.tileGrid.getExtent();
      const gridBottomLeft = getBottomLeft(gridExtent);
      const gridTopRight = getTopRight(gridExtent);
      expect(Math.round(gridBottomLeft[0])).to.be.eql(-10000000);
      expect(Math.round(gridBottomLeft[1])).to.be.eql(9999847);
      expect(Math.round(gridTopRight[0])).to.be.eql(-9999847);
      expect(Math.round(gridTopRight[1])).to.be.eql(10000000);

      const projExtent = options.projection.getExtent();
      const projBottomLeft = getBottomLeft(projExtent);
      const projTopRight = getTopRight(projExtent);
      expect(Math.round(projBottomLeft[0])).to.be.eql(-20037508);
      expect(Math.round(projBottomLeft[1])).to.be.eql(-20037508);
      expect(Math.round(projTopRight[0])).to.be.eql(20037508);
      expect(Math.round(projTopRight[1])).to.be.eql(20037508);
    });

    it("doesn't fail if the GetCap doesn't contains Constraint tags", function () {
      const tmpXml = content.replace(
        /<ows:Constraint[\s\S]*?<\/ows:Constraint>/g,
        ''
      );
      const tmpCapabilities = parser.read(tmpXml);
      expect(
        tmpCapabilities['OperationsMetadata']['GetTile']['DCP']['HTTP'][
          'Get'
        ][0]['Constraint']
      ).to.be(undefined);
      const options = optionsFromCapabilities(tmpCapabilities, {
        layer: 'BlueMarbleNextGeneration',
        matrixSet: 'google3857',
      });
      expect(options.layer).to.be.eql('BlueMarbleNextGeneration');
      expect(options.matrixSet).to.be.eql('google3857');
    });

    it("set KVP as default request encoding if the GetCap doesn't contains Constraint and ResourceUrl tags", function () {
      let tmpXml = content.replace(
        /<ows:Constraint[\s\S]*?<\/ows:Constraint>/g,
        ''
      );
      tmpXml = tmpXml.replace(/<ResourceURL[\s\S]*?"\/>/g, '');

      const tmpCapabilities = parser.read(tmpXml);
      expect(
        tmpCapabilities['OperationsMetadata']['GetTile']['DCP']['HTTP'][
          'Get'
        ][0]['Constraint']
      ).to.be(undefined);
      expect(tmpCapabilities['Contents']['Layer'][0]['ResourceURL']).to.be(
        undefined
      );
      const options = optionsFromCapabilities(tmpCapabilities, {
        layer: 'BlueMarbleNextGeneration',
        matrixSet: 'google3857',
      });
      expect(options.layer).to.be.eql('BlueMarbleNextGeneration');
      expect(options.matrixSet).to.be.eql('google3857');
      expect(options.urls).to.be.an('array');
      expect(options.urls).to.have.length(1);
      expect(options.urls[0]).to.be.eql(
        'http://www.maps.bob/cgi-bin/MiraMon5_0.cgi?'
      );
    });
  });

  describe('#getInterpolate()', function () {
    it('is true by default', function () {
      const source = new WMTS({});
      expect(source.getInterpolate()).to.be(true);
    });

    it('is false if constructed with interpolate: false', function () {
      const source = new WMTS({interpolate: false});
      expect(source.getInterpolate()).to.be(false);
    });

    it('is false if constructed with imageSmoothing: false', function () {
      const source = new WMTS({imageSmoothing: false});
      expect(source.getInterpolate()).to.be(false);
    });
  });

  describe('when creating tileUrlFunction', function () {
    const defaultTileGrid = new WMTSTileGrid({
      origin: [-20037508.342789244, 20037508.342789244],
      resolutions: [
        559082264.029 * 0.28e-3,
        279541132.015 * 0.28e-3,
        139770566.007 * 0.28e-3,
      ],
      matrixIds: [0, 1, 2],
    });

    it('can replace lowercase REST parameters', function () {
      const source = new WMTS({
        layer: 'layer',
        style: 'default',
        urls: [
          'http://host/{layer}/{style}/{tilematrixset}/{TileMatrix}/{TileCol}/{TileRow}.jpg',
        ],
        matrixSet: 'EPSG:3857',
        requestEncoding: 'REST',
        tileGrid: defaultTileGrid,
      });

      const projection = getProjection('EPSG:3857');
      const url = source.tileUrlFunction(
        source.getTileCoordForTileUrlFunction([1, 1, 1]),
        1,
        projection
      );
      expect(url).to.be.eql('http://host/layer/default/EPSG:3857/1/1/1.jpg');
    });

    it('can replace camelcase REST parameters', function () {
      const source = new WMTS({
        layer: 'layer',
        style: 'default',
        urls: [
          'http://host/{Layer}/{Style}/{tilematrixset}/{TileMatrix}/{TileCol}/{TileRow}.jpg',
        ],
        matrixSet: 'EPSG:3857',
        requestEncoding: 'REST',
        tileGrid: defaultTileGrid,
      });

      const projection = getProjection('EPSG:3857');
      const url = source.tileUrlFunction(
        source.getTileCoordForTileUrlFunction([1, 1, 1]),
        1,
        projection
      );
      expect(url).to.be.eql('http://host/layer/default/EPSG:3857/1/1/1.jpg');
    });

    it('can replace dimensions', function () {
      const source = new WMTS({
        layer: 'layer',
        style: 'default',
        dimensions: {'Time': 42},
        urls: [
          'http://host/{Layer}/{Style}/{Time}/{tilematrixset}/{TileMatrix}/{TileCol}/{TileRow}.jpg',
        ],
        matrixSet: 'EPSG:3857',
        requestEncoding: 'REST',
        tileGrid: defaultTileGrid,
      });

      const projection = getProjection('EPSG:3857');
      const url = source.tileUrlFunction(
        source.getTileCoordForTileUrlFunction([1, 1, 1]),
        1,
        projection
      );
      expect(url).to.be.eql('http://host/layer/default/42/EPSG:3857/1/1/1.jpg');
    });
  });

  describe('when creating options from Esri capabilities', function () {
    const parser = new WMTSCapabilities();
    let capabilities;
    before(function (done) {
      afterLoadText('spec/ol/format/wmts/arcgis.xml', function (xml) {
        try {
          capabilities = parser.read(xml);
        } catch (e) {
          done(e);
        }
        done();
      });
    });

    it('can create KVP options from spec/ol/format/wmts/arcgis.xml', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'Demographics_USA_Population_Density',
        requestEncoding: 'KVP',
        matrixSet: 'default028mm',
      });

      expect(options.urls).to.be.an('array');
      expect(options.urls).to.have.length(1);
      expect(options.urls[0]).to.be.eql(
        'https://services.arcgisonline.com/arcgis/rest/services/' +
          'Demographics/USA_Population_Density/MapServer/WMTS?'
      );
    });

    it('can create REST options from spec/ol/format/wmts/arcgis.xml', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'Demographics_USA_Population_Density',
        matrixSet: 'default028mm',
      });

      expect(options.urls).to.be.an('array');
      expect(options.urls).to.have.length(1);
      expect(options.urls[0]).to.be.eql(
        'https://services.arcgisonline.com/arcgis/rest/services/' +
          'Demographics/USA_Population_Density/MapServer/WMTS/' +
          'tile/1.0.0/Demographics_USA_Population_Density/' +
          '{Style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.png'
      );
    });
  });

  describe('when creating options from wgs84 capabilities', function () {
    const parser = new WMTSCapabilities();
    let capabilities;
    before(function (done) {
      afterLoadText(
        'spec/ol/format/wmts/capabilities_wgs84.xml',
        function (xml) {
          try {
            capabilities = parser.read(xml);
          } catch (e) {
            done(e);
          }
          done();
        }
      );
    });

    it('returns correct bounding box', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'baselayer',
        matrixSet: 'inspire_quad',
        requestEncoding: 'REST',
      });

      expect(options.urls).to.be.an('array');
      expect(options.urls).to.have.length(1);
      expect(options.urls[0]).to.be.eql(
        'https://example.com/wmts/baselayer/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png'
      );

      expect(options.layer).to.be.eql('baselayer');

      expect(options.matrixSet).to.be.eql('inspire_quad');

      expect(options.format).to.be.eql('image/png');

      expect(options.projection).to.be.a(Projection);
      expect(options.projection).to.be.eql(getProjection('EPSG:4326'));

      expect(options.requestEncoding).to.be.eql('REST');

      expect(options.tileGrid).to.be.a(WMTSTileGrid);
      expect(options.style).to.be.eql('default');

      const extent = options.tileGrid.getExtent();

      // compare with delta, due to rounding not the exact bounding box is returned...
      const expectDelta = (value, expected) =>
        expect(Math.abs(value - expected)).to.below(1e-10);

      expectDelta(extent[0], -180);
      expectDelta(extent[1], -90);
      expectDelta(extent[2], 180);
      expectDelta(extent[3], 90);
    });
  });
  describe('when creating options from wgs84 capabilities with BoundingBox', function () {
    const parser = new WMTSCapabilities();
    let capabilities;
    before(function (done) {
      afterLoadText(
        'spec/ol/format/wmts/capabilities_wgs84_with_boundingbox.xml',
        function (xml) {
          try {
            capabilities = parser.read(xml);
          } catch (e) {
            done(e);
          }
          done();
        }
      );
    });

    it('returns correct bounding box when the layer has BoundingBox', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'bmaphidpi',
        matrixSet: 'google3857',
        style: 'normal',
      });

      expect(options.layer).to.be.eql('bmaphidpi');

      expect(options.matrixSet).to.be.eql('google3857');

      expect(options.format).to.be.eql('image/jpeg');

      expect(options.requestEncoding).to.be.eql('REST');

      expect(options.tileGrid).to.be.a(WMTSTileGrid);
      expect(options.style).to.be.eql('normal');

      expect(options.projection).to.be.a(Projection);
      expect(options.projection).to.be.eql(getProjection('EPSG:3857'));

      const expectedMatrixSetExtend = [977650, 5838030, 1913530, 6281290];
      const extent = options.tileGrid.getExtent();

      // compare with delta, due to rounding not the exact bounding box is returned...
      const expectDelta = (value, expected) =>
        expect(Math.abs(value - expected)).to.below(1e-1);
      expectDelta(extent[0], expectedMatrixSetExtend[0]);
      expectDelta(extent[1], expectedMatrixSetExtend[1]);
      expectDelta(extent[2], expectedMatrixSetExtend[2]);
      expectDelta(extent[3], expectedMatrixSetExtend[3]);
    });
  });
  describe('set wrap x by bounding box if available', function () {
    const parser = new WMTSCapabilities();
    let capabilities;
    before(function (done) {
      afterLoadText(
        'spec/ol/format/wmts/capabilities_wrapx.xml',
        function (xml) {
          try {
            capabilities = parser.read(xml);
          } catch (e) {
            done(e);
          }
          done();
        }
      );
    });

    it('sets wrapx when no bounding box is set', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'no-bb',
        matrixSet: 'EPSG:3857',
        crossOrigin: 'anonymous',
      });
      expect(options.wrapX).to.be(true);
    });
    it('sets wrapx when only wgs 84 bb is set', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'only-wgs84-bb',
        matrixSet: 'EPSG:3857',
        crossOrigin: 'anonymous',
      });
      expect(options.wrapX).to.be(true);
    });
    it('does not set wrapx when wgs84 bb is set', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'no-wrap-wgs84-bb',
        matrixSet: 'EPSG:3857',
        crossOrigin: 'anonymous',
      });
      expect(options.wrapX).to.be(false);
    });
    it('does not set wrapx when tile matrix does not wrap', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'no-wrap-tm',
        matrixSet: 'EPSG:3857',
        crossOrigin: 'anonymous',
      });
      expect(options.wrapX).to.be(false);
    });
  });
  describe('when creating options from capabilities with TileMatrixSetLink', function () {
    const parser = new WMTSCapabilities();
    let capabilities;
    before(function (done) {
      afterLoadText(
        'spec/ol/format/wmts/capabilities_with_tilematrixsetlink.xml',
        function (xml) {
          try {
            capabilities = parser.read(xml);
          } catch (e) {
            done(e);
          }
          done();
        }
      );
    });

    it('returns correct bounding box for a layer', function () {
      const options = optionsFromCapabilities(capabilities, {
        layer: 'mean_atlas_land',
        matrixSet: 'inspire_quad',
        requestEncoding: 'REST',
      });

      expect(options.urls).to.be.an('array');
      expect(options.urls).to.have.length(1);
      expect(options.urls[0]).to.be.eql(
        'https://example.com/wmts/mean_atlas_land/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png'
      );

      expect(options.layer).to.be.eql('mean_atlas_land');

      expect(options.matrixSet).to.be.eql('inspire_quad');

      expect(options.format).to.be.eql('image/png');

      expect(options.projection).to.be.a(Projection);
      expect(options.projection).to.be.eql(getProjection('EPSG:4326'));

      expect(options.requestEncoding).to.be.eql('REST');

      expect(options.tileGrid).to.be.a(WMTSTileGrid);
      expect(options.style).to.be.eql('default');

      const extent = options.tileGrid.getExtent();

      // calculate with of one tile, this will be used as tolerance for result extent
      const tile_width =
        ((68247.34668319306 * 0.00028) /
          getProjection('EPSG:4326').getMetersPerUnit()) *
        256;

      // compare with delta, due to rounding not the exact bounding box is returned...
      const expectDelta = (value, expected) =>
        expect(Math.abs(value - expected)).to.below(tile_width + 1e-10);

      expectDelta(extent[0], -36);
      expectDelta(extent[1], 15);
      expectDelta(extent[2], 43);
      expectDelta(extent[3], 90);
    });
  });

  describe('#setUrls()', function () {
    it('sets the URL for the source', function () {
      const source = new WMTS({});

      const urls = [
        'https://a.example.com/',
        'https://b.example.com/',
        'https://c.example.com/',
      ];
      source.setUrls(urls);

      expect(source.getUrls()).to.eql(urls);
    });

    it('updates the key for the source', function () {
      const source = new WMTS({});

      const urls = [
        'https://a.example.com/',
        'https://b.example.com/',
        'https://c.example.com/',
      ];
      source.setUrls(urls);

      expect(source.getKey()).to.eql(urls.join('\n'));
    });

    it('generates the correct tileUrlFunction during application of setUrl()', function () {
      const projection = getProjection('EPSG:3857');
      const source = new WMTS({
        projection: projection,
        requestEncoding: 'REST',
        urls: [
          'http://1.example.com/{TileMatrix}/{TileRow}/{TileCol}.jpeg',
          'http://2.example.com/{TileMatrix}/{TileRow}/{TileCol}.jpeg',
        ],
        tileGrid: new WMTSTileGrid({
          matrixIds: [0, 1, 2, 3, 4, 5, 6, 7],
          origin: [2690000, 1285000],
          resolutions: [4000, 3750, 3500, 3250, 3000, 2750, 2500, 2250],
        }),
      });

      const urls = [
        'https://a.example.com/{TileMatrix}/{TileRow}/{TileCol}.jpg',
        'https://b.example.com/{TileMatrix}/{TileRow}/{TileCol}.jpg',
      ];
      source.setUrls(urls);
      const tileUrl1 = source.tileUrlFunction([2, 9, -5], 1, projection);
      expect(tileUrl1).to.match(/https\:\/\/[ab]\.example\.com\/2\/-5\/9\.jpg/);
    });
  });

  describe('url option', function () {
    it('expands url template', function () {
      const tileSource = new WMTS({
        url: '{1-3}',
      });

      const urls = tileSource.getUrls();
      expect(urls).to.eql(['1', '2', '3']);
    });
  });

  describe('#getUrls', function () {
    let sourceOptions;
    let source;

    beforeEach(function () {
      sourceOptions = {
        layer: 'layer',
        style: 'default',
        matrixSet: 'foo',
        requestEncoding: 'REST',
        tileGrid: new WMTSTileGrid({
          origin: [0, 0],
          resolutions: [],
          matrixIds: [],
        }),
      };
    });

    describe('using a "url" option', function () {
      beforeEach(function () {
        sourceOptions.url = 'some_wmts_url';
        source = new WMTS(sourceOptions);
      });

      it('returns the WMTS URLs', function () {
        const urls = source.getUrls();
        expect(urls).to.be.eql(['some_wmts_url']);
      });
    });

    describe('using a "urls" option', function () {
      beforeEach(function () {
        sourceOptions.urls = ['some_wmts_url1', 'some_wmts_url2'];
        source = new WMTS(sourceOptions);
      });

      it('returns the WMTS URLs', function () {
        const urls = source.getUrls();
        expect(urls).to.be.eql(['some_wmts_url1', 'some_wmts_url2']);
      });
    });
  });

  describe('#getRequestEncoding', function () {
    let source;

    beforeEach(function () {
      source = new WMTS({
        layer: 'layer',
        style: 'default',
        matrixSet: 'foo',
        requestEncoding: 'REST',
        tileGrid: new WMTSTileGrid({
          origin: [0, 0],
          resolutions: [],
          matrixIds: [],
        }),
      });
    });

    it('returns the request encoding', function () {
      const requestEncoding = source.getRequestEncoding();
      expect(requestEncoding).to.be.eql('REST');
    });
  });
});
