// Note: this doesn't check if the client already exists, and clobbers any existing client.
private async createVizierClient(cluster: ClusterConfig) {
    const { ipAddress, token } = await this.gqlClient.getClusterConnection(cluster.id, true);
    const client = new VizierGRPCClient(
      cluster.passthroughClusterAddress ?? ipAddress,
      // If in embed mode, we should always use the auth token with bearer auth.
      this.options.authToken ? this.options.authToken : token,
      cluster.id,
      (this.options.authToken ? false : cluster.attachCredentials ?? false),
    );

    // Note that this doesn't currently clean up clients that haven't been used in a while, so a particularly long
    // user session could hold onto a large number of stale connections. A simple page refresh drops them all.
    // If this becomes a problem, limit the number of clients and rotate out those that haven't been used in a while.
    this.clusterConnections.set(cluster.id, client);
    return client;
  }


private async getClusterClient(cluster: string | ClusterConfig) {
    let id: string;
    let passthroughClusterAddress: string;
    let attachCredentials = false;

    if (typeof cluster === 'string') {
      id = cluster;
    } else {
      ({ id, passthroughClusterAddress, attachCredentials } = cluster);
    }

    return this.clusterConnections.has(id)
      ? Promise.resolve(this.clusterConnections.get(id))
      : this.createVizierClient({ id, passthroughClusterAddress, attachCredentials });
  }


// TODO(nick): Once the authentication model settles down, make this easier to use outside of the browser.
/**
   * Checks whether the current cookies include a valid authentication token.
   * Checks by querying a purpose-built endpoint, to be certain the user really is authenticated.
   */
isAuthenticated(): Promise<boolean> {
    return fetch(`${this.options.uri}/authorized`,
      {
        headers: {
          ...{
            'x-csrf': GetCSRFCookie(),
          },
          ...(this.options.authToken
            ? { authorization: `Bearer ${this.options.authToken}`, 'X-Use-Bearer': 'true' } : {}),
        },
      }).then((response) => response.status === 200);
  }


/**
   * Creates a stream that listens for the health of the cluster and the API client's connection to it.
   * This is an Observable, so don't forget to unsubscribe when you're done with it.
   * @param cluster Which cluster to use. Either just its ID, or a full config. If that cluster has previously been
   *        connected in this session, that connection will be reused without changing its configuration.
   */
health(cluster: string | ClusterConfig): Observable<Status> {
    return from(this.getClusterClient(cluster))
      .pipe(switchMap((client) => client.health()));
  }


/**
   * Asks a connected cluster to run a PxL script. Returns an event stream that updates at each stage of execution.
   *
   * A typical event stream might look something like this:
   * - start
   * - metadata
   * - mutation info (if the script uses tracepoints)
   * - data
   * - stats
   *
   * @param cluster Which cluster to use. Either just its ID, or a full config. If that cluster has previously been
   *        connected in this session, that connection will be reused without changing its configuration.
   * @param script The source code of the script to be compiled and executed; whitespace and all.
   * @param opts Any extra options, such as encryption.
   * @param funcs Descriptions of which functions in the script to run, and what to do with their output.
   */
executeScript(
    cluster: string | ClusterConfig,
    script: string,
    opts: ExecuteScriptOptions,
    funcs: VizierQueryFunc[] = [],
  ): Observable<ExecutionStateUpdate> {
    const hasMutation = containsMutation(script);
    return from(this.getClusterClient(cluster))
      .pipe(switchMap((client) => client.executeScript(script, funcs, hasMutation, opts)));
  }


/**
   * Implementation detail for adapters.
   * Do not use directly unless writing such an adapter.
   *
   * Provides the internal CloudClient, which has a graphQL property.
   * That property is an ApolloClient, with which GraphQL queries can be run directly.
   *
   * @internal
   */
getCloudClient(): CloudClient {
    return this.gqlClient;
  }


() => {
  describe('VDomModel', () => {
    describe('#constructor()', () => {
      it('should create a VDomModel', () => {
        const model = new VDomModel();
        expect(model).to.be.an.instanceof(VDomModel);
      });

      it('should create a TestModel', () => {
        const model = new TestModel();
        expect(model).to.be.an.instanceof(TestModel);
      });

      it('should be properly disposed', () => {
        const model = new TestModel();
        model.dispose();
        expect(model.isDisposed).to.be.equal(true);
      });
    });

    describe('#stateChanged()', () => {
      it('should fire the stateChanged signal on a change', () => {
        const model = new TestModel();
        let changed = false;
        model.stateChanged.connect(() => {
          changed = true;
        });
        model.value = 'newvalue';
        expect(changed).to.equal(true);
      });
    });
  });

  describe('VDomRenderer', () => {
    describe('#constructor()', () => {
      it('should create a TestWidget', () => {
        const widget = new TestWidget();
        expect(widget).to.be.an.instanceof(TestWidget);
      });

      it('should be properly disposed', () => {
        const widget = new TestWidget();
        widget.dispose();
        expect(widget.isDisposed).to.equal(true);
      });
    });

    describe('#modelChanged()', () => {
      it('should fire the stateChanged signal on a change', () => {
        const widget = new TestWidget();
        const model = new TestModel();
        let changed = false;
        widget.modelChanged.connect(() => {
          changed = true;
        });
        widget.model = model;
        expect(changed).to.equal(true);
      });
    });

    describe('#render()', () => {
      it('should render the contents after a model change', async () => {
        const widget = new TestWidget();
        const model = new TestModel();
        widget.model = model;
        model.value = 'foo';
        await framePromise();
        let span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('foo');
      });
    });

    describe('#noModel()', () => {
      it('should work with a null model', async () => {
        const widget = new TestWidgetNoModel();
        Widget.attach(widget, document.body);
        await framePromise();
        const span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('No model!');
      });
    });
  });
}


() => {
    describe('#constructor()', () => {
      it('should create a VDomModel', () => {
        const model = new VDomModel();
        expect(model).to.be.an.instanceof(VDomModel);
      });

      it('should create a TestModel', () => {
        const model = new TestModel();
        expect(model).to.be.an.instanceof(TestModel);
      });

      it('should be properly disposed', () => {
        const model = new TestModel();
        model.dispose();
        expect(model.isDisposed).to.be.equal(true);
      });
    });

    describe('#stateChanged()', () => {
      it('should fire the stateChanged signal on a change', () => {
        const model = new TestModel();
        let changed = false;
        model.stateChanged.connect(() => {
          changed = true;
        });
        model.value = 'newvalue';
        expect(changed).to.equal(true);
      });
    });
  }


() => {
      it('should create a VDomModel', () => {
        const model = new VDomModel();
        expect(model).to.be.an.instanceof(VDomModel);
      });

      it('should create a TestModel', () => {
        const model = new TestModel();
        expect(model).to.be.an.instanceof(TestModel);
      });

      it('should be properly disposed', () => {
        const model = new TestModel();
        model.dispose();
        expect(model.isDisposed).to.be.equal(true);
      });
    }


() => {
        const model = new VDomModel();
        expect(model).to.be.an.instanceof(VDomModel);
      }


() => {
        const model = new TestModel();
        expect(model).to.be.an.instanceof(TestModel);
      }


() => {
        const model = new TestModel();
        model.dispose();
        expect(model.isDisposed).to.be.equal(true);
      }


() => {
      it('should fire the stateChanged signal on a change', () => {
        const model = new TestModel();
        let changed = false;
        model.stateChanged.connect(() => {
          changed = true;
        });
        model.value = 'newvalue';
        expect(changed).to.equal(true);
      });
    }


() => {
        const model = new TestModel();
        let changed = false;
        model.stateChanged.connect(() => {
          changed = true;
        });
        model.value = 'newvalue';
        expect(changed).to.equal(true);
      }


() => {
          changed = true;
        }


() => {
    describe('#constructor()', () => {
      it('should create a TestWidget', () => {
        const widget = new TestWidget();
        expect(widget).to.be.an.instanceof(TestWidget);
      });

      it('should be properly disposed', () => {
        const widget = new TestWidget();
        widget.dispose();
        expect(widget.isDisposed).to.equal(true);
      });
    });

    describe('#modelChanged()', () => {
      it('should fire the stateChanged signal on a change', () => {
        const widget = new TestWidget();
        const model = new TestModel();
        let changed = false;
        widget.modelChanged.connect(() => {
          changed = true;
        });
        widget.model = model;
        expect(changed).to.equal(true);
      });
    });

    describe('#render()', () => {
      it('should render the contents after a model change', async () => {
        const widget = new TestWidget();
        const model = new TestModel();
        widget.model = model;
        model.value = 'foo';
        await framePromise();
        let span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('foo');
      });
    });

    describe('#noModel()', () => {
      it('should work with a null model', async () => {
        const widget = new TestWidgetNoModel();
        Widget.attach(widget, document.body);
        await framePromise();
        const span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('No model!');
      });
    });
  }


() => {
      it('should create a TestWidget', () => {
        const widget = new TestWidget();
        expect(widget).to.be.an.instanceof(TestWidget);
      });

      it('should be properly disposed', () => {
        const widget = new TestWidget();
        widget.dispose();
        expect(widget.isDisposed).to.equal(true);
      });
    }


() => {
        const widget = new TestWidget();
        expect(widget).to.be.an.instanceof(TestWidget);
      }


() => {
        const widget = new TestWidget();
        widget.dispose();
        expect(widget.isDisposed).to.equal(true);
      }


() => {
      it('should fire the stateChanged signal on a change', () => {
        const widget = new TestWidget();
        const model = new TestModel();
        let changed = false;
        widget.modelChanged.connect(() => {
          changed = true;
        });
        widget.model = model;
        expect(changed).to.equal(true);
      });
    }


() => {
        const widget = new TestWidget();
        const model = new TestModel();
        let changed = false;
        widget.modelChanged.connect(() => {
          changed = true;
        });
        widget.model = model;
        expect(changed).to.equal(true);
      }


() => {
      it('should render the contents after a model change', async () => {
        const widget = new TestWidget();
        const model = new TestModel();
        widget.model = model;
        model.value = 'foo';
        await framePromise();
        let span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('foo');
      });
    }


async () => {
        const widget = new TestWidget();
        const model = new TestModel();
        widget.model = model;
        model.value = 'foo';
        await framePromise();
        let span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('foo');
      }


() => {
      it('should work with a null model', async () => {
        const widget = new TestWidgetNoModel();
        Widget.attach(widget, document.body);
        await framePromise();
        const span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('No model!');
      });
    }


