import { action, runInAction, toJS, when } from "mobx";
import { http, HttpResponse } from "msw";
import buildModuleUrl from "terriajs-cesium/Source/Core/buildModuleUrl";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import RequestScheduler from "terriajs-cesium/Source/Core/RequestScheduler";
import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource";
import Entity from "terriajs-cesium/Source/DataSources/Entity";
import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection";
import hashEntity from "../../lib/Core/hashEntity";
import Result from "../../lib/Core/Result";
import TerriaError from "../../lib/Core/TerriaError";
import PickedFeatures from "../../lib/Map/PickedFeatures/PickedFeatures";
import CameraView from "../../lib/Models/CameraView";
import CsvCatalogItem from "../../lib/Models/Catalog/CatalogItems/CsvCatalogItem";
import MagdaReference from "../../lib/Models/Catalog/CatalogReferences/MagdaReference";
import UrlReference, {
  UrlToCatalogMemberMapping
} from "../../lib/Models/Catalog/CatalogReferences/UrlReference";
import ArcGisFeatureServerCatalogItem from "../../lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem";
import ArcGisMapServerCatalogItem from "../../lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem";
import WebMapServiceCatalogGroup from "../../lib/Models/Catalog/Ows/WebMapServiceCatalogGroup";
import WebMapServiceCatalogItem from "../../lib/Models/Catalog/Ows/WebMapServiceCatalogItem";
import CommonStrata from "../../lib/Models/Definition/CommonStrata";
import { BaseModel } from "../../lib/Models/Definition/Model";
import TerriaFeature from "../../lib/Models/Feature/Feature";
import {
  isInitFromData,
  isInitFromDataPromise,
  isInitFromOptions,
  isInitFromUrl
} from "../../lib/Models/InitSource";
import Terria from "../../lib/Models/Terria";
import ViewerMode from "../../lib/Models/ViewerMode";
import ViewState from "../../lib/ReactViewModels/ViewState";
import { buildShareLink } from "../../lib/ReactViews/Map/Panels/SharePanel/BuildShareLink";
import SimpleCatalogItem from "../Helpers/SimpleCatalogItem";
import { defaultBaseMaps } from "../../lib/Models/BaseMaps/defaultBaseMaps";
import { worker } from "../mocks/browser";

import mapConfigBasicJson from "../../wwwroot/test/Magda/map-config-basic.json";
import mapConfigV7Json from "../../wwwroot/test/Magda/map-config-v7.json";
import mapConfigInlineInitJson from "../../wwwroot/test/Magda/map-config-inline-init.json";
import mapConfigDereferencedJson from "../../wwwroot/test/Magda/map-config-dereferenced.json";
import mapConfigDereferencedNewJson from "../../wwwroot/test/Magda/map-config-dereferenced-new.json";
import magdaRecord1 from "../../wwwroot/test/Magda/shareKeys/6b24aa39-1aa7-48d1-b6a6-9e755aff4476.json";
import magdaRecord2 from "../../wwwroot/test/Magda/shareKeys/bfc69476-1c85-4208-9046-4f736bab9b8e.json";
import magdaRecord3 from "../../wwwroot/test/Magda/shareKeys/12f26f07-f39e-4753-979d-2de01af54bd1.json";
import mapConfigOld from "../../wwwroot/test/Magda/shareKeys/map-config-example-old.json";
import mapConfigNew from "../../wwwroot/test/Magda/shareKeys/map-config-example-new.json";
import configProxy from "../../wwwroot/test/init/configProxy.json";
import serverConfig from "../../wwwroot/test/init/serverconfig.json";
import mapServerSimpleGroupJson from "../../wwwroot/test/Terria/applyInitData/MapServer/mapServerSimpleGroup.json";
import mapServerWithErrorJson from "../../wwwroot/test/Terria/applyInitData/MapServer/mapServerWithError.json";
import magdaGroupRecordJson from "../../wwwroot/test/Terria/applyInitData/MagdaReference/group_record.json";
import magdaWmsRecordJson from "../../wwwroot/test/Terria/applyInitData/MagdaReference/wms_record.json";
import esriFeatureServerJson from "../../wwwroot/test/Terria/applyInitData/FeatureServer/esri_feature_server.json";
import wmsCapabilitiesXml from "../../wwwroot/test/Terria/applyInitData/WmsServer/capabilities.xml";
import storyJson from "../../wwwroot/test/stories/TerriaJS App/my-story.json";

// i18nOptions for CI
const i18nOptions = {
  // Skip calling i18next.init in specs
  skipInit: true
};

