import events from 'events';
import fse from 'fs-extra';
import path from 'path';
import {fileURLToPath} from 'url';
import {setLevel as setLogLevel} from '../../../../src/ol/console.js';
import {overrideXHR, restoreXHR} from '../../../../src/ol/net.js';
import {get as getProjection} from '../../../../src/ol/proj.js';
import Projection from '../../../../src/ol/proj/Projection.js';
import {
  appendCollectionsQueryParam,
  getMapTileUrlTemplate,
  getTileSetInfo,
  getVectorTileUrlTemplate,
  parseTileMatrixSet,
} from '../../../../src/ol/source/ogcTileUtil.js';
import TileGrid from '../../../../src/ol/tilegrid/TileGrid.js';
import expect from '../../expect.js';

function getDataDir() {
  const modulePath = fileURLToPath(import.meta.url);
  return path.join(path.dirname(modulePath), 'data');
}

let baseUrl;

class MockXHR extends events.EventEmitter {
  addEventListener(type, listener) {
    this.addListener(type, listener);
  }

  open(method, url) {
    if (url.startsWith(baseUrl)) {
      url = url.slice(baseUrl.length);
    }
    this.url = url;
  }

  setRequestHeader(key, value) {
    // no-op
  }

  send() {
    let url = path.resolve(getDataDir(), this.url);
    if (!url.endsWith('.json')) {
      url = url + '.json';
    }
    fse.readJSON(url).then(
      (data) => {
        this.status = 200;
        this.responseText = JSON.stringify(data);
        this.emit('load', {target: this});
      },
      (err) => {
        console.error(err); // eslint-disable-line no-console
        this.emit('error', {target: this});
      },
    );
  }
}