async () => {
        const widget = new TestWidgetNoModel();
        Widget.attach(widget, document.body);
        await framePromise();
        const span = widget.node.firstChild as HTMLElement;
        expect(span.textContent).to.equal('No model!');
      }


class TestModel extends VDomModel {
  get value(): string {
    return this._value;
  }

  set value(newValue: string) {
    this._value = newValue;
    this.stateChanged.emit(void 0);
  }

  private _value = '';
}


class TestWidget extends VDomRenderer<TestModel> {
  protected render(): React.ReactElement<any> {
    return React.createElement('span', null, this.model.value);
  }
}


class TestWidgetNoModel extends VDomRenderer<null> {
  protected render(): React.ReactElement<any> {
    return React.createElement('span', null, 'No model!');
  }
}


protected render(): React.ReactElement<any> {
    return React.createElement('span', null, this.model.value);
  }


protected render(): React.ReactElement<any> {
    return React.createElement('span', null, 'No model!');
  }


async function claim() {
			if (isAppReady) {
				try {
					setClaimError(null);
					setClaimTxModalOpen(true);
					const contract = getContract(stakedAsset, signer);

					const gasLimit = await synthetix.getGasEstimateForTransaction({
						txArgs: [],
						method: contract.estimateGas.getReward,
					});
					const transaction: ethers.ContractTransaction = await contract.getReward({
						gasPrice: normalizedGasPrice(claimGasPrice),
						gasLimit,
					});

					if (transaction) {
						setClaimTxHash(transaction.hash);
						setClaimTransactionState(Transaction.WAITING);
						monitorTransaction({
							txHash: transaction.hash,
							onTxConfirmed: () => setClaimTransactionState(Transaction.SUCCESS),
						});
						setClaimTxModalOpen(false);
					}
				} catch (e) {
					setClaimTransactionState(Transaction.PRESUBMIT);
					setClaimError(e.message);
				}
			}
		}