describe("TerriaSpec", function () {
  let terria: Terria;

  beforeEach(function () {
    terria = new Terria({
      appBaseHref: "/",
      baseUrl: "./"
    });

    worker.use(
      http.get("*/serverconfig/*", () => HttpResponse.json({})),
      http.get("*/test-config.json", () => HttpResponse.json({}))
    );
  });

  describe("cesiumBaseUrl", function () {
    it("is set when passed as an option when constructing Terria", function () {
      terria = new Terria({
        appBaseHref: "/",
        baseUrl: "./",
        cesiumBaseUrl: "some/path/to/cesium"
      });
      const path = new URL(terria.cesiumBaseUrl).pathname;
      expect(path).toBe("/some/path/to/cesium/");
    });

    it("should default to a path relative to `baseUrl`", function () {
      terria = new Terria({
        appBaseHref: "/",
        baseUrl: "some/path/to/terria"
      });
      const path = new URL(terria.cesiumBaseUrl).pathname;
      expect(path).toBe("/some/path/to/terria/build/Cesium/build/");
    });

    it("should update the baseUrl setting in the cesium module", function () {
      expect(
        buildModuleUrl("Assets/some/image.png").endsWith(
          "/build/Cesium/build/Assets/some/image.png"
        )
      ).toBe(true);

      terria = new Terria({
        appBaseHref: "/",
        baseUrl: "some/path/to/terria"
      });
      expect(
        buildModuleUrl("Assets/some/image.png").endsWith(
          "/some/path/to/terria/build/Cesium/build/Assets/some/image.png"
        )
      ).toBe(true);
    });
  });

  describe("terria refresh catalog members from magda", function () {
    it("refreshes group aspect with given URL", async function () {
      worker.use(http.get("*/serverconfig/*", () => HttpResponse.json({})));

      function verifyGroups(groupAspect: any, groupNum: number) {
        const ids = groupAspect.members.map((member: any) => member.id);
        expect(terria.catalog.group.uniqueId).toEqual("/");
        // ensure user added data co-exists with dereferenced magda members
        expect(terria.catalog.group.members.length).toEqual(groupNum);
        expect(terria.catalog.userAddedDataGroup).toBeDefined();
        ids.forEach((id: string) => {
          const model = terria.getModelById(MagdaReference, id);
          if (!model) {
            throw new Error(`no record id. ID = ${id}`);
          }
          expect(terria.modelIds).toContain(id);
          expect(model.recordId).toEqual(id);
        });
      }

      await terria.start({
        configUrl: "test/Magda/map-config-dereferenced.json",
        i18nOptions
      });
      verifyGroups(mapConfigDereferencedJson.aspects["group"], 3);

      await terria.refreshCatalogMembersFromMagda(
        "test/Magda/map-config-dereferenced-new.json"
      );
      verifyGroups(mapConfigDereferencedNewJson.aspects["group"], 2);
    });
  });

  describe("terria start", function () {
    beforeEach(function () {
      worker.use(
        http.get("*/serverconfig/*", () => HttpResponse.json({ foo: "bar" })),
        http.get("*/proxyabledomains/*", () =>
          HttpResponse.json({ foo: "bar" })
        ),
        // from `terria.start()`
        http.get("*/test/Magda/map-config-basic.json", () =>
          HttpResponse.json(mapConfigBasicJson)
        ),
        http.get("*/test/Magda/map-config-v7.json", () =>
          HttpResponse.json(mapConfigV7Json)
        ),
        // terria's "Magda derived url"
        http.get("*/api/v0/registry/records/map-config-basic*", () =>
          HttpResponse.json(mapConfigBasicJson)
        ),
        // inline init
        http.get("*map-config-inline-init*", () =>
          HttpResponse.json(mapConfigInlineInitJson)
        ),
        http.get("*map-config-dereferenced-new*", () =>
          HttpResponse.json(mapConfigDereferencedNewJson)
        ),
        http.get("*map-config-dereferenced*", () =>
          HttpResponse.json(mapConfigDereferencedJson)
        )
      );
    });

    it("applies initSources in correct order", async function () {
      expect(terria.initSources.length).toEqual(0);
      worker.use(
        http.get("*/config.json", () =>
          HttpResponse.json({
            initializationUrls: ["something"]
          })
        ),
        http.get("*/init/something.json", () =>
          HttpResponse.json({
            workbench: ["test"],
            catalog: [
              { id: "test", type: "czml", url: "test.czml" },
              { id: "test-2", type: "czml", url: "test-2.czml" }
            ],
            showSplitter: false,
            splitPosition: 0.5
          })
        ),
        http.get("https://application.url/init/hash-init.json", () =>
          HttpResponse.json({
            // Override workbench in "init/something.json"
            workbench: ["test-2"],
            showSplitter: true
          })
        ),
        // This model is added to the workbench in "init/something.json" - which is loaded before "https://application.url/init/hash-init.json"
        // So we add a long delay to make sure that `workbench` is overridden by `hash-init.json`
        http.get("*/test.czml", async () => {
          return HttpResponse.json([{ id: "document", version: "1.0" }]);
        }),
        // Note: no delay for "test-2.czml" - which is added to `workbench` by `hash-init.json
        http.get("*/test-2.czml", () =>
          HttpResponse.json([{ id: "document", version: "1.0" }])
        )
      );
      await terria.start({
        configUrl: `config.json`,
        i18nOptions
      });

      await terria.updateApplicationUrl("https://application.url/#hash-init");

      expect(terria.initSources.length).toEqual(2);

      expect(terria.showSplitter).toBe(true);
      expect(terria.splitPosition).toBe(0.5);
      expect(terria.workbench.items.length).toBe(1);
      expect(terria.workbench.items[0].uniqueId).toBe("test-2");
    });

    it("works with initializationUrls and initFragmentPaths", async function () {
      expect(terria.initSources.length).toEqual(0);

      worker.use(
        http.get("*/path/to/config/configUrl.json", () =>
          HttpResponse.json({
            initializationUrls: ["something"],
            parameters: {
              applicationUrl: "https://application.url/",
              initFragmentPaths: [
                "path/to/init/",
                "https://hostname.com/some/other/path/"
              ]
            }
          })
        ),
        http.get("*/init/something.json", () =>
          HttpResponse.json({
            catalog: []
          })
        ),
        http.get("https://hostname.com/*", () => HttpResponse.json({}))
      );

      await terria.start({
        configUrl: `path/to/config/configUrl.json`,
        i18nOptions
      });

      expect(terria.initSources.length).toEqual(1);

      const initSource = terria.initSources[0];
      expect(isInitFromOptions(initSource)).toBeTruthy();

      if (!isInitFromOptions(initSource))
        throw "Init source is not from options";

      // Note: initFragmentPaths in `initializationUrls` are resolved to the base URL of configURL
      // - which is path/to/config/
      expect(
        initSource.options.map((source) =>
          isInitFromUrl(source) ? source.initUrl : ""
        )
      ).toEqual([
        "path/to/config/path/to/init/something.json",
        "https://hostname.com/some/other/path/something.json"
      ]);
    });

    describe("via loadMagdaConfig", function () {
      it("should dereference uniqueId to `/`", async function () {
        expect(terria.catalog.group.uniqueId).toEqual("/");

        worker.use(
          http.get("*/api/v0/registry/*", () =>
            HttpResponse.json(mapConfigBasicJson)
          )
        );
        // no init sources before starting
        expect(terria.initSources.length).toEqual(0);

        await terria.start({
          configUrl: "test/Magda/map-config-basic.json",
          i18nOptions
        });

        expect(terria.catalog.group.uniqueId).toEqual("/");
      });

      it("works with basic initializationUrls", async function () {
        worker.use(
          http.get("*/api/v0/registry/*", () =>
            HttpResponse.json(mapConfigBasicJson)
          )
        );

        // no init sources before starting
        expect(terria.initSources.length).toEqual(0);

        await terria.start({
          configUrl: "test/Magda/map-config-basic.json",
          i18nOptions
        });

        expect(terria.initSources.length).toEqual(1);
        expect(isInitFromUrl(terria.initSources[0])).toEqual(true);
        if (isInitFromUrl(terria.initSources[0])) {
          expect(terria.initSources[0].initUrl).toEqual(
            mapConfigBasicJson.aspects["terria-config"].initializationUrls[0]
          );
        } else {
          throw "not init source";
        }
      });

      it("works with v7initializationUrls", async function () {
        worker.use(
          http.get("*/api/v0/registry/*", () =>
            HttpResponse.json(mapConfigBasicJson)
          )
        );
        const groupName = "Simple converter test";
        worker.use(
          http.get("https://example.foo.bar/initv7.json", () =>
            HttpResponse.json({
              catalog: [{ name: groupName, type: "group", items: [] }]
            })
          )
        );
        // no init sources before starting
        expect(terria.initSources.length).toBe(0);

        await terria.start({
          configUrl: "test/Magda/map-config-v7.json",
          i18nOptions
        });

        expect(terria.initSources.length).toBe(1);
        expect(isInitFromDataPromise(terria.initSources[0])).toBeTruthy(
          "Expected initSources[0] to be an InitDataPromise"
        );
        if (isInitFromDataPromise(terria.initSources[0])) {
          const data = await terria.initSources[0].data;
          // JSON parse & stringify to avoid a problem where I think catalog-converter
          //  can return {"id": undefined} instead of no "id"
          expect(
            JSON.parse(JSON.stringify(data.ignoreError()?.data.catalog))
          ).toEqual([
            {
              name: groupName,
              type: "group",
              members: [],
              shareKeys: [`Root Group/${groupName}`]
            }
          ]);
        }
      });
      it("works with inline init", async function () {
        // inline init
        worker.use(
          http.get("*/api/v0/registry/*", () =>
            HttpResponse.json(mapConfigInlineInitJson)
          )
        );
        // no init sources before starting
        expect(terria.initSources.length).toEqual(0);
        await terria.start({
          configUrl: "test/Magda/map-config-inline-init.json",
          i18nOptions
        });

        const inlineInit = mapConfigInlineInitJson.aspects["terria-init"];
        /** Check cors domains */
        expect(terria.corsProxy.corsDomains).toEqual(inlineInit.corsDomains);
        /** Camera setting */
        expect(terria.mainViewer.homeCamera).toEqual(
          CameraView.fromJson(inlineInit.homeCamera)
        );

        /** Ensure inlined data catalog from init sources */
        expect(terria.initSources.length).toEqual(1);
        if (isInitFromData(terria.initSources[0])) {
          expect(terria.initSources[0].data.catalog).toEqual(
            inlineInit.catalog
          );
        } else {
          throw "not init source";
        }
      });
      it("parses dereferenced group aspect", async function () {
        expect(terria.catalog.group.uniqueId).toEqual("/");
        // dereferenced res
        worker.use(
          http.get("*/api/v0/registry/*", () =>
            HttpResponse.json(mapConfigDereferencedJson)
          )
        );

        await terria.start({
          configUrl: "test/Magda/map-config-dereferenced.json",
          i18nOptions
        });

        const groupAspect = mapConfigDereferencedJson.aspects["group"];
        const ids = groupAspect.members.map((member: any) => member.id);
        expect(terria.catalog.group.uniqueId).toEqual("/");
        // ensure user added data co-exists with dereferenced magda members
        expect(terria.catalog.group.members.length).toEqual(3);
        expect(terria.catalog.userAddedDataGroup).toBeDefined();
        ids.forEach((id: string) => {
          const model = terria.getModelById(MagdaReference, id);
          if (!model) {
            throw "no record id.";
          }
          expect(terria.modelIds).toContain(id);
          expect(model.recordId).toEqual(id);
        });
      });
    });

    it("calls `beforeRestoreAppState` before restoring app state from share data", async function () {
      terria = new Terria({
        appBaseHref: "/",
        baseUrl: "./"
      });

      const restoreAppState = spyOn(
        terria,
        "restoreAppState" as any
      ).and.callThrough();

      const beforeRestoreAppState = jasmine
        .createSpy("beforeRestoreAppState")
        // It should also handle errors when calling beforeRestoreAppState
        .and.callFake(() => Promise.reject("some error"));

      expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Cesium);
      await terria.start({
        configUrl: "test-config.json",
        applicationUrl: {
          href: "http://test.com/#map=2d"
        } as Location,
        beforeRestoreAppState
      });

      expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Leaflet);
      expect(beforeRestoreAppState).toHaveBeenCalledBefore(restoreAppState);
    });
  });

  describe("updateApplicationUrl", function () {
    it("works with initializationUrls and initFragmentPaths", async function () {
      expect(terria.initSources.length).toEqual(0);

      worker.use(
        http.get("*/path/to/config/configUrl.json", () =>
          HttpResponse.json({
            initializationUrls: ["something"],
            parameters: {
              applicationUrl: "https://application.url/",
              initFragmentPaths: [
                "path/to/init/",
                "https://hostname.com/some/other/path/"
              ]
            }
          })
        ),
        http.get("*/init/something.json", () =>
          HttpResponse.json({
            catalog: []
          })
        ),
        http.get("https://application.url/*", () => HttpResponse.json({})),
        http.get("https://hostname.com/*", () => HttpResponse.json({}))
      );

      await terria.start({
        configUrl: `path/to/config/configUrl.json`,
        i18nOptions
      });

      await terria.updateApplicationUrl(
        "https://application.url/#someInitHash"
      );

      expect(terria.initSources.length).toEqual(2);

      const initSource = terria.initSources[1];
      expect(isInitFromOptions(initSource)).toBeTruthy();

      if (!isInitFromOptions(initSource))
        throw "Init source is not from options";

      // Note: initFragmentPaths in hash parameters are resolved to the base URL of application URL
      // - which is https://application.url/
      expect(
        initSource.options.map((source) =>
          isInitFromUrl(source) ? source.initUrl : ""
        )
      ).toEqual([
        "https://application.url/path/to/init/someInitHash.json",
        "https://hostname.com/some/other/path/someInitHash.json"
      ]);
    });

    it("processes #start correctly", async function () {
      expect(terria.initSources.length).toEqual(0);

      worker.use(
        http.get("*/configUrl.json", () => HttpResponse.json({})),
        http.get("http://something/*", () => HttpResponse.json({}))
      );

      await terria.start({
        configUrl: `configUrl.json`,
        i18nOptions
      });

      // Test #start with two init sources
      // - one initURL = "http://something/init.json"
      // - one initData which sets `splitPosition`
      await terria.updateApplicationUrl(
        "https://application.url/#start=" +
          JSON.stringify({
            version: "8.0.0",
            initSources: ["http://something/init.json", { splitPosition: 0.3 }]
          })
      );

      expect(terria.initSources.length).toEqual(2);

      const urlInitSource = terria.initSources[0];
      expect(isInitFromUrl(urlInitSource)).toBeTruthy();

      if (!isInitFromUrl(urlInitSource)) throw "Init source is not from url";

      expect(urlInitSource.initUrl).toBe("http://something/init.json");

      const jsonInitSource = terria.initSources[1];
      expect(isInitFromData(jsonInitSource)).toBeTruthy();

      if (!isInitFromData(jsonInitSource)) throw "Init source is not from data";

      expect(jsonInitSource.data.splitPosition).toBe(0.3);
    });

    describe("test via serialise & load round-trip", function () {
      let newTerria: Terria;
      let viewState: ViewState;

      beforeEach(function () {
        newTerria = new Terria({ appBaseHref: "/", baseUrl: "./" });
        viewState = new ViewState({
          terria: terria,
          catalogSearchProvider: undefined
        });

        UrlToCatalogMemberMapping.register(
          (_s) => true,
          WebMapServiceCatalogItem.type,
          true
        );

        terria.catalog.userAddedDataGroup.addMembersFromJson(
          CommonStrata.user,
          [
            {
              id: "itemABC",
              name: "abc",
              type: "wms",
              url: "test/WMS/single_metadata_url.xml"
            },
            {
              id: "groupABC",
              name: "xyz",
              type: "wms-group",
              url: "test/WMS/single_metadata_url.xml"
            }
          ]
        );

        terria.catalog.group.addMembersFromJson(CommonStrata.user, [
          {
            id: "itemDEF",
            name: "def",
            type: "wms",
            url: "test/WMS/single_metadata_url.xml"
          }
        ]);
      });

      it("initializes user added data group with shared items", async function () {
        expect(newTerria.catalog.userAddedDataGroup.members).not.toContain(
          "itemABC"
        );
        expect(newTerria.catalog.userAddedDataGroup.members).not.toContain(
          "groupABC"
        );

        const shareLink = buildShareLink(terria, viewState);
        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();
        expect(newTerria.catalog.userAddedDataGroup.members).toContain(
          "itemABC"
        );
        expect(newTerria.catalog.userAddedDataGroup.members).toContain(
          "groupABC"
        );
      });

      it("initializes user added data group with shared UrlReference items", async function () {
        terria.catalog.userAddedDataGroup.addMembersFromJson(
          CommonStrata.user,
          [
            {
              id: "url_test",
              name: "foo",
              type: "url-reference",
              url: "test/WMS/single_metadata_url.xml"
            }
          ]
        );

        const shareLink = buildShareLink(terria, viewState);
        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();
        expect(newTerria.catalog.userAddedDataGroup.members).toContain(
          "url_test"
        );
        const urlRef = newTerria.getModelById(BaseModel, "url_test");
        expect(urlRef).toBeDefined();
        expect(urlRef instanceof UrlReference).toBe(true);

        if (urlRef instanceof UrlReference) {
          await urlRef.loadReference();
          expect(urlRef.target).toBeDefined();
        }
      });

      it("initializes workbench with shared workbench items", async function () {
        const model1 = terria.getModelById(
          BaseModel,
          "itemABC"
        ) as WebMapServiceCatalogItem;
        const model2 = terria.getModelById(
          BaseModel,
          "itemDEF"
        ) as WebMapServiceCatalogItem;
        await terria.workbench.add(model1);
        await terria.workbench.add(model2);
        expect(terria.workbench.itemIds).toContain("itemABC");
        expect(terria.workbench.itemIds).toContain("itemDEF");
        expect(newTerria.workbench.itemIds).toEqual([]);

        const shareLink = buildShareLink(terria, viewState);
        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();
        expect(newTerria.workbench.itemIds).toEqual(terria.workbench.itemIds);
      });

      it("initializes splitter correctly", async function () {
        const model1 = terria.getModelById(
          BaseModel,
          "itemABC"
        ) as WebMapServiceCatalogItem;
        await terria.workbench.add(model1);

        runInAction(() => {
          terria.showSplitter = true;
          terria.splitPosition = 0.7;
          model1.setTrait(
            CommonStrata.user,
            "splitDirection",
            SplitDirection.RIGHT
          );
        });

        const shareLink = buildShareLink(terria, viewState);
        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();
        expect(newTerria.showSplitter).toEqual(true);
        expect(newTerria.splitPosition).toEqual(0.7);
        expect(newTerria.workbench.itemIds).toEqual(["itemABC"]);

        const newModel1 = newTerria.getModelById(
          BaseModel,
          "itemABC"
        ) as WebMapServiceCatalogItem;
        expect(newModel1).toBeDefined();
        expect(newModel1.splitDirection).toEqual(SplitDirection.RIGHT as any);
      });

      it("opens and loads members of shared open groups", async function () {
        const group = terria.getModelById(
          BaseModel,
          "groupABC"
        ) as WebMapServiceCatalogGroup;
        await viewState.viewCatalogMember(group);
        expect(group.isOpen).toBe(true);
        expect(group.members.length).toBeGreaterThan(0);
        const shareLink = buildShareLink(terria, viewState);
        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();
        const newGroup = newTerria.getModelById(
          BaseModel,
          "groupABC"
        ) as WebMapServiceCatalogGroup;
        expect(newGroup.isOpen).toBe(true);
        expect(newGroup.members).toEqual(group.members);
      });
    });

    describe("using story route", function () {
      beforeEach(function () {
        // These specs must run with a Terria constructed with "appBaseHref": "/"
        // to make the specs work with browser runner
        terria.updateParameters({
          storyRouteUrlPrefix: "test/stories/TerriaJS%20App/"
        });

        worker.use(
          http.get("*/test/stories/TerriaJS%20App/my-story", () =>
            HttpResponse.json(storyJson)
          )
        );
      });

      it("sets playStory to 1", async function () {
        await terria.updateApplicationUrl(
          new URL("story/my-story", document.baseURI).toString()
        );
        expect(terria.userProperties.get("playStory")).toBe("1");
      });

      it("correctly adds the story share as a datasource", async function () {
        await terria.updateApplicationUrl(
          new URL("story/my-story", document.baseURI).toString()
        );
        expect(terria.initSources.length).toBe(1);
        expect(terria.initSources[0].name).toMatch(/my-story/);
        if (!isInitFromData(terria.initSources[0]))
          throw new Error("Expected initSource to be InitData from my-story");

        expect(toJS(terria.initSources[0].data)).toEqual(
          (storyJson as any).initSources[0]
        );
      });

      it("correctly adds the story share as a datasource when there's a trailing slash on story url", async function () {
        await terria.updateApplicationUrl(
          new URL("story/my-story/", document.baseURI).toString()
        );
        expect(terria.initSources.length).toBe(1);
        expect(terria.initSources[0].name).toMatch(/my-story/);
        if (!isInitFromData(terria.initSources[0]))
          throw new Error("Expected initSource to be InitData from my-story");

        expect(toJS(terria.initSources[0].data)).toEqual(
          (storyJson as any).initSources[0]
        );
      });
    });
  });

  // Test share keys by serialising from one catalog and deserialising with a reorganised catalog
  describe("shareKeys", function () {
    describe("with a JSON catalog", function () {
      let newTerria: Terria;
      let viewState: ViewState;
      beforeEach(async function () {
        // Create a config.json in a URL to pass to Terria.start
        const configUrl = `data:application/json;base64,${btoa(
          JSON.stringify({
            initializationUrls: [],
            parameters: {
              regionMappingDefinitionsUrls: ["data/regionMapping.json"]
            }
          })
        )}`;
        newTerria = new Terria({ baseUrl: "./" });
        viewState = new ViewState({
          terria: terria,
          catalogSearchProvider: undefined
        });

        await Promise.all(
          [terria, newTerria].map((t) => t.start({ configUrl, i18nOptions }))
        );

        terria.catalog.group.addMembersFromJson(CommonStrata.definition, [
          {
            name: "Old group",
            type: "group",
            members: [
              {
                name: "Random CSV",
                type: "csv",
                url: "data:text/csv,lon%2Clat%2Cval%2Cdate%0A151%2C-31%2C15%2C2010%0A151%2C-31%2C15%2C2011"
              }
            ]
          }
        ]);

        newTerria.catalog.group.addMembersFromJson(CommonStrata.definition, [
          {
            name: "New group",
            type: "group",
            members: [
              {
                name: "Extra group",
                type: "group",
                members: [
                  {
                    name: "My random CSV",
                    type: "csv",
                    url: "data:text/csv,lon%2Clat%2Cval%2Cdate%0A151%2C-31%2C15%2C2010%0A151%2C-31%2C15%2C2011",
                    shareKeys: ["//Old group/Random CSV"]
                  }
                ]
              }
            ]
          }
        ]);
      });

      it("correctly applies user stratum changes to moved item", async function () {
        const csv = terria.getModelById(
          CsvCatalogItem,
          "//Old group/Random CSV"
        );
        expect(csv).toBeDefined("Can't find csv item in source terria");
        csv?.setTrait(CommonStrata.user, "opacity", 0.5);
        const shareLink = buildShareLink(terria, viewState);
        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();

        const newCsv = newTerria.getModelById(
          CsvCatalogItem,
          "//New group/Extra group/My random CSV"
        );
        expect(newCsv).toBeDefined(
          "Can't find newCsv item in destination newTerria"
        );
        expect(newCsv?.opacity).toBe(0.5);
      });

      it("correctly adds moved item to workbench and timeline", async function () {
        const csv = terria.getModelById(
          CsvCatalogItem,
          "//Old group/Random CSV"
        );
        expect(csv).toBeDefined("csv not found in source terria");
        if (csv === undefined) return;
        await terria.workbench.add(csv);
        terria.timelineStack.addToTop(csv);
        const shareLink = buildShareLink(terria, viewState);
        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();

        const newCsv = newTerria.getModelById(
          CsvCatalogItem,
          "//New group/Extra group/My random CSV"
        );
        expect(newCsv).toBeDefined("newCsv not found in destination newTerria");
        if (newCsv === undefined) return;
        expect(newTerria.workbench.contains(newCsv)).toBeTruthy(
          "newCsv not found in destination newTerria workbench"
        );
        expect(newTerria.timelineStack.contains(newCsv)).toBeTruthy(
          "newCsv not found in destination newTerria timeline"
        );
      });
    });

    describe("with a Magda catalog", function () {
      // Simulate same as above but with Magda catalogs
      // This is really messy before a proper MagdaCatalogProvider is made
      //  that can call a (currently not yet written) Magda API to find the location of
      //  any id within a catalog

      // Could at least simulate moving an item deeper (similar to JSON catalog) and try having
      //  one of the knownContainerIds be shareKey linked to the new location?
      //  (hopefully that would trigger loading of the new group)

      let newTerria: Terria;
      let viewState: ViewState;
      beforeEach(async function () {
        // Create a config.json in a URL to pass to Terria.start
        const configUrl =
          "https://magda.example.com/api/v0/registry/records/map-config-example?optionalAspect=terria-config&optionalAspect=terria-init&optionalAspect=group&dereference=true";

        viewState = new ViewState({
          terria: terria,
          catalogSearchProvider: undefined
        });
        newTerria = new Terria({ baseUrl: "./" });

        // Simulate an update to catalog/config between terria and newTerria

        // Track how many times configUrl has been requested to serve different responses
        let configRequestCount = 0;
        worker.use(
          http.get("*/serverconfig/*", () => HttpResponse.json({})),
          http.get(
            "https://magda.example.com/api/v0/registry/records/6b24aa39-1aa7-48d1-b6a6-9e755aff4476",
            () => HttpResponse.json(magdaRecord1)
          ),
          http.get(
            "https://magda.example.com/api/v0/registry/records/bfc69476-1c85-4208-9046-4f736bab9b8e",
            () => HttpResponse.json(magdaRecord2)
          ),
          http.get(
            "https://magda.example.com/api/v0/registry/records/12f26f07-f39e-4753-979d-2de01af54bd1",
            () => HttpResponse.json(magdaRecord3)
          ),
          http.get(
            "https://magda.example.com/api/v0/registry/records/map-config-example",
            () => {
              configRequestCount++;
              if (configRequestCount === 1) {
                return HttpResponse.json(mapConfigOld);
              } else if (configRequestCount === 2) {
                return HttpResponse.json(mapConfigNew);
              }
              // Don't allow more requests to configUrl once Terrias are set up
              return HttpResponse.error();
            }
          )
        );

        await terria.start({
          configUrl,
          i18nOptions
        });

        await newTerria.start({
          configUrl,
          i18nOptions
        });
      });

      it("correctly applies user stratum changes to moved item", async function () {
        const oldGroupRef = terria.getModelById(
          MagdaReference,
          "6b24aa39-1aa7-48d1-b6a6-9e755aff4476"
        );
        expect(oldGroupRef).toBeDefined(
          "Can't find Old group reference in source terria"
        );
        if (oldGroupRef === undefined) return;
        await oldGroupRef.loadReference();
        expect(oldGroupRef.target).toBeDefined(
          "Can't dereference Old group in source terria"
        );

        const csv = terria.getModelById(
          CsvCatalogItem,
          "3432284e-a111-4844-97c8-26a1767f9986"
        );
        expect(csv).toBeDefined("Can't dereference csv in source terria");
        if (csv === undefined) return;
        csv.setTrait(CommonStrata.user, "opacity", 0.5);
        const shareLink = buildShareLink(terria, viewState);

        // Hack to make below test succeed. This needs to be there until we add a magda API that can locate any
        //  item by ID or share key within a Terria catalog
        // Loads "New group" (bfc69476-1c85-4208-9046-4f736bab9b8e) which registers shareKeys for
        //  "Extra group" (12f26f07-f39e-4753-979d-2de01af54bd1). And "Extra group" has a share key
        //  that matches the ancestor of the serialised Random CSV, so loading is triggered on "Extra group"
        //  followed by 3432284e-a111-4844-97c8-26a1767f9986 which points to "My random CSV"
        //  (decfc787-0425-4175-a98c-a40db064feb3)
        const newGroupRef = newTerria.getModelById(
          MagdaReference,
          "bfc69476-1c85-4208-9046-4f736bab9b8e"
        );
        if (newGroupRef === undefined) return;
        await newGroupRef.loadReference();

        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();

        // Why does this return a CSV item (when above hack isn't added)? It returns a brand new csv item without data or URL
        // Does serialisation save enough attributes that upsertModelFromJson thinks it can create a new model?
        // upsertModelFromJson should really be replaced with update + insert functions
        // But is it always easy to work out when share data should use update and when it should insert?
        // E.g. user added models should be inserted when deserialised, not updated
        const newCsv = newTerria.getModelByIdOrShareKey(
          CsvCatalogItem,
          "3432284e-a111-4844-97c8-26a1767f9986"
        );
        expect(newCsv).toBeDefined(
          "Can't find newCsv item in destination newTerria"
        );

        expect(newCsv?.uniqueId).toBe(
          "decfc787-0425-4175-a98c-a40db064feb3",
          "Failed to map share key to correct model"
        );
        expect(newCsv?.opacity).toBe(0.5);
      });

      it("correctly adds moved item to workbench and timeline", async function () {
        const oldGroupRef = terria.getModelById(
          MagdaReference,
          "6b24aa39-1aa7-48d1-b6a6-9e755aff4476"
        );
        expect(oldGroupRef).toBeDefined(
          "Can't find Old group reference in source terria"
        );
        if (oldGroupRef === undefined) return;
        await oldGroupRef.loadReference();
        expect(oldGroupRef.target).toBeDefined(
          "Can't dereference Old group in source terria"
        );

        const csv = terria.getModelById(
          CsvCatalogItem,
          "3432284e-a111-4844-97c8-26a1767f9986"
        );
        expect(csv).toBeDefined("Can't dereference csv in source terria");
        if (csv === undefined) return;
        await terria.workbench.add(csv);
        terria.timelineStack.addToTop(csv);

        const shareLink = buildShareLink(terria, viewState);

        // Hack to make below test succeed. Needs to be there until we add a magda API that can locate any
        //  item by ID or share key within a Terria catalog
        // Loads "New group" (bfc69476-1c85-4208-9046-4f736bab9b8e) which registers shareKeys for
        //  "Extra group" (12f26f07-f39e-4753-979d-2de01af54bd1). And "Extra group" has a share key
        //  that matches the ancestor of the serialised Random CSV, so loading is triggered on "Extra group"
        //  followed by 3432284e-a111-4844-97c8-26a1767f9986 which points to "My random CSV"
        //  (decfc787-0425-4175-a98c-a40db064feb3)
        const newGroupRef = newTerria.getModelById(
          MagdaReference,
          "bfc69476-1c85-4208-9046-4f736bab9b8e"
        );
        if (newGroupRef === undefined) return;
        await newGroupRef.loadReference();

        await newTerria.updateApplicationUrl(shareLink);
        await newTerria.loadInitSources();

        // Why does this return a CSV item (when above hack isn't added)? It returns a brand new csv item without data or URL
        // Does serialisation save enough attributes that upsertModelFromJson thinks it can create a new model?
        // upsertModelFromJson should really be replaced with update + insert functions
        // But is it always easy to work out when share data should use update and when it should insert?
        // E.g. user added models should be inserted when deserialised, not updated
        const newCsv = newTerria.getModelByIdOrShareKey(
          CsvCatalogItem,
          "3432284e-a111-4844-97c8-26a1767f9986"
        );
        expect(newCsv).toBeDefined(
          "Can't find newCsv item in destination newTerria"
        );
        if (newCsv === undefined) return;

        expect(newCsv.uniqueId).toBe(
          "decfc787-0425-4175-a98c-a40db064feb3",
          "Failed to map share key to correct model"
        );
        expect(newTerria.workbench.contains(newCsv)).toBeTruthy(
          "newCsv not found in destination newTerria workbench"
        );
        expect(newTerria.timelineStack.contains(newCsv)).toBeTruthy(
          "newCsv not found in destination newTerria timeline"
        );
      });
    });
  });

  describe("proxyConfiguration", function () {
    beforeEach(function () {
      worker.use(
        http.get("*test/init/configProxy*", () =>
          HttpResponse.json(configProxy)
        ),
        http.get("*/serverconfig/*", () => HttpResponse.json(serverConfig))
      );
    });

    it("initializes proxy with parameters from config file", async function () {
      await terria.start({
        configUrl: "test/init/configProxy.json",
        i18nOptions
      });

      expect(terria.corsProxy.baseProxyUrl).toBe("/myproxy/");
      expect(terria.corsProxy.proxyDomains).toEqual([
        "example.com",
        "csiro.au"
      ]);
    });
  });

  describe("removeModelReferences", function () {
    let model: SimpleCatalogItem;
    beforeEach(function () {
      model = new SimpleCatalogItem("testId", terria);
      terria.addModel(model);
    });

    it("removes the model from workbench", async function () {
      await terria.workbench.add(model);
      terria.removeModelReferences(model);
      expect(terria.workbench).not.toContain(model);
    });

    it(
      "it removes picked features & selected feature for the model",
      action(function () {
        terria.pickedFeatures = new PickedFeatures();
        const feature = new TerriaFeature({});
        terria.selectedFeature = feature;
        feature._catalogItem = model;
        terria.pickedFeatures.features.push(feature);
        terria.removeModelReferences(model);
        expect(terria.pickedFeatures.features.length).toBe(0);
        expect(terria.selectedFeature).toBeUndefined();
      })
    );

    it("unregisters the model from Terria", function () {
      terria.removeModelReferences(model);
      expect(terria.getModelById(BaseModel, "testId")).toBeUndefined();
    });
  });

  //   it("tells us there's a time enabled WMS with `checkNowViewingForTimeWms()`", function(done) {
  //     terria
  //       .start({
  //         configUrl: "test/init/configProxy.json",
  //         i18nOptions
  //       })
  //       .then(function() {
  //         expect(terria.checkNowViewingForTimeWms()).toEqual(false);
  //       })
  //       .then(function() {
  //         const wmsItem = new WebMapServiceCatalogItem(terria);
  //         wmsItem.updateFromJson({
  //           url: "http://example.com",
  //           metadataUrl: "test/WMS/comma_sep_datetimes_inherited.xml",
  //           layers: "13_intervals",
  //           dataUrl: "" // to prevent a DescribeLayer request
  //         });
  //         wmsItem
  //           .load()
  //           .then(function() {
  //             terria.nowViewing.add(wmsItem);
  //             expect(terria.checkNowViewingForTimeWms()).toEqual(true);
  //           })
  //           .then(done)
  //           .catch(done.fail);
  //       })
  //       .catch(done.fail);
  //   });

  describe("applyInitData", function () {
    describe("when pickedFeatures is not present in initData", function () {
      it("unsets the feature picking state if `canUnsetFeaturePickingState` is `true`", async function () {
        terria.pickedFeatures = new PickedFeatures();
        terria.selectedFeature = new Entity({
          name: "selected"
        }) as TerriaFeature;
        await terria.applyInitData({
          initData: {},
          canUnsetFeaturePickingState: true
        });
        expect(terria.pickedFeatures).toBeUndefined();
        expect(terria.selectedFeature).toBeUndefined();
      });

      it("otherwise, should not unset feature picking state", async function () {
        terria.pickedFeatures = new PickedFeatures();
        terria.selectedFeature = new Entity({
          name: "selected"
        }) as TerriaFeature;
        await terria.applyInitData({
          initData: {}
        });
        expect(terria.pickedFeatures).toBeDefined();
        expect(terria.selectedFeature).toBeDefined();
      });
    });

    describe("Sets workbench contents correctly", function () {
      const mapServerSimpleGroupUrl =
        "http://some.service.gov.au/arcgis/rest/services/mapServerSimpleGroup/MapServer";
      const mapServerWithErrorUrl =
        "http://some.service.gov.au/arcgis/rest/services/mapServerWithError/MapServer";
      const magdaRecordFeatureServerGroupUrl =
        "http://magda.reference.group.service.gov.au";
      const magdaRecordDerefencedToWmsUrl =
        "http://magda.references.wms.gov.au";

      const mapServerGroupModel = {
        type: "esri-mapServer-group",
        name: "A simple map server group",
        url: mapServerSimpleGroupUrl,
        id: "a-test-server-group"
      };

      const magdaRecordDerefencedToFeatureServerGroup = {
        type: "magda",
        name: "A magda record derefenced to a simple feature server group",
        url: magdaRecordFeatureServerGroupUrl,
        recordId: "magda-record-id-dereferenced-to-feature-server-group",
        id: "a-test-magda-record"
      };

      const magdaRecordDerefencedToWms = {
        type: "magda",
        name: "A magda record derefenced to wms",
        url: magdaRecordDerefencedToWmsUrl,
        recordId: "magda-record-id-dereferenced-to-wms",
        id: "another-test-magda-record"
      };

      const mapServerModelWithError = {
        type: "esri-mapServer-group",
        name: "A map server with error",
        url: mapServerWithErrorUrl,
        id: "a-test-server-with-error"
      };

      const theOrderedItemsIds = [
        "a-test-server-group/0",
        "a-test-magda-record/0",
        "another-test-magda-record"
      ];

      let loadMapItemsWms: any = undefined;
      let loadMapItemsArcGisMap: any = undefined;
      let loadMapItemsArcGisFeature: any = undefined;
      beforeEach(function () {
        worker.use(
          // MapServer group metadata
          http.get(
            "http://some.service.gov.au/arcgis/rest/services/mapServerSimpleGroup/MapServer",
            () => HttpResponse.json(mapServerSimpleGroupJson)
          ),
          http.get(
            "http://some.service.gov.au/arcgis/rest/services/mapServerWithError/MapServer",
            () => HttpResponse.json(mapServerWithErrorJson)
          ),
          // Magda registry records
          http.get(
            "http://magda.reference.group.service.gov.au/api/v0/registry/records/:recordId",
            () => HttpResponse.json(magdaGroupRecordJson)
          ),
          http.get(
            "http://magda.references.wms.gov.au/api/v0/registry/records/:recordId",
            () => HttpResponse.json(magdaWmsRecordJson)
          ),
          // Dereferenced service endpoints
          http.get(
            "https://services2.arcgis.com/iCBB4zKDwkw2iwDD/arcgis/rest/services/Forest_Management_Zones/FeatureServer",
            () => HttpResponse.json(esriFeatureServerJson)
          ),
          http.get(
            "https://mapprod1.environment.nsw.gov.au/arcgis/services/VIS/Vegetation_SouthCoast_SCIVI_V14_E_2230/MapServer/WMSServer",
            () => HttpResponse.xml(wmsCapabilitiesXml)
          )
        );

        // Do not call through.
        loadMapItemsArcGisMap = spyOn(
          ArcGisMapServerCatalogItem.prototype,
          "loadMapItems"
        ).and.callFake(() => Promise.resolve(Result.none()));
        loadMapItemsArcGisFeature = spyOn(
          ArcGisFeatureServerCatalogItem.prototype,
          "loadMapItems"
        ).and.callFake(() => Promise.resolve(Result.none()));
        loadMapItemsWms = spyOn(
          WebMapServiceCatalogItem.prototype,
          "loadMapItems"
        ).and.callFake(() => Promise.resolve(Result.none()));
      });

      it("when a workbench item is a simple map server group", async function () {
        await terria.applyInitData({
          initData: {
            catalog: [mapServerGroupModel],
            workbench: ["a-test-server-group"]
          }
        });
        expect(terria.workbench.itemIds).toEqual(["a-test-server-group/0"]);
        expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
      });

      it("when a workbench item is a referenced map server group", async function () {
        await terria.applyInitData({
          initData: {
            catalog: [magdaRecordDerefencedToFeatureServerGroup],
            workbench: ["a-test-magda-record"]
          }
        });
        expect(terria.workbench.itemIds).toEqual(["a-test-magda-record/0"]);
        expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
      });

      it("when a workbench item is a referenced wms", async function () {
        await terria.applyInitData({
          initData: {
            catalog: [magdaRecordDerefencedToWms],
            workbench: ["another-test-magda-record"]
          }
        });
        expect(terria.workbench.itemIds).toEqual(["another-test-magda-record"]);
        expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
      });

      it("when the workbench has more than one items", async function () {
        await terria.applyInitData({
          initData: {
            catalog: [
              mapServerGroupModel,
              magdaRecordDerefencedToFeatureServerGroup,
              magdaRecordDerefencedToWms
            ],
            workbench: [
              "a-test-server-group",
              "a-test-magda-record",
              "another-test-magda-record"
            ]
          }
        });

        expect(terria.workbench.itemIds).toEqual(theOrderedItemsIds);
        expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
        expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
        expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
      });

      it("when the workbench has an unknown item", async function () {
        await terria.applyInitData({
          initData: {
            catalog: [
              mapServerGroupModel,
              magdaRecordDerefencedToFeatureServerGroup,
              magdaRecordDerefencedToWms
            ],
            workbench: [
              "id_of_unknown_model",
              "a-test-server-group",
              "a-test-magda-record",
              "another-test-magda-record"
            ]
          }
        });

        expect(terria.workbench.itemIds).toEqual(theOrderedItemsIds);
        expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
        expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
        expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
      });

      it("when a workbench item has errors", async function () {
        let error: TerriaError | undefined = undefined;
        try {
          await terria.applyInitData({
            initData: {
              catalog: [
                mapServerModelWithError,
                mapServerGroupModel,
                magdaRecordDerefencedToFeatureServerGroup,
                magdaRecordDerefencedToWms
              ],
              workbench: [
                "a-test-server-with-error",
                "a-test-server-group",
                "a-test-magda-record",
                "another-test-magda-record"
              ]
            }
          });
        } catch (e) {
          error = e as TerriaError;
          expect(error.message === "models.terria.loadingInitSourceErrorTitle");
        } finally {
          expect(error).not.toEqual(undefined);
          expect(terria.workbench.itemIds).toEqual(theOrderedItemsIds);
          expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
          expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
          expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
        }
      });
    });

    describe("Enable/disable shorten share URL via init data", function () {
      beforeEach(function () {
        window.localStorage.clear();
      });

      it("should not change local property shortenShareUrls", async function () {
        await terria.applyInitData({
          initData: {}
        });
        expect(terria.getLocalProperty("shortenShareUrls")).toBe(null);

        terria.setLocalProperty("shortenShareUrls", true);
        await terria.applyInitData({
          initData: {}
        });
        expect(terria.getLocalProperty("shortenShareUrls")).toBeTruthy();

        terria.setLocalProperty("shortenShareUrls", false);
        await terria.applyInitData({
          initData: {}
        });
        expect(terria.getLocalProperty("shortenShareUrls")).toBeFalsy();
      });

      it("should set local property shortenShareUrls to true", async function () {
        await terria.applyInitData({
          initData: {
            settings: {
              shortenShareUrls: true
            }
          }
        });
        expect(terria.getLocalProperty("shortenShareUrls")).toBeTruthy();
      });

      it("should set local property shortenShareUrls to false", async function () {
        await terria.applyInitData({
          initData: {
            settings: {
              shortenShareUrls: false
            }
          }
        });
        expect(terria.getLocalProperty("shortenShareUrls")).toBeFalsy();

        terria.setLocalProperty("shortenShareUrls", true);
        await terria.applyInitData({
          initData: {
            settings: {
              shortenShareUrls: false
            }
          }
        });
        expect(terria.getLocalProperty("shortenShareUrls")).toBeFalsy();
      });
    });
  });

  describe("mapSettings", function () {
    it("properly interprets map hash parameter", async () => {
      const getLocalPropertySpy = spyOn(terria, "getLocalProperty");
      const location = {
        href: "http://test.com/#map=2d"
      } as Location;
      await terria.start({
        configUrl: "test-config.json",
        applicationUrl: location
      });
      terria.loadPersistedMapSettings();
      expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Leaflet);
      expect(getLocalPropertySpy).not.toHaveBeenCalledWith("viewermode");
    });

    it("properly resolves persisted map viewer", async () => {
      const getLocalPropertySpy = spyOn(
        terria,
        "getLocalProperty"
      ).and.returnValue("2d");
      await terria.start({ configUrl: "test-config.json" });
      terria.loadPersistedMapSettings();
      expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Leaflet);
      expect(getLocalPropertySpy).toHaveBeenCalledWith("viewermode");
    });

    it("properly interprets wrong map hash parameter and resolves persisted value", async () => {
      const getLocalPropertySpy = spyOn(
        terria,
        "getLocalProperty"
      ).and.returnValue("3dsmooth");
      const location = {
        href: "http://test.com/#map=4d"
      } as Location;
      await terria.start({
        configUrl: "test-config.json",
        applicationUrl: location
      });
      terria.loadPersistedMapSettings();
      expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Cesium);
      expect(terria.mainViewer.viewerOptions.useTerrain).toBe(false);
      expect(getLocalPropertySpy).toHaveBeenCalledWith("viewermode");
    });

    it("uses `settings` in initsource", async () => {
      const setBaseMapSpy = spyOn(terria.mainViewer, "setBaseMap");

      await terria.start({ configUrl: "test-config.json" });

      await terria.applyInitData({
        initData: {
          settings: {
            baseMaximumScreenSpaceError: 1,
            useNativeResolution: true,
            alwaysShowTimeline: true,
            baseMapId: "basemap-natural-earth-II",
            terrainSplitDirection: -1,
            depthTestAgainstTerrainEnabled: true
          }
        }
      });

      await terria.loadInitSources();

      expect(terria.baseMaximumScreenSpaceError).toBe(1);
      expect(terria.useNativeResolution).toBeTruthy();
      expect(terria.timelineStack.alwaysShowingTimeline).toBeTruthy();
      expect(setBaseMapSpy).toHaveBeenCalledWith(
        terria.baseMapsModel.baseMapItems.find(
          (item) => item.item.uniqueId === "basemap-natural-earth-II"
        )?.item
      );

      expect(terria.terrainSplitDirection).toBe(SplitDirection.LEFT);
      expect(terria.depthTestAgainstTerrainEnabled).toBeTruthy();
    });
  });

  describe("basemaps", function () {
    it("when no base maps are specified load defaultBaseMaps", async function () {
      await terria.start({ configUrl: "test-config.json" });
      await terria.applyInitData({
        initData: {}
      });
      await terria.loadInitSources();
      const _defaultBaseMaps = defaultBaseMaps(terria);
      expect(terria.baseMapsModel).toBeDefined();
      expect(terria.baseMapsModel.baseMapItems.length).toBe(
        _defaultBaseMaps.length
      );
    });

    it("correctly loads the base maps", async function () {
      await terria.start({ configUrl: "test-config.json" });
      await (
        await terria._applyInitData({
          initData: {
            settings: { baseMapId: "basemap-2" },
            baseMaps: {
              items: [
                {
                  item: {
                    id: "basemap-natural-earth-II",
                    name: "Natural Earth II",
                    type: "url-template-imagery",
                    url: "https://storage.googleapis.com/terria-datasets-public/basemaps/natural-earth-tiles/{z}/{x}/{reverseY}.png",
                    attribution:
                      "<a href='https://www.naturalearthdata.com/downloads/10m-raster-data/10m-natural-earth-2/'>Natural Earth II</a> - From Natural Earth. <a href='https://www.naturalearthdata.com/about/terms-of-use/'>Public Domain</a>.",
                    maximumLevel: 7,
                    opacity: 1.0
                  },
                  image: "build/TerriaJS/images/natural-earth.png",
                  contrastColor: "#000000"
                },
                {
                  item: {
                    id: "basemap-2",
                    name: "Base map 2",
                    type: "url-template-imagery",
                    url: "https://example.com"
                  }
                }
              ]
            }
          }
        })
      ).baseMapPromise;
      const _defaultBaseMaps = defaultBaseMaps(terria);
      expect(terria.baseMapsModel).toBeDefined();
      expect(terria.baseMapsModel.baseMapItems.length).toEqual(
        _defaultBaseMaps.length + 1
      );
      expect(terria.mainViewer.baseMap?.uniqueId).toBe("basemap-2");
    });
  });

  describe("loadPickedFeatures", function () {
    let container: HTMLElement;
    beforeEach(async function () {
      // Attach cesium viewer and wait for it to be loaded
      container = document.createElement("div");
      document.body.appendChild(container);
      terria.mainViewer.viewerOptions.useTerrain = false;
      terria.mainViewer.attach(container);
      return terria.mainViewer.viewerLoadPromise;
    });

    afterEach(() => {
      terria.mainViewer.destroy();
      document.body.removeChild(container);
    });

    it("sets the pickCoords", async function () {
      const Cesium = (await import("../../lib/Models/Cesium")).default;
      expect(terria.currentViewer instanceof Cesium).toBeTruthy();
      await terria.loadPickedFeatures({
        pickCoords: {
          lat: 84.93,
          lng: 77.91,
          height: -5400810.41
        },
        providerCoords: {
          "https://foo": { x: 123, y: 456, level: 7 },
          "https://bar": { x: 42, y: 42, level: 4 }
        }
      });
      const pickPosition = terria.pickedFeatures?.pickPosition;
      expect(pickPosition).toBeDefined();
      if (pickPosition) {
        const { x, y, z } = pickPosition;
        expect(x.toFixed(2)).toBe("18483.85");
        expect(y.toFixed(2)).toBe("86292.94");
        expect(z.toFixed(2)).toBe("952035.13");
      }
    });

    it("sets the selectedFeature", async function () {
      const testItem = new SimpleCatalogItem("test", terria);
      const ds = new CustomDataSource("ds");
      const entity = new Entity({ name: "foo" });
      ds.entities.add(entity);
      testItem.mapItems = [ds];
      await terria.workbench.add(testItem);
      const entityHash = hashEntity(entity, terria);
      await terria.loadPickedFeatures({
        pickCoords: {
          lat: 84.93,
          lng: 77.91,
          height: -5400810.41
        },
        providerCoords: {
          "https://foo": { x: 123, y: 456, level: 7 },
          "https://bar": { x: 42, y: 42, level: 4 }
        },
        entities: [
          {
            hash: entityHash,
            name: "foo"
          }
        ],
        current: {
          hash: entityHash,
          name: "foo"
        }
      });
      expect(terria.selectedFeature).toBeDefined();
      expect(terria.selectedFeature?.name).toBe("foo");
    });
  });

  it("customRequestSchedulerLimits sets RequestScheduler limits for domains", async function () {
    const configUrl = `data:application/json;base64,${btoa(
      JSON.stringify({
        initializationUrls: [],
        parameters: {
          customRequestSchedulerLimits: {
            "test.domain:333": 12
          }
        }
      })
    )}`;
    await terria.start({ configUrl, i18nOptions });
    expect(RequestScheduler.requestsByServer["test.domain:333"]).toBe(12);
  });

  describe("initial zoom", function () {
    describe("behaviour of `initialCamera.focusWorkbenchItems`", function () {
      let container: HTMLElement;

      beforeEach(function () {
        // Attach cesium viewer and wait for it to be loaded
        container = document.createElement("div");
        document.body.appendChild(container);
        terria.mainViewer.viewerOptions.useTerrain = false;
        terria.mainViewer.attach(container);

        const configJson = {
          initializationUrls: ["focus-workbench-items.json"]
        };

        // An init source with a pre-loaded workbench item
        const initJson = {
          initialCamera: { focusWorkbenchItems: true },
          catalog: [
            {
              id: "points",
              type: "geojson",
              name: "Points",
              geoJsonData: {
                type: "Feature",
                bbox: [-10.0, -10.0, 10.0, 10.0],
                properties: {
                  foo: "hi",
                  bar: "bye"
                },
                geometry: {
                  type: "Polygon",
                  coordinates: [
                    [
                      [100.0, 0.0],
                      [101.0, 0.0],
                      [101.0, 1.0],
                      [100.0, 1.0],
                      [100.0, 0.0]
                    ],
                    [
                      [100.2, 0.2],
                      [100.8, 0.2],
                      [100.8, 0.8],
                      [100.2, 0.8],
                      [100.2, 0.2]
                    ]
                  ]
                }
              }
            }
          ],
          workbench: ["points"],
          baseMaps: {
            enabledBaseMaps: []
          }
        };
        worker.use(
          http.get("*/serverconfig/*", () => HttpResponse.json({})),
          http.get("*/test-config.json", () => HttpResponse.json(configJson)),
          http.get("*/focus-workbench-items.json", () =>
            HttpResponse.json(initJson)
          )
        );
      });

      afterEach(() => {
        terria.mainViewer.destroy();
        document.body.removeChild(container);
      });

      it("zooms the map to focus on the workbench items", async function () {
        await terria.start({ configUrl: "test-config.json" });
        await terria.loadInitSources();
        await when(() => terria.currentViewer.type === "Cesium");

        const cameraPos = terria.cesium?.scene.camera.positionCartographic;
        expect(cameraPos).toBeDefined();
        const { longitude, latitude, height } = cameraPos!;
        expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(100.5);
        expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(0.5);
        expect(height).toBeCloseTo(191276.7939);
      });

      it("works correctly even when there is a delay in a Cesium/Leaflet viewer becoming available", async function () {
        // Start with NoViewer
        runInAction(() => {
          terria.mainViewer.viewerMode = undefined;
        });
        await terria.start({ configUrl: "test-config.json" });
        await terria.loadInitSources();
        expect(terria.currentViewer.type).toEqual("none");
        // Switch to Cesium viewer
        runInAction(() => {
          terria.mainViewer.viewerMode = ViewerMode.Cesium;
        });
        // Wait for the switch to happen
        await when(() => terria.mainViewer.currentViewer.type === "Cesium");
        // Ensure that the camera position is correctly updated after the switch
        const cameraPos = terria.cesium?.scene.camera.positionCartographic;
        const { longitude, latitude, height } = cameraPos!;
        expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(100.5);
        expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(0.5);
        expect(height).toBeCloseTo(191276.7939);
      });

      it("is not applied if subsequent init sources override the initialCamera settings", async function () {
        await terria.start({ configUrl: "test-config.json" });
        terria.initSources.push({
          data: {
            initialCamera: {
              west: 42,
              east: 44,
              north: 44,
              south: 42,
              zoomDuration: 0
            }
          }
        });

        // Terria uses a 2 second flight duration when zooming to CameraView.
        // Here we re-define zoomTo() to ignore duration and zoom to the target
        // immediately so that we can observe the effects without delay.
        const originalZoomTo = terria.currentViewer.zoomTo.bind(
          terria.currentViewer
        );
        terria.currentViewer.zoomTo = (target, _duration) =>
          originalZoomTo(target, 0.0);

        await terria.loadInitSources();
        await when(() => terria.currentViewer.type === "Cesium");

        const cameraPos = terria.cesium?.scene.camera.positionCartographic;
        expect(cameraPos).toBeDefined();
        const { longitude, latitude, height } = cameraPos!;
        expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(43);
        expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(43);
        expect(height).toBeCloseTo(384989.3092);
      });

      it("is not applied when share URL specifies a different initialCamera setting", async function () {
        // Terria uses a 2 second flight duration when zooming to CameraView.
        // Here we re-define zoomTo() to ignore duration and zoom to the target
        // immediately so that we can observe the effects without delay.
        await when(() => terria.currentViewer.type === "Cesium");
        const originalZoomTo = terria.currentViewer.zoomTo.bind(
          terria.currentViewer
        );
        terria.currentViewer.zoomTo = (target, _duration) =>
          originalZoomTo(target, 0.0);

        await terria.start({
          configUrl: "test-config.json",
          applicationUrl: {
            // A share URL with a different `initialCamera` setting
            href: "http://localhost:3001/#start=%7B%22version%22%3A%228.0.0%22%2C%22initSources%22%3A%5B%7B%22stratum%22%3A%22user%22%2C%22initialCamera%22%3A%7B%22east%22%3A80.48324442836365%2C%22west%22%3A74.16912021554141%2C%22north%22%3A10.82936711956377%2C%22south%22%3A7.882086009700934%7D%2C%22workbench%22%3A%5B%22points%22%5D%7D%5D%7D"
          } as Location
        });

        await terria.loadInitSources();
        await when(() => terria.currentViewer.type === "Cesium");

        const cameraPos = terria.cesium?.scene.camera.positionCartographic;
        expect(cameraPos).toBeDefined();
        const { longitude, latitude, height } = cameraPos!;
        expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(77.3261);
        expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(9.3557);
        expect(height).toBeCloseTo(591140.7251);
      });
    });
  });
});