describe('ol/source/ogcTileUtil.js', () => {
  describe('getTileSetInfo()', () => {
    beforeEach(() => {
      overrideXHR(MockXHR);
    });

    afterEach(() => {
      baseUrl = '';
      restoreXHR();
    });

    it('fetches and parses map tile info', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad',
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.urlTemplate).to.be(
        '/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.jpg',
      );
      expect(tileInfo.projection).to.be.a(Projection);
      expect(tileInfo.projection.getCode()).to.be(
        'http://www.opengis.net/def/crs/EPSG/0/3857',
      );
      expect(tileInfo.grid).to.be.a(TileGrid);
      expect(tileInfo.grid.getTileSize(0)).to.eql([256, 256]);
      expect(tileInfo.grid.getResolutions()).to.have.length(10);
      expect(tileInfo.urlFunction).to.be.a(Function);
      expect(tileInfo.urlFunction([3, 2, 1])).to.be(
        'https://maps.ecere.com/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad/3/1/2.jpg',
      );
      expect(tileInfo.urlFunction([3, -1, 0])).to.be(undefined); // below min x
      expect(tileInfo.urlFunction([3, 8, 0])).to.be(undefined); // above max x
      expect(tileInfo.urlFunction([3, 0, -1])).to.be(undefined); // below min y
      expect(tileInfo.urlFunction([3, 0, 8])).to.be(undefined); // above max y
    });

    it('allows preferred media type to be configured', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad',
        mediaType: 'image/png',
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.urlTemplate).to.be(
        '/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.png',
      );
      expect(tileInfo.urlFunction).to.be.a(Function);
      expect(tileInfo.urlFunction([3, 2, 1])).to.be(
        'https://maps.ecere.com/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad/3/1/2.png',
      );
    });

    it('fetches and parses vector tile info', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/ne_10m_admin_0_countries/tiles/WebMercatorQuad',
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.json',
      );
      expect(tileInfo.grid).to.be.a(TileGrid);
      expect(tileInfo.grid.getTileSize(0)).to.eql([256, 256]);
      expect(tileInfo.grid.getResolutions()).to.have.length(8);
      expect(tileInfo.urlFunction).to.be.a(Function);
      expect(tileInfo.urlFunction([3, 2, 1])).to.be(
        'https://maps.ecere.com/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/3/1/2.json',
      );
      expect(tileInfo.urlFunction([2, -1, 0])).to.be(undefined); // below min x
      expect(tileInfo.urlFunction([2, 4, 0])).to.be(undefined); // above max x
      expect(tileInfo.urlFunction([2, 0, -1])).to.be(undefined); // below min y
      expect(tileInfo.urlFunction([2, 0, 4])).to.be(undefined); // above max y
    });

    it('orderedAxes overrides the projection axis orientation', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/ne_10m_admin_0_countries/tiles/WorldCRS84Quad',
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.projection).to.be.a(Projection);
      expect(tileInfo.projection.getCode()).to.be(
        'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
      );
      expect(tileInfo.urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WorldCRS84Quad/{tileMatrix}/{tileRow}/{tileCol}.json',
      );
      expect(tileInfo.grid).to.be.a(TileGrid);
      expect(tileInfo.grid.getExtent()).to.eql([-180, -90, 180, 90]);
      expect(tileInfo.grid.getTileSize(0)).to.eql([256, 256]);
      expect(tileInfo.grid.getResolutions()).to.have.length(7);
      expect(tileInfo.urlFunction).to.be.a(Function);
      expect(tileInfo.urlFunction([3, 2, 1])).to.be(
        'https://maps.ecere.com/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WorldCRS84Quad/3/1/2.json',
      );
      expect(tileInfo.urlFunction([2, -1, 0])).to.be(undefined); // below min x
      expect(tileInfo.urlFunction([2, 4, 0])).to.not.be(undefined); // below max x
      expect(tileInfo.urlFunction([2, 8, 0])).to.be(undefined); // above max x
      expect(tileInfo.urlFunction([2, 0, -1])).to.be(undefined); // below min y
      expect(tileInfo.urlFunction([2, 0, 4])).to.be(undefined); // above max y
    });

    it('allows projection to be overridden', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/ne_10m_admin_0_countries/tiles/WorldCRS84Quad',
        projection: getProjection('EPSG:4326'),
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.projection).to.be.a(Projection);
      expect(tileInfo.projection.getCode()).to.be('EPSG:4326');
    });

    it('allows preferred media type to be configured', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/ne_10m_admin_0_countries/tiles/WebMercatorQuad',
        mediaType: 'application/vnd.mapbox-vector-tile',
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.mvt',
      );
      expect(tileInfo.urlFunction).to.be.a(Function);
      expect(tileInfo.urlFunction([3, 2, 1])).to.be(
        'https://maps.ecere.com/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/3/1/2.mvt',
      );
    });

    it('uses supported media types if available', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/ne_10m_admin_0_countries/tiles/WebMercatorQuad',
        supportedMediaTypes: [
          'bogus-media-type',
          'application/vnd.mapbox-vector-tile',
          'application/geo+json', // should not be used
        ],
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.mvt',
      );
      expect(tileInfo.urlFunction).to.be.a(Function);
      expect(tileInfo.urlFunction([3, 2, 1])).to.be(
        'https://maps.ecere.com/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/3/1/2.mvt',
      );
    });

    it('treats supported media types in descending order of priority', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/ne_10m_admin_0_countries/tiles/WebMercatorQuad',
        supportedMediaTypes: [
          'bogus-media-type',
          'application/geo+json', // should be preferred
          'application/vnd.mapbox-vector-tile',
        ],
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.json',
      );
      expect(tileInfo.urlFunction).to.be.a(Function);
      expect(tileInfo.urlFunction([3, 2, 1])).to.be(
        'https://maps.ecere.com/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/3/1/2.json',
      );
    });

    it('works with a tile matrix set that uses a crs object with uri string', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuadObjectCRS',
      };
      const tileInfo = await getTileSetInfo(sourceInfo);
      expect(tileInfo).to.be.an(Object);
      expect(tileInfo.projection).to.be.a(Projection);
      expect(tileInfo.projection.getCode()).to.be(
        'http://www.opengis.net/def/crs/EPSG/0/3857',
      );
    });

    it('fails with a tile matrix set that uses a crs object with a wkt object', async () => {
      baseUrl = 'https://maps.ecere.com/';
      const sourceInfo = {
        url: 'https://maps.ecere.com/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuadObjectWKT',
      };

      let error;
      try {
        await getTileSetInfo(sourceInfo);
      } catch (err) {
        error = err;
      }
      expect(error).to.be.an(Error);
      expect(error.message).to.be(
        'Unsupported CRS: {"wkt":{"supported":false}}',
      );
    });
  });

  describe('getVectorTileUrlTemplate()', () => {
    let collectionLinks;
    let links;
    before(async () => {
      const collectionUrl = path.join(
        getDataDir(),
        'ogcapi/collections/ne_10m_admin_0_countries/tiles/WebMercatorQuad.json',
      );
      const collectionTileSet = await fse.readJSON(collectionUrl);
      collectionLinks = collectionTileSet.links;
      const url = path.join(getDataDir(), 'ogcapi/tiles/WebMercatorQuad.json');
      const tileSet = await fse.readJSON(url);
      links = tileSet.links;
    });

    it('gets the last known vector type if the preferred media type is absent', () => {
      const urlTemplate = getVectorTileUrlTemplate(collectionLinks);
      expect(urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.json',
      );
    });

    it('gets the preferred media type if given', () => {
      const urlTemplate = getVectorTileUrlTemplate(
        collectionLinks,
        'application/vnd.mapbox-vector-tile',
      );
      expect(urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.mvt',
      );
    });

    it('uses supported media types is preferred media type is not given', () => {
      const urlTemplate = getVectorTileUrlTemplate(collectionLinks, undefined, [
        'application/vnd.mapbox-vector-tile',
      ]);
      expect(urlTemplate).to.be(
        '/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.mvt',
      );
    });

    it('throws if it cannot find preferred media type or a known fallback', () => {
      function call() {
        getVectorTileUrlTemplate([], 'application/vnd.mapbox-vector-tile');
      }
      expect(call).to.throwException('Could not find "item" link');
    });

    it('appends the collections query parameter if given', () => {
      const urlTemplate = getVectorTileUrlTemplate(
        links,
        'application/vnd.mapbox-vector-tile',
        undefined,
        ['AeronauticCrv', 'CulturePnt'],
      );
      expect(urlTemplate).to.be(
        '/ogcapi/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}?f=mvt&collections=AeronauticCrv,CulturePnt',
      );
    });
  });

  describe('getMapTileUrlTemplate()', () => {
    let links;
    before(async () => {
      const url = path.join(
        getDataDir(),
        'ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad.json',
      );
      const tileSet = await fse.readJSON(url);
      links = tileSet.links;
    });

    it('gets the last known image type if the preferred media type is absent', () => {
      const urlTemplate = getMapTileUrlTemplate(links);
      expect(urlTemplate).to.be(
        '/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.jpg',
      );
    });

    it('gets the preferred media type if given', () => {
      const urlTemplate = getMapTileUrlTemplate(links, 'image/png');
      expect(urlTemplate).to.be(
        '/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}.png',
      );
    });

    it('throws if it cannot find preferred media type or a known fallback', () => {
      function call() {
        getMapTileUrlTemplate([], 'image/png');
      }
      expect(call).to.throwException('Could not find "item" link');
    });
  });

  describe('appendCollectionsQueryParam()', () => {
    beforeEach(() => {
      setLogLevel('none');
    });

    afterEach(() => {
      setLogLevel('info');
    });

    const collectionUrl =
      '/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad.json';
    const url = '/ogcapi/tiles/WebMercatorQuad.json';
    it('appends the collections parameter to the url', () => {
      const collections = ['foo', 'bar'];
      const appendedUrl = appendCollectionsQueryParam(url, collections);
      expect(appendedUrl).to.be(
        '/ogcapi/tiles/WebMercatorQuad.json?collections=foo,bar',
      );
    });

    it('returns the original url, if collections is empty', () => {
      const collections = [];
      const appendedUrl = appendCollectionsQueryParam(url, collections);
      expect(appendedUrl).to.be('/ogcapi/tiles/WebMercatorQuad.json');
    });

    it('returns the original url, if it points to a collection tileset', () => {
      const collections = ['foo'];
      const appendedUrl = appendCollectionsQueryParam(
        collectionUrl,
        collections,
      );
      expect(appendedUrl).to.be(
        '/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad.json',
      );
    });

    it('urlencodes a comma in the collection identifier', () => {
      const collections = ['foo,bar', 'baz'];
      const appendedUrl = appendCollectionsQueryParam(url, collections);
      expect(appendedUrl).to.be(
        '/ogcapi/tiles/WebMercatorQuad.json?collections=foo%2Cbar,baz',
      );
    });
  });

  describe('parseTileMatrixSet()', () => {
    it('handles minZoom correctly', async () => {
      const tileSet = await fse.readJSON(
        path.join(getDataDir(), 'ogcapi/tiles/WebMercatorQuad.json'),
      );
      const tileMatrixSet = await fse.readJSON(
        path.join(getDataDir(), 'ogcapi/tileMatrixSets/WebMercatorQuad.json'),
      );
      const tileInfo = parseTileMatrixSet(
        {},
        tileMatrixSet,
        undefined,
        tileSet.tileMatrixSetLimits,
      );
      expect(tileInfo.grid.getMinZoom()).to.be(6);
    });
  });
});