({
	stakedAsset,
	icon = stakedAsset,
	type,
	tokenRewards,
	allowance,
	userBalance,
	userBalanceBN,
	staked,
	stakedBN,
	needsToSettle,
	secondTokenRate,
}) => {
	const { t } = useTranslation();
	const { signer } = Connector.useContainer();
	const { monitorTransaction } = TransactionNotifier.useContainer();
	const [showApproveOverlayModal, setShowApproveOverlayModal] = useState<boolean>(false);
	const [showSettleOverlayModal, setShowSettleOverlayModal] = useState<boolean>(false);

	const isAppReady = useRecoilValue(appReadyState);

	const [claimGasPrice, setClaimGasPrice] = useState<number>(0);
	const [claimTransactionState, setClaimTransactionState] = useState<Transaction>(
		Transaction.PRESUBMIT
	);
	const [claimTxHash, setClaimTxHash] = useState<string | null>(null);
	const [claimError, setClaimError] = useState<string | null>(null);
	const [claimTxModalOpen, setClaimTxModalOpen] = useState<boolean>(false);

	const { blockExplorerInstance } = Etherscan.useContainer();
	const claimLink =
		blockExplorerInstance != null && claimTxHash != null
			? blockExplorerInstance.txLink(claimTxHash)
			: undefined;

	const exchangeRatesQuery = useExchangeRatesQuery();
	const SNXRate = exchangeRatesQuery.data?.SNX ?? 0;

	const tabData = useMemo(() => {
		const commonStakeTabProps = {
			stakedAsset,
			userBalance,
			userBalanceBN,
			staked,
			stakedBN,
			icon,
			type,
		};

		return [
			{
				title: t('earn.actions.stake.title'),
				tabChildren: <StakeTab {...commonStakeTabProps} isStake={true} />,
				blue: true,
				key: 'stake',
			},
			{
				title: t('earn.actions.unstake.title'),
				tabChildren: <StakeTab {...commonStakeTabProps} isStake={false} />,
				blue: false,
				key: 'unstake',
			},
		];
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [t, stakedAsset, userBalance, staked]);

	useEffect(() => {
		if (allowance === 0 && userBalance > 0) {
			setShowApproveOverlayModal(true);
		}
	}, [allowance, userBalance]);

	useEffect(() => {
		if (needsToSettle) {
			setShowSettleOverlayModal(true);
		}
	}, [needsToSettle]);

	const handleClaim = useCallback(() => {
		async function claim() {
			if (isAppReady) {
				try {
					setClaimError(null);
					setClaimTxModalOpen(true);
					const contract = getContract(stakedAsset, signer);

					const gasLimit = await synthetix.getGasEstimateForTransaction({
						txArgs: [],
						method: contract.estimateGas.getReward,
					});
					const transaction: ethers.ContractTransaction = await contract.getReward({
						gasPrice: normalizedGasPrice(claimGasPrice),
						gasLimit,
					});

					if (transaction) {
						setClaimTxHash(transaction.hash);
						setClaimTransactionState(Transaction.WAITING);
						monitorTransaction({
							txHash: transaction.hash,
							onTxConfirmed: () => setClaimTransactionState(Transaction.SUCCESS),
						});
						setClaimTxModalOpen(false);
					}
				} catch (e) {
					setClaimTransactionState(Transaction.PRESUBMIT);
					setClaimError(e.message);
				}
			}
		}
		claim();
	}, [stakedAsset, signer, claimGasPrice, monitorTransaction, isAppReady]);

	const translationKey = useMemo(() => {
		if (stakedAsset === Synths.iETH) {
			return 'earn.incentives.options.ieth.description';
		} else if (stakedAsset === Synths.iBTC) {
			return 'earn.incentives.options.ibtc.description';
		} else if (stakedAsset === LP.CURVE_sUSD) {
			return 'earn.incentives.options.curve.description';
		} else if (stakedAsset === LP.CURVE_sEURO) {
			return 'earn.incentives.options.seur.description';
		} else if (stakedAsset === LP.BALANCER_sTSLA) {
			return 'earn.incentives.options.stsla.description';
		} else if (stakedAsset === LP.UNISWAP_DHT) {
			return 'earn.incentives.options.dht.description';
		} else if (stakedAsset in lpToSynthTranslationKey) {
			return `earn.incentives.options.${lpToSynthTranslationKey[stakedAsset]}.description`;
		} else {
			throw new Error('unexpected staking asset for translation key');
		}
	}, [stakedAsset]);

	const DualRewardsClaimInfo = (
		<StyledFlexDiv>
			<StyledFlexDivColCentered>
				<GreyHeader>{t('earn.actions.claim.claiming')}</GreyHeader>
				<WhiteSubheader>
					{t('earn.actions.claim.amount', {
						amount: formatNumber((tokenRewards as DualRewards).a, {
							decimals: DEFAULT_CRYPTO_DECIMALS,
						}),
						asset: CryptoCurrency.SNX,
					})}
				</WhiteSubheader>
			</StyledFlexDivColCentered>
			<StyledFlexDivColCentered>
				<GreyHeader>{t('earn.actions.claim.claiming')}</GreyHeader>
				<WhiteSubheader>
					{t('earn.actions.claim.amount', {
						amount: formatNumber((tokenRewards as DualRewards).b, {
							decimals: DEFAULT_CRYPTO_DECIMALS,
						}),
						asset: CryptoCurrency.DHT,
					})}
				</WhiteSubheader>
			</StyledFlexDivColCentered>
		</StyledFlexDiv>


() => {
		const commonStakeTabProps = {
			stakedAsset,
			userBalance,
			userBalanceBN,
			staked,
			stakedBN,
			icon,
			type,
		};

		return [
			{
				title: t('earn.actions.stake.title'),
				tabChildren: <StakeTab {...commonStakeTabProps} isStake={true} />,
				blue: true,
				key: 'stake',
			},
			{
				title: t('earn.actions.unstake.title'),
				tabChildren: <StakeTab {...commonStakeTabProps} isStake={false} />,
				blue: false,
				key: 'unstake',
			},
		];
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}


() => {
		if (allowance === 0 && userBalance > 0) {
			setShowApproveOverlayModal(true);
		}
	}


() => {
		if (needsToSettle) {
			setShowSettleOverlayModal(true);
		}
	}


() => {
		async function claim() {
			if (isAppReady) {
				try {
					setClaimError(null);
					setClaimTxModalOpen(true);
					const contract = getContract(stakedAsset, signer);

					const gasLimit = await synthetix.getGasEstimateForTransaction({
						txArgs: [],
						method: contract.estimateGas.getReward,
					});
					const transaction: ethers.ContractTransaction = await contract.getReward({
						gasPrice: normalizedGasPrice(claimGasPrice),
						gasLimit,
					});

					if (transaction) {
						setClaimTxHash(transaction.hash);
						setClaimTransactionState(Transaction.WAITING);
						monitorTransaction({
							txHash: transaction.hash,
							onTxConfirmed: () => setClaimTransactionState(Transaction.SUCCESS),
						});
						setClaimTxModalOpen(false);
					}
				} catch (e) {
					setClaimTransactionState(Transaction.PRESUBMIT);
					setClaimError(e.message);
				}
			}
		}
		claim();
	}


() => setClaimTransactionState(Transaction.SUCCESS)


() => {
		if (stakedAsset === Synths.iETH) {
			return 'earn.incentives.options.ieth.description';
		} else if (stakedAsset === Synths.iBTC) {
			return 'earn.incentives.options.ibtc.description';
		} else if (stakedAsset === LP.CURVE_sUSD) {
			return 'earn.incentives.options.curve.description';
		} else if (stakedAsset === LP.CURVE_sEURO) {
			return 'earn.incentives.options.seur.description';
		} else if (stakedAsset === LP.BALANCER_sTSLA) {
			return 'earn.incentives.options.stsla.description';
		} else if (stakedAsset === LP.UNISWAP_DHT) {
			return 'earn.incentives.options.dht.description';
		} else if (stakedAsset in lpToSynthTranslationKey) {
			return `earn.incentives.options.${lpToSynthTranslationKey[stakedAsset]}.description`;
		} else {
			throw new Error('unexpected staking asset for translation key');
		}
	}


() => {
		switch (stakedAsset) {
			case LP.BALANCER_sTSLA:
				return `https://pools.balancer.exchange/#/pool/0x055db9aff4311788264798356bbf3a733ae181c6/`;
			case LP.BALANCER_sFB:
				return `https://pools.balancer.exchange/#/pool/0x3f2d077acff8a66c4e0c79c37b6a662a7197889b/`;
			case LP.BALANCER_sAAPL:
				return `https://pools.balancer.exchange/#/pool/0xb94865e18b25114b2b10bd9ecbd689c877f949e8/`;
			case LP.BALANCER_sAMZN:
				return `https://pools.balancer.exchange/#/pool/0x74821343b5b969c0d4b31aff3931e00a40990cfd/`;
			case LP.BALANCER_sNFLX:
				return `https://pools.balancer.exchange/#/pool/0x6418c69b0de51873a1cc01cf73ba6e408acc1940/`;
			case LP.BALANCER_sGOOG:
				return `https://pools.balancer.exchange/#/pool/0x608410f602ce8967d1e59f599566aed340280efc/`;
			case LP.UNISWAP_DHT:
				return `https://uniswap.exchange/add/0x57ab1ec28d129707052df4df418d58a2d46d5f51/0xca1207647ff814039530d7d35df0e1dd2e91fa84`;
			default:
				return EXTERNAL_LINKS.Synthetix.Incentives;
		}
	}


type DualRewards = {
	a: number;
	b: number;
};


type LPTabProps = {
	stakedAsset: CurrencyKey;
	icon?: CurrencyKey;
	type?: CurrencyIconType;
	tokenRewards: number | DualRewards;
	allowance: number | null;
	userBalance: number;
	userBalanceBN: BigNumber;
	staked: number;
	stakedBN: BigNumber;
	needsToSettle?: boolean;
	secondTokenRate?: number;
};


t('earn.actions.rewards.waiting')


getLink()


async function setOneImpliedJson(
    id: string,
    val: unknown,
  ): Promise<{id: string; val: unknown}> {
    const [result] = await db.query(sql`
      INSERT INTO json_test.json (id, val) VALUES (${id}, ${val})
      ON CONFLICT (id) DO UPDATE SET val=EXCLUDED.val
      RETURNING *;
    `);
    return result;
  }


async function setOneExplicitJson(
    id: string,
    val: unknown,
  ): Promise<{id: string; val: unknown}> {
    const [result] = await db.query(sql`
      INSERT INTO json_test.json (id, val) VALUES (${id}, ${JSON.stringify(
      val,
    )})
      ON CONFLICT (id) DO UPDATE SET val=EXCLUDED.val
      RETURNING *;
    `);
    return result;
  }


async function setMany(
    ...values: readonly {
      readonly id: string;
      readonly val: unknown;
    }[]
  ): Promise<{id: string; val: unknown}[]> {
    const results = await db.query(
      values.map(
        ({id, val}) => sql`
          INSERT INTO json_test.json (id, val)
          VALUES (${id}, ${JSON.stringify(val)})
          ON CONFLICT (id) DO UPDATE SET val=EXCLUDED.val
          RETURNING *;
        `,
      ),
    );
    return results.map(([{id, val}]) => ({id, val}));
  }


async () => {
  await db.dispose();
}


async () => {
  const [{foo}] = await db.query(sql`SELECT 1 + 1 as foo`);
  expect(foo).toBe(2);
}


async () => {
  const resultA = await db.query([sql`SELECT 1 + 1 as foo`]);
  expect(resultA).toEqual([[{foo: 2}]]);
  const resultB = await db.query([
    sql`SELECT ${1} + 1 as foo;`,
    sql`SELECT 1 + ${2} as bar;`,
  ]);
  expect(resultB).toEqual([[{foo: 2}], [{bar: 3}]]);
}


async () => {
  const [{foo}] = await db.query(sql`SELECT 1 + ${41} as ${sql.ident('foo')}`);
  expect(foo).toBe(42);
}


async () => {
  await db.query(sql`CREATE SCHEMA json_test`);
  await db.query(
    sql`CREATE TABLE json_test.json (id TEXT NOT NULL PRIMARY KEY, val JSONB NOT NULL);`,
  );
  async function setOneImpliedJson(
    id: string,
    val: unknown,
  ): Promise<{id: string; val: unknown}> {
    const [result] = await db.query(sql`
      INSERT INTO json_test.json (id, val) VALUES (${id}, ${val})
      ON CONFLICT (id) DO UPDATE SET val=EXCLUDED.val
      RETURNING *;
    `);
    return result;
  }
  async function setOneExplicitJson(
    id: string,
    val: unknown,
  ): Promise<{id: string; val: unknown}> {
    const [result] = await db.query(sql`
      INSERT INTO json_test.json (id, val) VALUES (${id}, ${JSON.stringify(
      val,
    )})
      ON CONFLICT (id) DO UPDATE SET val=EXCLUDED.val
      RETURNING *;
    `);
    return result;
  }
  async function setMany(
    ...values: readonly {
      readonly id: string;
      readonly val: unknown;
    }[]
  ): Promise<{id: string; val: unknown}[]> {
    const results = await db.query(
      values.map(
        ({id, val}) => sql`
          INSERT INTO json_test.json (id, val)
          VALUES (${id}, ${JSON.stringify(val)})
          ON CONFLICT (id) DO UPDATE SET val=EXCLUDED.val
          RETURNING *;
        `,
      ),
    );
    return results.map(([{id, val}]) => ({id, val}));
  }

  await expect(setOneImpliedJson('obj', {foo: 'bar'})).resolves.toEqual({
    id: 'obj',
    val: {
      foo: 'bar',
    },
  });
  await expect(setOneExplicitJson('arr', ['my string', 56])).resolves.toEqual({
    id: 'arr',
    val: ['my string', 56],
  });
  await expect(setOneExplicitJson('str', 'my string')).resolves.toEqual({
    id: 'str',
    val: 'my string',
  });
  await expect(setOneExplicitJson('num', 42)).resolves.toEqual({
    id: 'num',
    val: 42,
  });

  await expect(setOneImpliedJson('obj', {foo: 'bing'})).resolves.toEqual({
    id: 'obj',
    val: {
      foo: 'bing',
    },
  });
  await expect(setOneExplicitJson('arr', [56, 'my string'])).resolves.toEqual({
    id: 'arr',
    val: [56, 'my string'],
  });
  await expect(setOneExplicitJson('str', 'my other string')).resolves.toEqual({
    id: 'str',
    val: 'my other string',
  });
  await expect(setOneExplicitJson('num', 21)).resolves.toEqual({
    id: 'num',
    val: 21,
  });

  await expect(
    setMany(
      {id: 'obj', val: {foo: 'bar'}},
      {id: 'arr', val: ['my string', 56]},
      {id: 'str', val: 'my string'},
      {id: 'num', val: 42},
    ),
  ).resolves.toEqual([
    {id: 'obj', val: {foo: 'bar'}},
    {id: 'arr', val: ['my string', 56]},
    {id: 'str', val: 'my string'},
    {id: 'num', val: 42},
  ]);
}


({id, val}) => sql`
          INSERT INTO json_test.json (id, val)
          VALUES (${id}, ${JSON.stringify(val)})
          ON CONFLICT (id) DO UPDATE SET val=EXCLUDED.val
          RETURNING *;
        `


([{id, val}]) => ({id, val})


async () => {
  await db.query(sql`CREATE SCHEMA bigint_test`);
  await db.query(
    sql`CREATE TABLE bigint_test.bigints (id BIGINT NOT NULL PRIMARY KEY);`,
  );
  await db.query(sql`
    INSERT INTO bigint_test.bigints (id)
    VALUES (1),
           (2),
           (42);
  `);
  const result = await db.query(sql`SELECT id from bigint_test.bigints;`);
  expect(result).toEqual([{id: 1}, {id: 2}, {id: 42}]);

  expect(await db.query(sql`SELECT id from bigint_test.bigints;`)).toEqual([
    {id: 1},
    {id: 2},
    {id: 42},
  ]);
}


@Component({
  selector: 'dialog-github-initial-setup',
  templateUrl: './dialog-github-initial-setup.component.html',
  styleUrls: ['./dialog-github-initial-setup.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DialogGithubInitialSetupComponent implements OnInit {
  T: typeof T = T;
  githubCfg: GithubCfg;
  formGroup: FormGroup = new FormGroup({});
  formConfig: FormlyFieldConfig[] = GITHUB_CONFIG_FORM;

  constructor(
    @Inject(MAT_DIALOG_DATA) public data: any,
    private _matDialogRef: MatDialogRef<DialogGithubInitialSetupComponent>,
  ) {
    this.githubCfg = this.data.githubCfg || DEFAULT_GITHUB_CFG;
  }

  ngOnInit(): void {}

  saveGithubCfg(gitCfg: GithubCfg): void {
    this._matDialogRef.close(gitCfg);
  }

  close(): void {
    this._matDialogRef.close();
  }
}


saveGithubCfg(gitCfg: GithubCfg): void {
    this._matDialogRef.close(gitCfg);
  }


close(): void {
    this._matDialogRef.close();
  }


export class PostgresQueryBuilder {
  constructor(private target, private queryModel) {}

  buildSchemaQuery() {
    var query = 'SELECT schema_name FROM information_schema.schemata WHERE';
    query += " schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE '\\_%' AND schema_name <> 'information_schema';";

    return query;
  }

  buildTableQuery() {
    var query = 'SELECT table_name FROM information_schema.tables WHERE ';
    query += 'table_schema = ' + this.queryModel.quoteLiteral(this.target.schema);
    return query;
  }

  buildColumnQuery(type?: string) {
    var query = 'SELECT column_name FROM information_schema.columns WHERE ';
    query += 'table_schema = ' + this.queryModel.quoteLiteral(this.target.schema);
    query += ' AND table_name = ' + this.queryModel.quoteLiteral(this.target.table);

    switch (type) {
      case 'time': {
        query +=
          " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')";
        break;
      }
      case 'metric': {
        query += " AND data_type IN ('text','char','varchar')";
        break;
      }
      case 'value': {
        query += " AND data_type IN ('bigint','integer','double precision','real')";
        break;
      }
    }

    return query;
  }

  buildValueQuery(column: string) {
    var query = 'SELECT DISTINCT ' + this.queryModel.quoteIdentifier(column) + '::text';
    query += ' FROM ' + this.queryModel.quoteIdentifier(this.target.schema);
    query += '.' + this.queryModel.quoteIdentifier(this.target.table);
    query += ' ORDER BY ' + this.queryModel.quoteIdentifier(column);
    query += ' LIMIT 100';
    return query;
  }
}


buildSchemaQuery() {
    var query = 'SELECT schema_name FROM information_schema.schemata WHERE';
    query += " schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE '\\_%' AND schema_name <> 'information_schema';";

    return query;
  }


buildTableQuery() {
    var query = 'SELECT table_name FROM information_schema.tables WHERE ';
    query += 'table_schema = ' + this.queryModel.quoteLiteral(this.target.schema);
    return query;
  }


buildColumnQuery(type?: string) {
    var query = 'SELECT column_name FROM information_schema.columns WHERE ';
    query += 'table_schema = ' + this.queryModel.quoteLiteral(this.target.schema);
    query += ' AND table_name = ' + this.queryModel.quoteLiteral(this.target.table);

    switch (type) {
      case 'time': {
        query +=
          " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')";
        break;
      }
      case 'metric': {
        query += " AND data_type IN ('text','char','varchar')";
        break;
      }
      case 'value': {
        query += " AND data_type IN ('bigint','integer','double precision','real')";
        break;
      }
    }

    return query;
  }


buildValueQuery(column: string) {
    var query = 'SELECT DISTINCT ' + this.queryModel.quoteIdentifier(column) + '::text';
    query += ' FROM ' + this.queryModel.quoteIdentifier(this.target.schema);
    query += '.' + this.queryModel.quoteIdentifier(this.target.table);
    query += ' ORDER BY ' + this.queryModel.quoteIdentifier(column);
    query += ' LIMIT 100';
    return query;
  }


({ children, title }) => {
  const [isThemeDark, setIsThemeDark] = useThemeState(false)

  return (
    <>
      <Global styles={NORMALIZE} />
      <SEO title={title} />
      <ThemeContext.Provider value={{ isThemeDark, setIsThemeDark }}


interface ILayout {
  title?: string
}


/*
 * Licensed to Elasticsearch B.V. under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch B.V. licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
class FunctionScoreQuery extends QueryBase {
  boost_mode?: FunctionBoostMode
  functions?: FunctionScoreContainer[]
  max_boost?: double
  min_score?: double
  query?: QueryContainer
  score_mode?: FunctionScoreMode
  boost?: float
}


() => MoveArgs


() => FuzzyMoveArgs


export class MoveService {
  private static readonly fuzzySearch = new FuzzySearch(moves, ['name', 'aliases']);
  public static getByMoveName(@Args(() => MoveArgs) { move }: MoveArgs): PokemonTypes.Move | undefined {
    return moves.get(move);
  }

  public static mapMoveDataToMoveGraphQL({ data, requestedFields }: MapMoveDataToMoveGraphQLParameters): Move {
    const move = new Move();

    addPropertyToClass(move, 'key', data.key, requestedFields);
    addPropertyToClass(move, 'name', data.name, requestedFields);
    addPropertyToClass(move, 'desc', data.desc, requestedFields);
    addPropertyToClass(move, 'shortDesc', data.shortDesc, requestedFields);
    addPropertyToClass(move, 'type', data.type, requestedFields);
    addPropertyToClass(move, 'contestType', data.contestType, requestedFields);
    addPropertyToClass(move, 'basePower', data.basePower, requestedFields);
    addPropertyToClass(move, 'pp', data.pp, requestedFields);
    addPropertyToClass(move, 'category', data.category, requestedFields);
    addPropertyToClass(move, 'accuracy', data.accuracy, requestedFields);
    addPropertyToClass(move, 'priority', data.priority, requestedFields);
    addPropertyToClass(move, 'target', data.target, requestedFields);
    addPropertyToClass(move, 'isNonstandard', data.isNonstandard, requestedFields);
    addPropertyToClass(move, 'isGMax', data.isGMax, requestedFields);
    addPropertyToClass(move, 'isZ', parseZCrystal(data.isZ), requestedFields);
    addPropertyToClass(move, 'isFieldMove', data.isFieldMove, requestedFields);
    addPropertyToClass(move, 'maxMovePower', data.maxMovePower, requestedFields);
    addPropertyToClass(move, 'zMovePower', this.parseZMovePower(data.basePower, data.zMovePower), requestedFields);
    addPropertyToClass(move, 'serebiiPage', `https://www.serebii.net/attackdex-swsh/${toLowerSingleWordCase(data.name)}.shtml`, requestedFields);
    addPropertyToClass(move, 'bulbapediaPage', `https://bulbapedia.bulbagarden.net/wiki/${toTitleSnakeCase(data.name)}_(move)`, requestedFields);
    addPropertyToClass(move, 'smogonPage', `https://www.smogon.com/dex/ss/moves/${toLowerHyphenCase(data.name)}`, requestedFields);

    return move;
  }

  public static findByFuzzy(@Args(() => FuzzyMoveArgs) { move, offset, reverse, take }: FuzzyMoveArgs): PokemonTypes.Move[] {
    move = preParseInput(move);

    const fuzzyResult = this.fuzzySearch.runFuzzy(move);

    if (reverse) {
      fuzzyResult.reverse();
    }

    return fuzzyResult.slice(offset, offset + take);
  }

  /**
   * Converts basePower and zMovePower to the correct Z-Move power, using datamined conversion table seen below.
   *
   * | Base move power  | Z-Move power |
   * |------------------|--------------|
   * | 0-55             | 100          |
   * | 60-65            | 120          |
   * | 70-75            | 140          |
   * | 80-85            | 160          |
   * | 90-95            | 175          |
   * | 100              | 180          |
   * | 110              | 185          |
   * | 120-125          | 190          |
   * | 130              | 195          |
   * | 140+             | 200          |
   * @param basePower The basepower of a move
   * @param zMovePower The z-move power of a move, if specified it is preferred.
   */
  private static parseZMovePower(basePower: string, zMovePower: number | undefined): number {
    // If zMovePower was defined on the move data then just return that value
    if (typeof zMovePower === 'number') return zMovePower;

    // If the basePower is 0 (status moves) then return 0
    if (basePower === '0') return 0;

    const basePowerAsNumber = Number(basePower);

    if (basePowerAsNumber >= 0 && basePowerAsNumber <= 55) return 100;
    if (basePowerAsNumber >= 60 && basePowerAsNumber <= 65) return 120;

    if (basePowerAsNumber >= 70 && basePowerAsNumber <= 75) return 140;
    if (basePowerAsNumber >= 80 && basePowerAsNumber <= 85) return 160;
    if (basePowerAsNumber >= 90 && basePowerAsNumber <= 95) return 175;
    if (basePowerAsNumber === 100) return 180;
    if (basePowerAsNumber === 110) return 185;
    if (basePowerAsNumber >= 120 && basePowerAsNumber <= 125) return 190;
    if (basePowerAsNumber === 130) return 195;
    if (basePowerAsNumber >= 140) return 200;

    // If none of the cases matched then we return 0. This can happen if the move cannot be a Z-move
    // Which is the case for moves that were exclusive to a game that didn't feature Z-Moves
    // Such as "Pika Papow" and "Veevee Volley"
    return 0;
  }
}


interface MapMoveDataToMoveGraphQLParameters {
  data: PokemonTypes.Move;
  requestedFields: GraphQLSet<keyof Move>;
}


public static getByMoveName(@Args(() => MoveArgs) { move }: MoveArgs): PokemonTypes.Move | undefined {
    return moves.get(move);
  }


public static mapMoveDataToMoveGraphQL({ data, requestedFields }: MapMoveDataToMoveGraphQLParameters): Move {
    const move = new Move();

    addPropertyToClass(move, 'key', data.key, requestedFields);
    addPropertyToClass(move, 'name', data.name, requestedFields);
    addPropertyToClass(move, 'desc', data.desc, requestedFields);
    addPropertyToClass(move, 'shortDesc', data.shortDesc, requestedFields);
    addPropertyToClass(move, 'type', data.type, requestedFields);
    addPropertyToClass(move, 'contestType', data.contestType, requestedFields);
    addPropertyToClass(move, 'basePower', data.basePower, requestedFields);
    addPropertyToClass(move, 'pp', data.pp, requestedFields);
    addPropertyToClass(move, 'category', data.category, requestedFields);
    addPropertyToClass(move, 'accuracy', data.accuracy, requestedFields);
    addPropertyToClass(move, 'priority', data.priority, requestedFields);
    addPropertyToClass(move, 'target', data.target, requestedFields);
    addPropertyToClass(move, 'isNonstandard', data.isNonstandard, requestedFields);
    addPropertyToClass(move, 'isGMax', data.isGMax, requestedFields);
    addPropertyToClass(move, 'isZ', parseZCrystal(data.isZ), requestedFields);
    addPropertyToClass(move, 'isFieldMove', data.isFieldMove, requestedFields);
    addPropertyToClass(move, 'maxMovePower', data.maxMovePower, requestedFields);
    addPropertyToClass(move, 'zMovePower', this.parseZMovePower(data.basePower, data.zMovePower), requestedFields);
    addPropertyToClass(move, 'serebiiPage', `https://www.serebii.net/attackdex-swsh/${toLowerSingleWordCase(data.name)}.shtml`, requestedFields);
    addPropertyToClass(move, 'bulbapediaPage', `https://bulbapedia.bulbagarden.net/wiki/${toTitleSnakeCase(data.name)}_(move)`, requestedFields);
    addPropertyToClass(move, 'smogonPage', `https://www.smogon.com/dex/ss/moves/${toLowerHyphenCase(data.name)}`, requestedFields);

    return move;
  }


public static findByFuzzy(@Args(() => FuzzyMoveArgs) { move, offset, reverse, take }: FuzzyMoveArgs): PokemonTypes.Move[] {
    move = preParseInput(move);

    const fuzzyResult = this.fuzzySearch.runFuzzy(move);

    if (reverse) {
      fuzzyResult.reverse();
    }

    return fuzzyResult.slice(offset, offset + take);
  }


/**
   * Converts basePower and zMovePower to the correct Z-Move power, using datamined conversion table seen below.
   *
   * | Base move power  | Z-Move power |
   * |------------------|--------------|
   * | 0-55             | 100          |
   * | 60-65            | 120          |
   * | 70-75            | 140          |
   * | 80-85            | 160          |
   * | 90-95            | 175          |
   * | 100              | 180          |
   * | 110              | 185          |
   * | 120-125          | 190          |
   * | 130              | 195          |
   * | 140+             | 200          |
   * @param basePower The basepower of a move
   * @param zMovePower The z-move power of a move, if specified it is preferred.
   */
private static parseZMovePower(basePower: string, zMovePower: number | undefined): number {
    // If zMovePower was defined on the move data then just return that value
    if (typeof zMovePower === 'number') return zMovePower;

    // If the basePower is 0 (status moves) then return 0
    if (basePower === '0') return 0;

    const basePowerAsNumber = Number(basePower);

    if (basePowerAsNumber >= 0 && basePowerAsNumber <= 55) return 100;
    if (basePowerAsNumber >= 60 && basePowerAsNumber <= 65) return 120;

    if (basePowerAsNumber >= 70 && basePowerAsNumber <= 75) return 140;
    if (basePowerAsNumber >= 80 && basePowerAsNumber <= 85) return 160;
    if (basePowerAsNumber >= 90 && basePowerAsNumber <= 95) return 175;
    if (basePowerAsNumber === 100) return 180;
    if (basePowerAsNumber === 110) return 185;
    if (basePowerAsNumber >= 120 && basePowerAsNumber <= 125) return 190;
    if (basePowerAsNumber === 130) return 195;
    if (basePowerAsNumber >= 140) return 200;

    // If none of the cases matched then we return 0. This can happen if the move cannot be a Z-move
    // Which is the case for moves that were exclusive to a game that didn't feature Z-Moves
    // Such as "Pika Papow" and "Veevee Volley"
    return 0;
  }


() => {
    it('should create style instructions on the element', () => {
      const files = {
        app: {
          'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`<div [style]="myStyleExp"></div>\`
                })
                export class MyComponent {
                  myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
        }
      };

      const template = `
          template: function MyComponent_Template(rf, $ctx$) {
            if (rf & 1) {
              $r3$.ɵE(0, "div");
              $r3$.ɵs(null, null, $r3$.ɵzss);
              $r3$.ɵe();
            }
            if (rf & 2) {
              $r3$.ɵsm(0, null, $ctx$.myStyleExp);
              $r3$.ɵsa(0);
            }
          }
          `;

      const result = compile(files, angularFiles);
      expectEmit(result.source, template, 'Incorrect template');
    });

    it('should place initial, multi, singular and application followed by attribute style instructions in the template code in that order',
       () => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`

\`
                })
                export class MyComponent {
                  myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
                  myWidth = '100px';
                  myHeight = '100px';
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["opacity","width","height",${InitialStylingFlags.VALUES_MODE},"opacity","1"];
          …
          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
              type: MyComponent,
              selectors:[["my-component"]],
              factory:function MyComponent_Factory(){
                return new MyComponent();
              },
              features: [$r3$.ɵPublicFeature],
              template: function MyComponent_Template(rf, $ctx$) {
                if (rf & 1) {
                  $r3$.ɵE(0, "div");
                  $r3$.ɵs(null, _c0, $r3$.ɵzss);
                  $r3$.ɵe();
                }
                if (rf & 2) {
                  $r3$.ɵsm(0, null, $ctx$.myStyleExp);
                  $r3$.ɵsp(0, 1, $ctx$.myWidth);
                  $r3$.ɵsp(0, 2, $ctx$.myHeight);
                  $r3$.ɵsa(0);
                  $r3$.ɵa(0, "style", $r3$.ɵb("border-width: 10px"), $r3$.ɵzs);
                }
              }
            });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       });

    it('should assign a sanitizer instance to the element style allocation instruction if any url-based properties are detected',
       () => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`<div [style.background-image]="myImage">\`
                })
                export class MyComponent {
                  myImage = 'url(foo.jpg)';
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["background-image"];
          export class MyComponent {
              constructor() {
                  this.myImage = 'url(foo.jpg)';
              }
          }

          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
            type: MyComponent,
            selectors: [["my-component"]],
            factory: function MyComponent_Factory() {
              return new MyComponent();
            },
            features: [$r3$.ɵPublicFeature],
            template: function MyComponent_Template(rf, ctx) {
              if (rf & 1) {
                $r3$.ɵE(0, "div");
                $r3$.ɵs(null, _c0, $r3$.ɵzss);
                $r3$.ɵe();
              }
              if (rf & 2) {
                $r3$.ɵsp(0, 0, ctx.myImage);
                $r3$.ɵsa(0);
              }
            }
          });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       });
  }


() => {
      const files = {
        app: {
          'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`<div [style]="myStyleExp"></div>\`
                })
                export class MyComponent {
                  myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
        }
      };

      const template = `
          template: function MyComponent_Template(rf, $ctx$) {
            if (rf & 1) {
              $r3$.ɵE(0, "div");
              $r3$.ɵs(null, null, $r3$.ɵzss);
              $r3$.ɵe();
            }
            if (rf & 2) {
              $r3$.ɵsm(0, null, $ctx$.myStyleExp);
              $r3$.ɵsa(0);
            }
          }
          `;

      const result = compile(files, angularFiles);
      expectEmit(result.source, template, 'Incorrect template');
    }


() => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`

\`
                })
                export class MyComponent {
                  myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
                  myWidth = '100px';
                  myHeight = '100px';
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["opacity","width","height",${InitialStylingFlags.VALUES_MODE},"opacity","1"];
          …
          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
              type: MyComponent,
              selectors:[["my-component"]],
              factory:function MyComponent_Factory(){
                return new MyComponent();
              },
              features: [$r3$.ɵPublicFeature],
              template: function MyComponent_Template(rf, $ctx$) {
                if (rf & 1) {
                  $r3$.ɵE(0, "div");
                  $r3$.ɵs(null, _c0, $r3$.ɵzss);
                  $r3$.ɵe();
                }
                if (rf & 2) {
                  $r3$.ɵsm(0, null, $ctx$.myStyleExp);
                  $r3$.ɵsp(0, 1, $ctx$.myWidth);
                  $r3$.ɵsp(0, 2, $ctx$.myHeight);
                  $r3$.ɵsa(0);
                  $r3$.ɵa(0, "style", $r3$.ɵb("border-width: 10px"), $r3$.ɵzs);
                }
              }
            });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       }


() => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`<div [style.background-image]="myImage">\`
                })
                export class MyComponent {
                  myImage = 'url(foo.jpg)';
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["background-image"];
          export class MyComponent {
              constructor() {
                  this.myImage = 'url(foo.jpg)';
              }
          }

          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
            type: MyComponent,
            selectors: [["my-component"]],
            factory: function MyComponent_Factory() {
              return new MyComponent();
            },
            features: [$r3$.ɵPublicFeature],
            template: function MyComponent_Template(rf, ctx) {
              if (rf & 1) {
                $r3$.ɵE(0, "div");
                $r3$.ɵs(null, _c0, $r3$.ɵzss);
                $r3$.ɵe();
              }
              if (rf & 2) {
                $r3$.ɵsp(0, 0, ctx.myImage);
                $r3$.ɵsa(0);
              }
            }
          });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       }


() => {
    it('should create class styling instructions on the element', () => {
      const files = {
        app: {
          'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`<div [class]="myClassExp"></div>\`
                })
                export class MyComponent {
                  myClassExp = {'foo':true}
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
        }
      };

      const template = `
          template: function MyComponent_Template(rf, $ctx$) {
            if (rf & 1) {
              $r3$.ɵE(0, "div");
              $r3$.ɵs();
              $r3$.ɵe();
            }
            if (rf & 2) {
              $r3$.ɵsm(0,$ctx$.myClassExp);
              $r3$.ɵsa(0);
            }
          }
          `;

      const result = compile(files, angularFiles);
      expectEmit(result.source, template, 'Incorrect template');
    });

    it('should place initial, multi, singular and application followed by attribute class instructions in the template code in that order',
       () => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`

\`
                })
                export class MyComponent {
                  myClassExp = {a:true, b:true};
                  yesToApple = true;
                  yesToOrange = true;
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["grape","apple","orange",${InitialStylingFlags.VALUES_MODE},"grape",true];
          …
          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
              type: MyComponent,
              selectors:[["my-component"]],
              factory:function MyComponent_Factory(){
                return new MyComponent();
              },
              features: [$r3$.ɵPublicFeature],
              template: function MyComponent_Template(rf, $ctx$) {
                if (rf & 1) {
                  $r3$.ɵE(0, "div");
                  $r3$.ɵs(_c0);
                  $r3$.ɵe();
                }
                if (rf & 2) {
                  $r3$.ɵsm(0, $ctx$.myClassExp);
                  $r3$.ɵcp(0, 1, $ctx$.yesToApple);
                  $r3$.ɵcp(0, 2, $ctx$.yesToOrange);
                  $r3$.ɵsa(0);
                  $r3$.ɵa(0, "class", $r3$.ɵb("banana"));
                }
              }
            });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       });

    it('should not generate the styling apply instruction if there are only static style/class attributes',
       () => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`

\`
                })
                export class MyComponent {}

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true];
          const _c1 = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"];
          …
          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
              type: MyComponent,
              selectors:[["my-component"]],
              factory:function MyComponent_Factory(){
                return new MyComponent();
              },
              features: [$r3$.ɵPublicFeature],
              template: function MyComponent_Template(rf, $ctx$) {
                if (rf & 1) {
                  $r3$.ɵE(0, "div");
                  $r3$.ɵs(_c0, _c1);
                  $r3$.ɵe();
                }
                if (rf & 2) {
                  $r3$.ɵa(0, "class", $r3$.ɵb("round"));
                  $r3$.ɵa(0, "style", $r3$.ɵb("height:100px"), $r3$.ɵzs);
                }
              }
            });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       });
  }


() => {
      const files = {
        app: {
          'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`<div [class]="myClassExp"></div>\`
                })
                export class MyComponent {
                  myClassExp = {'foo':true}
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
        }
      };

      const template = `
          template: function MyComponent_Template(rf, $ctx$) {
            if (rf & 1) {
              $r3$.ɵE(0, "div");
              $r3$.ɵs();
              $r3$.ɵe();
            }
            if (rf & 2) {
              $r3$.ɵsm(0,$ctx$.myClassExp);
              $r3$.ɵsa(0);
            }
          }
          `;

      const result = compile(files, angularFiles);
      expectEmit(result.source, template, 'Incorrect template');
    }


() => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`

\`
                })
                export class MyComponent {
                  myClassExp = {a:true, b:true};
                  yesToApple = true;
                  yesToOrange = true;
                }

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["grape","apple","orange",${InitialStylingFlags.VALUES_MODE},"grape",true];
          …
          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
              type: MyComponent,
              selectors:[["my-component"]],
              factory:function MyComponent_Factory(){
                return new MyComponent();
              },
              features: [$r3$.ɵPublicFeature],
              template: function MyComponent_Template(rf, $ctx$) {
                if (rf & 1) {
                  $r3$.ɵE(0, "div");
                  $r3$.ɵs(_c0);
                  $r3$.ɵe();
                }
                if (rf & 2) {
                  $r3$.ɵsm(0, $ctx$.myClassExp);
                  $r3$.ɵcp(0, 1, $ctx$.yesToApple);
                  $r3$.ɵcp(0, 2, $ctx$.yesToOrange);
                  $r3$.ɵsa(0);
                  $r3$.ɵa(0, "class", $r3$.ɵb("banana"));
                }
              }
            });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       }


() => {
         const files = {
           app: {
             'spec.ts': `
                import {Component, NgModule} from '@angular/core';

                @Component({
                  selector: 'my-component',
                  template: \`

\`
                })
                export class MyComponent {}

                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
           }
         };

         const template = `
          const _c0 = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true];
          const _c1 = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"];
          …
          MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
              type: MyComponent,
              selectors:[["my-component"]],
              factory:function MyComponent_Factory(){
                return new MyComponent();
              },
              features: [$r3$.ɵPublicFeature],
              template: function MyComponent_Template(rf, $ctx$) {
                if (rf & 1) {
                  $r3$.ɵE(0, "div");
                  $r3$.ɵs(_c0, _c1);
                  $r3$.ɵe();
                }
                if (rf & 2) {
                  $r3$.ɵa(0, "class", $r3$.ɵb("round"));
                  $r3$.ɵa(0, "style", $r3$.ɵb("height:100px"), $r3$.ɵzs);
                }
              }
            });
        `;

         const result = compile(files, angularFiles);
         expectEmit(result.source, template, 'Incorrect template');
       }


// #endregion
@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    RouterModule,
    ReactiveFormsModule,
    AlainThemeModule.forChild(),
    DelonACLModule,
    DelonFormModule,
    ...SHARED_DELON_MODULES,
    ...SHARED_ZORRO_MODULES,
    // third libs
    ...THIRDMODULES
  ],
  declarations: [
    // your components
    ...COMPONENTS,
    ...DIRECTIVES
  ],
  exports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModule,
    AlainThemeModule,
    DelonACLModule,
    DelonFormModule,
    ...SHARED_DELON_MODULES,
    ...SHARED_ZORRO_MODULES,
    // third libs
    ...THIRDMODULES,
    // your components
    ...COMPONENTS,
    ...DIRECTIVES
  ]
})
export class SharedModule {}


(subscription) => subscription.remove()


(state) => {
      // Update UI state (toggle switch, changePace button)
      this.addEvent('State', new Date(), state);
      this.zone.run(() => {
        this.isMoving = state.isMoving;
        this.enabled = state.enabled;
      });
    }


() => {
        this.isMoving = state.isMoving;
        this.enabled = state.enabled;
      }


(location) => {
      console.log('- getCurrentPosition: ', location);
    }


(error) => {
      console.warn('- Location error: ', error);
    }


() => {
      this.events.push({
        name: name,
        timestamp: timestamp,
        object: event,
        content: JSON.stringify(event, null, 2)
      });
    }


@Component({
  selector: 'app-hello-world',
  templateUrl: './hello-world.page.html',
  styleUrls: ['./hello-world.page.scss'],
})
export class HelloWorldPage implements OnInit, OnDestroy {

	// UI State
  enabled: boolean;
  isMoving: boolean;

  // ion-list datasource
  events: any;

  subscriptions: any;

  constructor(public navCtrl:NavController, public router:Router, private zone:NgZone) {
    this.events = [];
    this.subscriptions = [];

  }

  subscribe(subscription:Subscription) {
    this.subscriptions.push(subscription);
  }

  unsubscribe() {
    this.subscriptions.forEach((subscription) => subscription.remove() );
    this.subscriptions = [];
  }

  ngAfterContentInit() {
    console.log('⚙ ngAfterContentInit');
    this.configureBackgroundGeolocation();
  }

  ionViewWillEnter() {
    console.log('⚙ ionViewWillEnter');
  }

  ngOnInit() {
  }

  ngOnDestroy() {
    this.unsubscribe();
  }

  async configureBackgroundGeolocation() {
    // Step 1:  Listen to BackgroundGeolocation events.
    this.subscribe(BackgroundGeolocation.onEnabledChange(this.onEnabledChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onLocation(this.onLocation.bind(this)));
    this.subscribe(BackgroundGeolocation.onMotionChange(this.onMotionChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onGeofence(this.onGeofence.bind(this)));
    this.subscribe(BackgroundGeolocation.onActivityChange(this.onActivityChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onHttp(this.onHttp.bind(this)));
    this.subscribe(BackgroundGeolocation.onProviderChange(this.onProviderChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onPowerSaveChange(this.onPowerSaveChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onConnectivityChange(this.onConnectivityChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onAuthorization(this.onAuthorization.bind(this)));

    // Compose #url: tracker.transistorsoft.com/locations/{username}
    const orgname = (await Storage.get({key: 'orgname'})).value;
    const username = (await Storage.get({key: 'username'})).value;

    const token:TransistorAuthorizationToken = await BackgroundGeolocation.findOrCreateTransistorAuthorizationToken(
      orgname,
      username,
      environment.TRACKER_HOST
    );

    // Step 2:  Configure the plugin
    BackgroundGeolocation.ready({
      reset: true,
      debug: true,
      logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,
      distanceFilter: 10,
      stopTimeout: 1,
      stopOnTerminate: false,
      startOnBoot: true,
      url: environment.TRACKER_HOST + '/api/locations',
      // [Android] backgroundPermissionRationale for Always permission.
      backgroundPermissionRationale: {
        title: "Allow {applicationName} to access this device's location even when closed or not in use.",
        message: "This app collects location data to enable recording your trips to work and calculate distance-travelled.",
        positiveAction: 'Change to "{backgroundPermissionOptionLabel}"',
        negativeAction: 'Cancel'
      },
      authorization: {  // <-- JWT authorization for tracker.transistorsoft.com
        strategy: 'jwt',
        accessToken: token.accessToken,
        refreshToken: token.refreshToken,
        refreshUrl: environment.TRACKER_HOST + '/api/refresh_token',
        refreshPayload: {
          refresh_token: '{refreshToken}'
        },
        expires: token.expires
      },
      autoSync: true
    }).then((state) => {
      // Update UI state (toggle switch, changePace button)
      this.addEvent('State', new Date(), state);
      this.zone.run(() => {
        this.isMoving = state.isMoving;
        this.enabled = state.enabled;
      });
    });
  }
  // Return to Home screen (app switcher)
  onClickHome() {
    this.navCtrl.navigateBack('/home');
  }

  // #start / #stop tracking
  onToggleEnabled() {
    if (this.enabled) {
      BackgroundGeolocation.start();
    } else {
      BackgroundGeolocation.stop();
    }
  }

  // Fetch the current position
  onClickGetCurrentPosition() {
    BackgroundGeolocation.getCurrentPosition({persist: true}, (location) => {
      console.log('- getCurrentPosition: ', location);
    }, (error) => {
      console.warn('- Location error: ', error);
    });
  }

  // Change plugin state between stationary / tracking
  onClickChangePace() {
    this.isMoving = !this.isMoving;
    BackgroundGeolocation.changePace(this.isMoving);
  }

  // Clear the list of events from ion-list
  onClickClear() {
    this.events = [];
  }

  /// @event enabledchange
  onEnabledChange(enabled:boolean) {
    this.isMoving = false;
    this.addEvent('onEnabledChange', new Date(), {enabled: enabled});
  }

  /// @event location
  onLocation(location:Location) {
    console.log('[event] location: ', location);
    this.addEvent('onLocation', new Date(location.timestamp), location);

  }

  /// @event motionchange
  onMotionChange(event:MotionChangeEvent) {
    console.log('[event] motionchange, isMoving: ', event.isMoving, ', location: ', event.location);
    this.addEvent('onMotionChange', new Date(event.location.timestamp), event);
    this.isMoving = event.isMoving;
  }

  /// @event activitychange
  onActivityChange(event:MotionActivityEvent) {
    console.log('[event] activitychange: ', event);
    this.addEvent('onActivityChange', new Date(), event);
  }

  /// @event geofence
  onGeofence(event:GeofenceEvent) {
    console.log('[event] geofence: ', event);
    this.addEvent('onGeofence', new Date(event.location.timestamp), event);
  }
  /// @event http
  onHttp(response:HttpEvent) {
    console.log('[event] http: ', response);
    this.addEvent('onHttp', new Date(), response);
  }

  /// @event providerchange
  onProviderChange(provider:ProviderChangeEvent) {
    console.log('[event] providerchange', provider);
    this.addEvent('onProviderChange', new Date(), provider);
  }

  /// @event powersavechange
  onPowerSaveChange(isPowerSaveEnabled:boolean) {
    console.log('[event] powersavechange', isPowerSaveEnabled);
    this.addEvent('onPowerSaveChange', new Date(), {isPowerSaveEnabled: isPowerSaveEnabled});
  }
  /// @event connectivitychange
  onConnectivityChange(event:ConnectivityChangeEvent) {
    console.log('[event] connectivitychange connected? ', event.connected);
    this.addEvent('onConnectivityChange', new Date(), event);
  }

  /// @event authorization
  onAuthorization(event:AuthorizationEvent) {
    console.log('[event] authorization: ', event);
  }

  /// Add a record to ion-list
  private addEvent(name, date, event) {
    const timestamp = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
    this.zone.run(() => {
      this.events.push({
        name: name,
        timestamp: timestamp,
        object: event,
        content: JSON.stringify(event, null, 2)
      });
    })
  }
}


subscribe(subscription:Subscription) {
    this.subscriptions.push(subscription);
  }


unsubscribe() {
    this.subscriptions.forEach((subscription) => subscription.remove() );
    this.subscriptions = [];
  }


ngAfterContentInit() {
    console.log('⚙ ngAfterContentInit');
    this.configureBackgroundGeolocation();
  }


ionViewWillEnter() {
    console.log('⚙ ionViewWillEnter');
  }


ngOnDestroy() {
    this.unsubscribe();
  }


async configureBackgroundGeolocation() {
    // Step 1:  Listen to BackgroundGeolocation events.
    this.subscribe(BackgroundGeolocation.onEnabledChange(this.onEnabledChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onLocation(this.onLocation.bind(this)));
    this.subscribe(BackgroundGeolocation.onMotionChange(this.onMotionChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onGeofence(this.onGeofence.bind(this)));
    this.subscribe(BackgroundGeolocation.onActivityChange(this.onActivityChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onHttp(this.onHttp.bind(this)));
    this.subscribe(BackgroundGeolocation.onProviderChange(this.onProviderChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onPowerSaveChange(this.onPowerSaveChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onConnectivityChange(this.onConnectivityChange.bind(this)));
    this.subscribe(BackgroundGeolocation.onAuthorization(this.onAuthorization.bind(this)));

    // Compose #url: tracker.transistorsoft.com/locations/{username}
    const orgname = (await Storage.get({key: 'orgname'})).value;
    const username = (await Storage.get({key: 'username'})).value;

    const token:TransistorAuthorizationToken = await BackgroundGeolocation.findOrCreateTransistorAuthorizationToken(
      orgname,
      username,
      environment.TRACKER_HOST
    );

    // Step 2:  Configure the plugin
    BackgroundGeolocation.ready({
      reset: true,
      debug: true,
      logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,
      distanceFilter: 10,
      stopTimeout: 1,
      stopOnTerminate: false,
      startOnBoot: true,
      url: environment.TRACKER_HOST + '/api/locations',
      // [Android] backgroundPermissionRationale for Always permission.
      backgroundPermissionRationale: {
        title: "Allow {applicationName} to access this device's location even when closed or not in use.",
        message: "This app collects location data to enable recording your trips to work and calculate distance-travelled.",
        positiveAction: 'Change to "{backgroundPermissionOptionLabel}"',
        negativeAction: 'Cancel'
      },
      authorization: {  // <-- JWT authorization for tracker.transistorsoft.com
        strategy: 'jwt',
        accessToken: token.accessToken,
        refreshToken: token.refreshToken,
        refreshUrl: environment.TRACKER_HOST + '/api/refresh_token',
        refreshPayload: {
          refresh_token: '{refreshToken}'
        },
        expires: token.expires
      },
      autoSync: true
    }).then((state) => {
      // Update UI state (toggle switch, changePace button)
      this.addEvent('State', new Date(), state);
      this.zone.run(() => {
        this.isMoving = state.isMoving;
        this.enabled = state.enabled;
      });
    });
  }


// Return to Home screen (app switcher)
onClickHome() {
    this.navCtrl.navigateBack('/home');
  }


// #start / #stop tracking
onToggleEnabled() {
    if (this.enabled) {
      BackgroundGeolocation.start();
    } else {
      BackgroundGeolocation.stop();
    }
  }


// Fetch the current position
onClickGetCurrentPosition() {
    BackgroundGeolocation.getCurrentPosition({persist: true}, (location) => {
      console.log('- getCurrentPosition: ', location);
    }, (error) => {
      console.warn('- Location error: ', error);
    });
  }


// Change plugin state between stationary / tracking
onClickChangePace() {
    this.isMoving = !this.isMoving;
    BackgroundGeolocation.changePace(this.isMoving);
  }


// Clear the list of events from ion-list
onClickClear() {
    this.events = [];
  }


/// @event enabledchange
onEnabledChange(enabled:boolean) {
    this.isMoving = false;
    this.addEvent('onEnabledChange', new Date(), {enabled: enabled});
  }


/// @event location
onLocation(location:Location) {
    console.log('[event] location: ', location);
    this.addEvent('onLocation', new Date(location.timestamp), location);

  }


/// @event motionchange
onMotionChange(event:MotionChangeEvent) {
    console.log('[event] motionchange, isMoving: ', event.isMoving, ', location: ', event.location);
    this.addEvent('onMotionChange', new Date(event.location.timestamp), event);
    this.isMoving = event.isMoving;
  }


/// @event activitychange
onActivityChange(event:MotionActivityEvent) {
    console.log('[event] activitychange: ', event);
    this.addEvent('onActivityChange', new Date(), event);
  }


/// @event geofence
onGeofence(event:GeofenceEvent) {
    console.log('[event] geofence: ', event);
    this.addEvent('onGeofence', new Date(event.location.timestamp), event);
  }


/// @event http
onHttp(response:HttpEvent) {
    console.log('[event] http: ', response);
    this.addEvent('onHttp', new Date(), response);
  }


/// @event providerchange
onProviderChange(provider:ProviderChangeEvent) {
    console.log('[event] providerchange', provider);
    this.addEvent('onProviderChange', new Date(), provider);
  }


/// @event powersavechange
onPowerSaveChange(isPowerSaveEnabled:boolean) {
    console.log('[event] powersavechange', isPowerSaveEnabled);
    this.addEvent('onPowerSaveChange', new Date(), {isPowerSaveEnabled: isPowerSaveEnabled});
  }


/// @event connectivitychange
onConnectivityChange(event:ConnectivityChangeEvent) {
    console.log('[event] connectivitychange connected? ', event.connected);
    this.addEvent('onConnectivityChange', new Date(), event);
  }


/// @event authorization
onAuthorization(event:AuthorizationEvent) {
    console.log('[event] authorization: ', event);
  }


/// Add a record to ion-list
private addEvent(name, date, event) {
    const timestamp = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
    this.zone.run(() => {
      this.events.push({
        name: name,
        timestamp: timestamp,
        object: event,
        content: JSON.stringify(event, null, 2)
      });
    })
  }


export default function Home(partner: any) {
  const router = useRouter();
  const { locale, defaultLocale } = router;
  const homeData: any = home;
  const benefitsData: any = benefits;
  const partnersData: any = partners;
  const stepsData: any = steps;
  const faqData: any = faq;

  return (
    <MainLayout>
      <Head>
        <title>{homeData[locale!].pageTitle}</title>
        <meta name="description" content={homeData[locale!].pageMeta}></meta>
      </Head>
      <Script src="https://cadirect-widget.netlify.app/cadwidget-v1.0.js" />
      <Hero
        title={homeData[locale!].title}
        subtitle={homeData[locale!].subtitle}
        img={homeData[locale!].bannerImg}
        partner={partner.partner}
      />
      <Icons data={benefitsData[locale!].benefits} />
      <Steps title={stepsData[locale!].title} data={stepsData[locale!].steps} />
      <Faq title={faqData[locale!].title} data={faqData[locale!].accordions} />
      <Partners
        data={partnersData[defaultLocale!]}
        title={partnersData[locale!].title}
      />
    </MainLayout>
  );
}


export class AddonEnabledRepositoryImpl implements AddonEnabledRepository {
  set(on: boolean): void {
    enabled = on;
  }

  get(): boolean {
    return enabled;
  }
}


export default interface AddonEnabledRepository {
  set(on: boolean): void;

  get(): boolean;
}


set(on: boolean): void {
    enabled = on;
  }


get(): boolean {
    return enabled;
  }


function initLinkSrv() {
    const _dashboard: any = {
      time: { from: 'now-6h', to: 'now' },
      getTimezone: jest.fn(() => 'browser'),
      timeRangeUpdated: () => {},
    };

    const timeSrv = new TimeSrv({} as any);
    timeSrv.init(_dashboard);
    timeSrv.setTime({ from: 'now-1h', to: 'now' });
    _dashboard.refresh = false;
    setTimeSrv(timeSrv);

    templateSrv = initTemplateSrv('key', [
      { type: 'query', name: 'home', current: { value: '127.0.0.1' } },
      { type: 'query', name: 'server1', current: { value: '192.168.0.100' } },
    ]);

    setTemplateSrv(templateSrv);

    linkSrv = new LinkSrv();
  }


() => ({
  appEvents: {
    subscribe: () => {},
  },
})


() => {
  let linkSrv: LinkSrv;
  let templateSrv: TemplateSrv;
  let originalTimeService: TimeSrv;

  function initLinkSrv() {
    const _dashboard: any = {
      time: { from: 'now-6h', to: 'now' },
      getTimezone: jest.fn(() => 'browser'),
      timeRangeUpdated: () => {},
    };

    const timeSrv = new TimeSrv({} as any);
    timeSrv.init(_dashboard);
    timeSrv.setTime({ from: 'now-1h', to: 'now' });
    _dashboard.refresh = false;
    setTimeSrv(timeSrv);

    templateSrv = initTemplateSrv('key', [
      { type: 'query', name: 'home', current: { value: '127.0.0.1' } },
      { type: 'query', name: 'server1', current: { value: '192.168.0.100' } },
    ]);

    setTemplateSrv(templateSrv);

    linkSrv = new LinkSrv();
  }

  beforeAll(() => {
    originalTimeService = getTimeSrv();
    variableAdapters.register(createQueryVariableAdapter());
  });

  beforeEach(() => {
    initLinkSrv();

    jest.resetAllMocks();
  });

  afterAll(() => {
    setTimeSrv(originalTimeService);
  });

  describe('getDataLinkUIModel', () => {
    describe('built in variables', () => {
      it('should not trim white space from data links', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'White space',
              url: 'www.google.com?query=some query',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=some query');
      });

      it('should remove new lines from data link', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'New line',
              url: 'www.google.com?query=some\nquery',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=somequery');
      });
    });

    describe('sanitization', () => {
      const url = "javascript:alert('broken!);";
      it.each`
        disableSanitizeHtml | expected
        ${true}             | ${url}
        ${false}            | ${'about:blank'}
      `(
        "when disable disableSanitizeHtml set to '$disableSanitizeHtml' then result should be '$expected'",
        ({ disableSanitizeHtml, expected }) => {
          updateConfig({
            disableSanitizeHtml,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    });

    describe('Building links with root_url set', () => {
      it.each`
        url                 | appSubUrl     | expected
        ${'/d/XXX'}         | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'/grafana/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${'/grafana'} | ${'d/whatever'}
        ${'/d/XXX'}         | ${''}         | ${'/d/XXX'}
        ${'/grafana/d/XXX'} | ${''}         | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${''}         | ${'d/whatever'}
      `(
        "when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'",
        ({ url, appSubUrl, expected }) => {
          locationUtil.initialize({
            config: { appSubUrl } as any,
            getVariablesUrlParams: (() => {}) as any,
            getTimeRangeForUrl: (() => {}) as any,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    });
  });

  describe('getAnchorInfo', () => {
    it('returns variable values for variable names in link.href and link.tooltip', () => {
      jest.spyOn(linkSrv, 'getLinkUrl');
      jest.spyOn(templateSrv, 'replace');

      expect(linkSrv.getLinkUrl).toBeCalledTimes(0);
      expect(templateSrv.replace).toBeCalledTimes(0);

      const link = linkSrv.getAnchorInfo({
        type: 'link',
        icon: 'dashboard',
        tags: [],
        url: '/graph?home=$home',
        title: 'Visit home',
        tooltip: 'Visit ${home:raw}',
      });

      expect(linkSrv.getLinkUrl).toBeCalledTimes(1);
      expect(templateSrv.replace).toBeCalledTimes(3);
      expect(link).toStrictEqual({ href: '/graph?home=127.0.0.1', title: 'Visit home', tooltip: 'Visit 127.0.0.1' });
    });
  });

  describe('getLinkUrl', () => {
    it('converts link urls', () => {
      const linkUrl = linkSrv.getLinkUrl({
        url: '/graph',
      });
      const linkUrlWithVar = linkSrv.getLinkUrl({
        url: '/graph?home=$home',
      });

      expect(linkUrl).toBe('/graph');
      expect(linkUrlWithVar).toBe('/graph?home=127.0.0.1');
    });

    it('appends current dashboard time range if keepTime is true', () => {
      const anchorInfoKeepTime = linkSrv.getLinkUrl({
        keepTime: true,
        url: '/graph',
      });

      expect(anchorInfoKeepTime).toBe('/graph?from=now-1h&to=now');
    });

    it('adds all variables to the url if includeVars is true', () => {
      const anchorInfoIncludeVars = linkSrv.getLinkUrl({
        includeVars: true,
        url: '/graph',
      });

      expect(anchorInfoIncludeVars).toBe('/graph?var-home=127.0.0.1&var-server1=192.168.0.100');
    });

    it('respects config disableSanitizeHtml', () => {
      const anchorInfo = {
        url: 'javascript:alert(document.domain)',
      };

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe('about:blank');

      updateConfig({
        disableSanitizeHtml: true,
      });

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe(anchorInfo.url);
    });
  });
}


() => 'browser'


() => {
    originalTimeService = getTimeSrv();
    variableAdapters.register(createQueryVariableAdapter());
  }


() => {
    initLinkSrv();

    jest.resetAllMocks();
  }


() => {
    setTimeSrv(originalTimeService);
  }


() => {
    describe('built in variables', () => {
      it('should not trim white space from data links', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'White space',
              url: 'www.google.com?query=some query',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=some query');
      });

      it('should remove new lines from data link', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'New line',
              url: 'www.google.com?query=some\nquery',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=somequery');
      });
    });

    describe('sanitization', () => {
      const url = "javascript:alert('broken!);";
      it.each`
        disableSanitizeHtml | expected
        ${true}             | ${url}
        ${false}            | ${'about:blank'}
      `(
        "when disable disableSanitizeHtml set to '$disableSanitizeHtml' then result should be '$expected'",
        ({ disableSanitizeHtml, expected }) => {
          updateConfig({
            disableSanitizeHtml,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    });

    describe('Building links with root_url set', () => {
      it.each`
        url                 | appSubUrl     | expected
        ${'/d/XXX'}         | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'/grafana/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${'/grafana'} | ${'d/whatever'}
        ${'/d/XXX'}         | ${''}         | ${'/d/XXX'}
        ${'/grafana/d/XXX'} | ${''}         | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${''}         | ${'d/whatever'}
      `(
        "when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'",
        ({ url, appSubUrl, expected }) => {
          locationUtil.initialize({
            config: { appSubUrl } as any,
            getVariablesUrlParams: (() => {}) as any,
            getTimeRangeForUrl: (() => {}) as any,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    });
  }


() => {
      it('should not trim white space from data links', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'White space',
              url: 'www.google.com?query=some query',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=some query');
      });

      it('should remove new lines from data link', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'New line',
              url: 'www.google.com?query=some\nquery',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=somequery');
      });
    }


() => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'White space',
              url: 'www.google.com?query=some query',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=some query');
      }


() => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'New line',
              url: 'www.google.com?query=some\nquery',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=somequery');
      }


() => {
      const url = "javascript:alert('broken!);";
      it.each`
        disableSanitizeHtml | expected
        ${true}             | ${url}
        ${false}            | ${'about:blank'}
      `(
        "when disable disableSanitizeHtml set to '$disableSanitizeHtml' then result should be '$expected'",
        ({ disableSanitizeHtml, expected }) => {
          updateConfig({
            disableSanitizeHtml,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    }


({ disableSanitizeHtml, expected }) => {
          updateConfig({
            disableSanitizeHtml,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }


() => {
      it.each`
        url                 | appSubUrl     | expected
        ${'/d/XXX'}         | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'/grafana/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${'/grafana'} | ${'d/whatever'}
        ${'/d/XXX'}         | ${''}         | ${'/d/XXX'}
        ${'/grafana/d/XXX'} | ${''}         | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${''}         | ${'d/whatever'}
      `(
        "when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'",
        ({ url, appSubUrl, expected }) => {
          locationUtil.initialize({
            config: { appSubUrl } as any,
            getVariablesUrlParams: (() => {}) as any,
            getTimeRangeForUrl: (() => {}) as any,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    }


({ url, appSubUrl, expected }) => {
          locationUtil.initialize({
            config: { appSubUrl } as any,
            getVariablesUrlParams: (() => {}) as any,
            getTimeRangeForUrl: (() => {}) as any,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }


() => {
    it('returns variable values for variable names in link.href and link.tooltip', () => {
      jest.spyOn(linkSrv, 'getLinkUrl');
      jest.spyOn(templateSrv, 'replace');

      expect(linkSrv.getLinkUrl).toBeCalledTimes(0);
      expect(templateSrv.replace).toBeCalledTimes(0);

      const link = linkSrv.getAnchorInfo({
        type: 'link',
        icon: 'dashboard',
        tags: [],
        url: '/graph?home=$home',
        title: 'Visit home',
        tooltip: 'Visit ${home:raw}',
      });

      expect(linkSrv.getLinkUrl).toBeCalledTimes(1);
      expect(templateSrv.replace).toBeCalledTimes(3);
      expect(link).toStrictEqual({ href: '/graph?home=127.0.0.1', title: 'Visit home', tooltip: 'Visit 127.0.0.1' });
    });
  }


() => {
      jest.spyOn(linkSrv, 'getLinkUrl');
      jest.spyOn(templateSrv, 'replace');

      expect(linkSrv.getLinkUrl).toBeCalledTimes(0);
      expect(templateSrv.replace).toBeCalledTimes(0);

      const link = linkSrv.getAnchorInfo({
        type: 'link',
        icon: 'dashboard',
        tags: [],
        url: '/graph?home=$home',
        title: 'Visit home',
        tooltip: 'Visit ${home:raw}',
      });

      expect(linkSrv.getLinkUrl).toBeCalledTimes(1);
      expect(templateSrv.replace).toBeCalledTimes(3);
      expect(link).toStrictEqual({ href: '/graph?home=127.0.0.1', title: 'Visit home', tooltip: 'Visit 127.0.0.1' });
    }


() => {
    it('converts link urls', () => {
      const linkUrl = linkSrv.getLinkUrl({
        url: '/graph',
      });
      const linkUrlWithVar = linkSrv.getLinkUrl({
        url: '/graph?home=$home',
      });

      expect(linkUrl).toBe('/graph');
      expect(linkUrlWithVar).toBe('/graph?home=127.0.0.1');
    });

    it('appends current dashboard time range if keepTime is true', () => {
      const anchorInfoKeepTime = linkSrv.getLinkUrl({
        keepTime: true,
        url: '/graph',
      });

      expect(anchorInfoKeepTime).toBe('/graph?from=now-1h&to=now');
    });

    it('adds all variables to the url if includeVars is true', () => {
      const anchorInfoIncludeVars = linkSrv.getLinkUrl({
        includeVars: true,
        url: '/graph',
      });

      expect(anchorInfoIncludeVars).toBe('/graph?var-home=127.0.0.1&var-server1=192.168.0.100');
    });

    it('respects config disableSanitizeHtml', () => {
      const anchorInfo = {
        url: 'javascript:alert(document.domain)',
      };

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe('about:blank');

      updateConfig({
        disableSanitizeHtml: true,
      });

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe(anchorInfo.url);
    });
  }


() => {
      const linkUrl = linkSrv.getLinkUrl({
        url: '/graph',
      });
      const linkUrlWithVar = linkSrv.getLinkUrl({
        url: '/graph?home=$home',
      });

      expect(linkUrl).toBe('/graph');
      expect(linkUrlWithVar).toBe('/graph?home=127.0.0.1');
    }


() => {
      const anchorInfoKeepTime = linkSrv.getLinkUrl({
        keepTime: true,
        url: '/graph',
      });

      expect(anchorInfoKeepTime).toBe('/graph?from=now-1h&to=now');
    }


() => {
      const anchorInfoIncludeVars = linkSrv.getLinkUrl({
        includeVars: true,
        url: '/graph',
      });

      expect(anchorInfoIncludeVars).toBe('/graph?var-home=127.0.0.1&var-server1=192.168.0.100');
    }


() => {
      const anchorInfo = {
        url: 'javascript:alert(document.domain)',
      };

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe('about:blank');

      updateConfig({
        disableSanitizeHtml: true,
      });

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe(anchorInfo.url);
    }


() => {
    it('then it should return correct suggestions', () => {
      const frame = toDataFrame({
        name: 'indoor',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'temperature', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      const suggestions = getDataFrameVars([frame]);

      expect(suggestions).toEqual([
        {
          value: '__data.fields.time',
          label: 'time',
          documentation: `Formatted value for time on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: '__data.fields.temperature',
          label: 'temperature',
          documentation: `Formatted value for temperature on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields[0]`,
          label: `Select by index`,
          documentation: `Enter the field order`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields.temperature.numeric`,
          label: `Show numeric value`,
          documentation: `the numeric field value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields.temperature.text`,
          label: `Show text value`,
          documentation: `the text value`,
          origin: VariableOrigin.Fields,
        },
      ]);
    });
  }


() => {
      const frame = toDataFrame({
        name: 'indoor',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'temperature', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      const suggestions = getDataFrameVars([frame]);

      expect(suggestions).toEqual([
        {
          value: '__data.fields.time',
          label: 'time',
          documentation: `Formatted value for time on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: '__data.fields.temperature',
          label: 'temperature',
          documentation: `Formatted value for temperature on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields[0]`,
          label: `Select by index`,
          documentation: `Enter the field order`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields.temperature.numeric`,
          label: `Show numeric value`,
          documentation: `the numeric field value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields.temperature.text`,
          label: `Show text value`,
          documentation: `the text value`,
          origin: VariableOrigin.Fields,
        },
      ]);
    }


() => {
    it('then it should return correct suggestions', () => {
      const frame = toDataFrame({
        name: 'temperatures',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      const suggestions = getDataFrameVars([frame]);

      expect(suggestions).toEqual([
        {
          value: '__data.fields.time',
          label: 'time',
          documentation: `Formatted value for time on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: '__data.fields["temperature.indoor"]',
          label: 'temperature.indoor',
          documentation: `Formatted value for temperature.indoor on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields[0]`,
          label: `Select by index`,
          documentation: `Enter the field order`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["temperature.indoor"].numeric`,
          label: `Show numeric value`,
          documentation: `the numeric field value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["temperature.indoor"].text`,
          label: `Show text value`,
          documentation: `the text value`,
          origin: VariableOrigin.Fields,
        },
      ]);
    });
  }