import {
  AllPublishOptions,
  asArray,
  CancellationToken,
  newError,
  PublishConfiguration,
  UpdateInfo,
  UUID,
  DownloadOptions,
  CancellationError
} from "builder-util-runtime";
import { randomBytes } from "crypto";
import { Notification } from "electron";
import { EventEmitter } from "events";
import { ensureDir, outputFile, readFile, rename, unlink } from "fs-extra";
import { OutgoingHttpHeaders } from "http";
import { safeLoad } from "js-yaml";
import { Lazy } from "lazy-val";
import * as path from "path";
import {
  eq as isVersionsEqual,
  gt as isVersionGreaterThan,
  lt as isVersionLessThan,
  parse as parseVersion,
  prerelease as getVersionPreleaseComponents,
  SemVer
} from "semver";
import { AppAdapter } from "./AppAdapter";
import {
  createTempUpdateFile,
  DownloadedUpdateHelper
} from "./DownloadedUpdateHelper";
import { ElectronAppAdapter } from "./ElectronAppAdapter";
import { ElectronHttpExecutor, getNetSession } from "./electronHttpExecutor";
import { GenericProvider } from "./providers/GenericProvider";
import {
  DOWNLOAD_PROGRESS,
  Logger,
  Provider,
  ResolvedUpdateFileInfo,
  UPDATE_DOWNLOADED,
  UpdateCheckResult,
  UpdateDownloadedEvent,
  UpdaterSignal
} from "./main";
import {
  createClient,
  isUrlProbablySupportMultiRangeRequests
} from "./providerFactory";
import { ProviderPlatform } from "./providers/Provider";
import { isZipAvailabeForDifferentialDownload } from "./prepareAppZip";
import Session = Electron.Session;

export abstract class AppUpdater extends EventEmitter {
  /**
   * Whether to automatically download an update when it is found.
   */
  autoDownload: boolean = true;

  /**
   * Whether to automatically install a downloaded update on app quit (if `quitAndInstall` was not called before).
   *
   * Applicable only on Windows and Linux.
   */
  autoInstallOnAppQuit: boolean = true;

  /**
   * *GitHub provider only.* Whether to allow update to pre-release versions. Defaults to `true` if application version contains prerelease components (e.g. `0.12.1-alpha.1`, here `alpha` is a prerelease component), otherwise `false`.
   *
   * If `true`, downgrade will be allowed (`allowDowngrade` will be set to `true`).
   */
  allowPrerelease: boolean = false;

  /**
   * *GitHub provider only.* Get all release notes (from current version to latest), not just the latest.
   * @default false
   */
  fullChangelog: boolean = false;

  /**
   * Whether to allow version downgrade (when a user from the beta channel wants to go back to the stable channel).
   *
   * Taken in account only if channel differs (pre-release version component in terms of semantic versioning).
   *
   * @default false
   */
  allowDowngrade: boolean = false;

  /**
   * The current application version.
   */
  readonly currentVersion: SemVer;

  private _channel: string | null = null;

  protected downloadedUpdateHelper: DownloadedUpdateHelper | null = null;

  /**
   * Get the update channel. Not applicable for GitHub. Doesn't return `channel` from the update configuration, only if was previously set.
   */
  get channel(): string | null {
    return this._channel;
  }

  /**
   * Set the update channel. Not applicable for GitHub. Overrides `channel` in the update configuration.
   *
   * `allowDowngrade` will be automatically set to `true`. If this behavior is not suitable for you, simple set `allowDowngrade` explicitly after.
   */
  set channel(value: string | null) {
    if (this._channel != null) {
      // noinspection SuspiciousTypeOfGuard
      if (typeof value !== "string") {
        throw newError(
          `Channel must be a string, but got: ${value}`,
          "ERR_UPDATER_INVALID_CHANNEL"
        );
      } else if (value.length === 0) {
        throw newError(
          `Channel must be not an empty string`,
          "ERR_UPDATER_INVALID_CHANNEL"
        );
      }
    }

    this._channel = value;
    this.allowDowngrade = true;
  }

  /**
   *  The request headers.
   */
  requestHeaders: OutgoingHttpHeaders | null = null;

  protected _logger: Logger = console;

  // noinspection JSMethodCanBeStatic,JSUnusedGlobalSymbols
  get netSession(): Session {
    return getNetSession();
  }

  /**
   * The logger. You can pass [electron-log](https://github.com/megahertz/electron-log), [winston](https://github.com/winstonjs/winston) or another logger with the following interface: `{ info(), warn(), error() }`.
   * Set it to `null` if you would like to disable a logging feature.
   */
  get logger(): Logger | null {
    return this._logger;
  }

  set logger(value: Logger | null) {
    this._logger = value == null ? new NoOpLogger() : value;
  }

  // noinspection JSUnusedGlobalSymbols
  /**
   * For type safety you can use signals, e.g. `autoUpdater.signals.updateDownloaded(() => {})` instead of `autoUpdater.on('update-available', () => {})`
   */
  readonly signals = new UpdaterSignal(this);

  private _appUpdateConfigPath: string | null = null;

  // noinspection JSUnusedGlobalSymbols
  /**
   * test only
   * @private
   */
  set updateConfigPath(value: string | null) {
    this.clientPromise = null;
    this._appUpdateConfigPath = value;
    this.configOnDisk = new Lazy<any>(() => this.loadUpdateConfig());
  }

  private clientPromise: Promise<Provider<any>> | null = null;

  protected readonly stagingUserIdPromise = new Lazy<string>(() =>
    this.getOrCreateStagingUserId()
  );

  // public, allow to read old config for anyone
  /** @internal */
  configOnDisk = new Lazy<any>(() => this.loadUpdateConfig());

  private checkForUpdatesPromise: Promise<UpdateCheckResult> | null = null;

  protected readonly app: AppAdapter;

  protected updateInfoAndProvider: UpdateInfoAndProvider | null = null;

  /** @internal */
  readonly httpExecutor: ElectronHttpExecutor;

  protected constructor(
    options: AllPublishOptions | null | undefined,
    app?: AppAdapter
  ) {
    super();

    this.on("error", (error: Error) => {
      this._logger.error(`Error: ${error.stack || error.message}`);
    });

    if (app == null) {
      this.app = new ElectronAppAdapter();
      this.httpExecutor = new ElectronHttpExecutor((authInfo, callback) =>
        this.emit("login", authInfo, callback)
      );
    } else {
      this.app = app;
      this.httpExecutor = null as any;
    }

    const currentVersionString = this.app.version;
    const currentVersion = parseVersion(currentVersionString);
    if (currentVersion == null) {
      throw newError(
        `App version is not a valid semver version: "${currentVersionString}"`,
        "ERR_UPDATER_INVALID_VERSION"
      );
    }
    this.currentVersion = currentVersion;
    this.allowPrerelease = hasPrereleaseComponents(currentVersion);

    if (options != null) {
      this.setFeedURL(options);

      if (typeof options !== "string" && options.requestHeaders) {
        this.requestHeaders = options.requestHeaders;
      }
    }
  }

  //noinspection JSMethodCanBeStatic,JSUnusedGlobalSymbols
  getFeedURL(): string | null | undefined {
    return "Deprecated. Do not use it.";
  }

  /**
   * Configure update provider. If value is `string`, [GenericServerOptions](/configuration/publish#genericserveroptions) will be set with value as `url`.
   * @param options If you want to override configuration in the `app-update.yml`.
   */
  setFeedURL(options: PublishConfiguration | AllPublishOptions | string) {
    const runtimeOptions = this.createProviderRuntimeOptions();
    // https://github.com/electron-userland/electron-builder/issues/1105
    let provider: Provider<any>;
    if (typeof options === "string") {
      provider = new GenericProvider(
        { provider: "generic", url: options },
        this,
        {
          ...runtimeOptions,
          isUseMultipleRangeRequest: isUrlProbablySupportMultiRangeRequests(
            options
          )
        }
      );
    } else {
      provider = createClient(options, this, runtimeOptions);
    }
    this.clientPromise = Promise.resolve(provider);
  }

  /**
   * Asks the server whether there is an update.
   */
  checkForUpdates(): Promise<UpdateCheckResult | null> {
    if (!isZipAvailabeForDifferentialDownload()) {
      this.emit(
        "error",
        "Configuring update for differential download",
        "Cannot check for updates"
      );
      return Promise.reject("Configuring update for differential download. Please try after some time");
    }
    let checkForUpdatesPromise = this.checkForUpdatesPromise;
    if (checkForUpdatesPromise != null) {
      this._logger.info("Checking for update (already in progress)");
      return checkForUpdatesPromise;
    }

    const nullizePromise = () => (this.checkForUpdatesPromise = null);

    this._logger.info("Checking for update");
    checkForUpdatesPromise = this.doCheckForUpdates()
      .then(it => {
        nullizePromise();
        return it;
      })
      .catch(e => {
        nullizePromise();
        this.emit(
          "error",
          e,
          `Cannot check for updates: ${(e.stack || e).toString()}`
        );
        throw e;
      });

    this.checkForUpdatesPromise = checkForUpdatesPromise;
    return checkForUpdatesPromise;
  }

  public isUpdaterActive(): boolean {
    if (!this.app.isPackaged) {
      this._logger.info(
        "Skip checkForUpdatesAndNotify because application is not packed"
      );
      return false;
    }
    return true;
  }

  // noinspection JSUnusedGlobalSymbols
  checkForUpdatesAndNotify(
    downloadNotification?: DownloadNotification
  ): Promise<UpdateCheckResult | null> {
    if (!this.isUpdaterActive()) {
      return Promise.resolve(null);
    }

    return this.checkForUpdates().then(it => {
      const downloadPromise = it && it.downloadPromise;
      if (downloadPromise == null) {
        const debug = this._logger.debug;
        if (debug != null) {
          debug("checkForUpdatesAndNotify called, downloadPromise is null");
        }
        return it;
      }

      downloadPromise.then(() => {
        const notificationContent = this.formatDownloadNotification(
          it ? it.updateInfo.version : "",
          this.app.name,
          downloadNotification
        );
        new Notification(notificationContent).show();
      });

      return it;
    });
  }

  private formatDownloadNotification(
    version: string,
    appName: string,
    downloadNotification?: DownloadNotification
  ): DownloadNotification {
    if (downloadNotification == null) {
      downloadNotification = {
        title: "A new update is ready to install",
        body: `{appName} version {version} has been downloaded and will be automatically installed on exit`
      };
    }
    downloadNotification = {
      title: downloadNotification.title
        .replace("{appName}", appName)
        .replace("{version}", version),
      body: downloadNotification.body
        .replace("{appName}", appName)
        .replace("{version}", version)
    };
    return downloadNotification;
  }

  private async isStagingMatch(updateInfo: UpdateInfo): Promise<boolean> {
    const rawStagingPercentage = updateInfo.stagingPercentage;
    let stagingPercentage = rawStagingPercentage;
    if (stagingPercentage == null) {
      return true;
    }

    stagingPercentage = parseInt(stagingPercentage as any, 10);
    if (isNaN(stagingPercentage)) {
      this._logger.warn(`Staging percentage is NaN: ${rawStagingPercentage}`);
      return true;
    }

    // convert from user 0-100 to internal 0-1
    stagingPercentage = stagingPercentage / 100;

    const stagingUserId = await this.stagingUserIdPromise.value;
    const val = UUID.parse(stagingUserId).readUInt32BE(12);
    const percentage = val / 0xffffffff;
    this._logger.info(
      `Staging percentage: ${stagingPercentage}, percentage: ${percentage}, user id: ${stagingUserId}`
    );
    return percentage < stagingPercentage;
  }

  private computeFinalHeaders(headers: OutgoingHttpHeaders) {
    if (this.requestHeaders != null) {
      Object.assign(headers, this.requestHeaders);
    }
    return headers;
  }

  private async isUpdateAvailable(updateInfo: UpdateInfo): Promise<boolean> {
    const latestVersion = parseVersion(updateInfo.version);
    if (latestVersion == null) {
      throw newError(
        `This file could not be downloaded, or the latest version (from update server) does not have a valid semver version: "${updateInfo.version}"`,
        "ERR_UPDATER_INVALID_VERSION"
      );
    }

    const currentVersion = this.currentVersion;
    if (isVersionsEqual(latestVersion, currentVersion)) {
      return false;
    }

    const isStagingMatch = await this.isStagingMatch(updateInfo);
    if (!isStagingMatch) {
      return false;
    }

    // https://github.com/electron-userland/electron-builder/pull/3111#issuecomment-405033227
    // https://github.com/electron-userland/electron-builder/pull/3111#issuecomment-405030797
    const isLatestVersionNewer = isVersionGreaterThan(
      latestVersion,
      currentVersion
    );
    const isLatestVersionOlder = isVersionLessThan(
      latestVersion,
      currentVersion
    );

    if (isLatestVersionNewer) {
      return true;
    }
    return this.allowDowngrade && isLatestVersionOlder;
  }

  public async getUpdateInfoAndProvider(): Promise<UpdateInfoAndProvider> {
    await this.app.whenReady();

    if (this.clientPromise == null) {
      this.clientPromise = this.configOnDisk.value.then(it =>
        createClient(it, this, this.createProviderRuntimeOptions())
      );
    }

    const client = await this.clientPromise;
    const stagingUserId = await this.stagingUserIdPromise.value;
    client.setRequestHeaders(
      this.computeFinalHeaders({ "x-user-staging-id": stagingUserId })
    );
    return {
      info: await client.getLatestVersion(),
      provider: client
    };
  }

  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
  private createProviderRuntimeOptions() {
    return {
      isUseMultipleRangeRequest: true,
      platform:
        this._testOnlyOptions == null
          ? (process.platform as ProviderPlatform)
          : this._testOnlyOptions.platform,
      executor: this.httpExecutor
    };
  }

  private async doCheckForUpdates(): Promise<UpdateCheckResult> {
    this.emit("checking-for-update");

    const result = await this.getUpdateInfoAndProvider();
    const updateInfo = result.info;
    if (!(await this.isUpdateAvailable(updateInfo))) {
      this._logger.info(
        `Update for version ${
          this.currentVersion
        } is not available (latest version: ${
          updateInfo.version
        }, downgrade is ${this.allowDowngrade ? "allowed" : "disallowed"}).`
      );
      this.emit("update-not-available", updateInfo);
      return {
        versionInfo: updateInfo,
        updateInfo
      };
    }

    this.updateInfoAndProvider = result;
    this.onUpdateAvailable(updateInfo);

    const cancellationToken = new CancellationToken();
    //noinspection ES6MissingAwait
    return {
      versionInfo: updateInfo,
      updateInfo,
      cancellationToken,
      downloadPromise: this.autoDownload
        ? this.downloadUpdate(cancellationToken)
        : null
    };
  }

  protected onUpdateAvailable(updateInfo: UpdateInfo): void {
    this._logger.info(
      `Found version ${updateInfo.version} (url: ${asArray(updateInfo.files)
        .map(it => it.url)
        .join(", ")})`
    );
    this.emit("update-available", updateInfo);
  }

  /**
   * Start downloading update manually. You can use this method if `autoDownload` option is set to `false`.
   * @returns {Promise<string>} Path to downloaded file.
   */
  downloadUpdate(
    cancellationToken: CancellationToken = new CancellationToken()
  ): Promise<any> {
    const updateInfoAndProvider = this.updateInfoAndProvider;
    if (updateInfoAndProvider == null) {
      const error = new Error("Please check update first");
      this.dispatchError(error);
      return Promise.reject(error);
    }

    this._logger.info(
      `Downloading update from ${asArray(updateInfoAndProvider.info.files)
        .map(it => it.url)
        .join(", ")}`
    );
    const errorHandler = (e: Error): Error => {
      // https://github.com/electron-userland/electron-builder/issues/1150#issuecomment-436891159
      if (!(e instanceof CancellationError)) {
        try {
          this.dispatchError(e);
        } catch (nestedError) {
          this._logger.warn(
            `Cannot dispatch error event: ${nestedError.stack || nestedError}`
          );
        }
      }

      return e;
    };

    try {
      return this.doDownloadUpdate({
        updateInfoAndProvider,
        requestHeaders: this.computeRequestHeaders(
          updateInfoAndProvider.provider
        ),
        cancellationToken
      }).catch(e => {
        throw errorHandler(e);
      });
    } catch (e) {
      return Promise.reject(errorHandler(e));
    }
  }

  protected dispatchError(e: Error): void {
    this.emit("error", e, (e.stack || e).toString());
  }

  protected dispatchUpdateDownloaded(event: UpdateDownloadedEvent): void {
    this.emit(UPDATE_DOWNLOADED, event);
  }

  protected abstract async doDownloadUpdate(
    downloadUpdateOptions: DownloadUpdateOptions
  ): Promise<Array<string>>;

  /**
   * Restarts the app and installs the update after it has been downloaded.
   * It should only be called after `update-downloaded` has been emitted.
   *
   * **Note:** `autoUpdater.quitAndInstall()` will close all application windows first and only emit `before-quit` event on `app` after that.
   * This is different from the normal quit event sequence.
   *
   * @param isSilent *windows-only* Runs the installer in silent mode. Defaults to `false`.
   * @param isForceRunAfter Run the app after finish even on silent install. Not applicable for macOS. Ignored if `isSilent` is set to `false`.
   */
  abstract quitAndInstall(isSilent?: boolean, isForceRunAfter?: boolean): void;

  private async loadUpdateConfig(): Promise<any> {
    if (this._appUpdateConfigPath == null) {
      this._appUpdateConfigPath = this.app.appUpdateConfigPath;
    }
    return safeLoad(await readFile(this._appUpdateConfigPath, "utf-8"));
  }

  private computeRequestHeaders(provider: Provider<any>): OutgoingHttpHeaders {
    const fileExtraDownloadHeaders = provider.fileExtraDownloadHeaders;
    if (fileExtraDownloadHeaders != null) {
      const requestHeaders = this.requestHeaders;
      return requestHeaders == null
        ? fileExtraDownloadHeaders
        : {
            ...fileExtraDownloadHeaders,
            ...requestHeaders
          };
    }
    return this.computeFinalHeaders({ accept: "*/*" });
  }

  private async getOrCreateStagingUserId(): Promise<string> {
    const file = path.join(this.app.userDataPath, ".updaterId");
    try {
      const id = await readFile(file, "utf-8");
      if (UUID.check(id)) {
        return id;
      } else {
        this._logger.warn(
          `Staging user id file exists, but content was invalid: ${id}`
        );
      }
    } catch (e) {
      if (e.code !== "ENOENT") {
        this._logger.warn(
          `Couldn't read staging user ID, creating a blank one: ${e}`
        );
      }
    }

    const id = UUID.v5(randomBytes(4096), UUID.OID);
    this._logger.info(`Generated new staging user ID: ${id}`);
    try {
      await outputFile(file, id);
    } catch (e) {
      this._logger.warn(`Couldn't write out staging user ID: ${e}`);
    }
    return id;
  }

  /** @internal */
  get isAddNoCacheQuery(): boolean {
    const headers = this.requestHeaders;
    // https://github.com/electron-userland/electron-builder/issues/3021
    if (headers == null) {
      return true;
    }

    for (const headerName of Object.keys(headers)) {
      const s = headerName.toLowerCase();
      if (s === "authorization" || s === "private-token") {
        return false;
      }
    }
    return true;
  }

  /**
   * @private
   * @internal
   */
  _testOnlyOptions: TestOnlyUpdaterOptions | null = null;

  private async getOrCreateDownloadHelper(
    useAppSupportCache: any
  ): Promise<DownloadedUpdateHelper> {
    let result = this.downloadedUpdateHelper;

    if (result == null) {
      const dirName = (await this.configOnDisk.value).updaterCacheDirName;
      const logger = this._logger;
      if (dirName == null) {
        logger.error(
          "updaterCacheDirName is not specified in app-update.yml Was app build using at least electron-builder 20.34.0?"
        );
      }

      const cacheDir = path.join(
        useAppSupportCache
          ? this.getAppSupportCacheDir()
          : this.app.baseCachePath,
        dirName || this.app.name
      );

      if (logger.debug != null) {
        logger.debug(`updater cache dir: ${cacheDir}`);
      }

      result = new DownloadedUpdateHelper(cacheDir);
      this.downloadedUpdateHelper = result;
    }
    return result;
  }
  public getAppSupportCacheDir() {
    let result: string;
    const appSupportPath = this.app.userDataPath;
    if (process.platform === "win32") {
      result = appSupportPath;
    } else if (process.platform === "darwin") {
      result = appSupportPath;
    } else {
      result = process.env.XDG_CACHE_HOME || appSupportPath;
    }
    return result;
  }
  protected async executeDownload(
    taskOptions: DownloadExecutorTask
  ): Promise<Array<string>> {
    const fileInfo = taskOptions.fileInfo;
    const downloadOptions: DownloadOptions = {
      headers: taskOptions.downloadUpdateOptions.requestHeaders,
      cancellationToken: taskOptions.downloadUpdateOptions.cancellationToken,
      sha2: (fileInfo.info as any).sha2,
      sha512: fileInfo.info.sha512
    };

    if (this.listenerCount(DOWNLOAD_PROGRESS) > 0) {
      downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it);
    }

    const updateInfo =
      taskOptions.downloadUpdateOptions.updateInfoAndProvider.info;
    const version = updateInfo.version;
    const packageInfo = fileInfo.packageInfo;
    const {
      configuration
    } = taskOptions.downloadUpdateOptions.updateInfoAndProvider.provider;
    function getCacheUpdateFileName(): string {
      // NodeJS URL doesn't decode automatically
      const urlPath = decodeURIComponent(taskOptions.fileInfo.url.pathname);
      if (urlPath.endsWith(`.${taskOptions.fileExtension}`)) {
        return path.posix.basename(urlPath);
      } else {
        // url like /latest, generate name
        return `update.${taskOptions.fileExtension}`;
      }
    }
    const useAppSupportCache = configuration ? configuration.useAppSupportCache : false;
    const downloadedUpdateHelper = await this.getOrCreateDownloadHelper(
      useAppSupportCache
    );
    const cacheDir = downloadedUpdateHelper.cacheDirForPendingUpdate;
    await ensureDir(cacheDir);
    const updateFileName = getCacheUpdateFileName();
    let updateFile = path.join(cacheDir, updateFileName);
    const packageFile =
      packageInfo == null
        ? null
        : path.join(
            cacheDir,
            `package-${version}${path.extname(packageInfo.path) || ".7z"}`
          );

    const done = async (isSaveCache: boolean) => {
      await downloadedUpdateHelper.setDownloadedFile(
        updateFile,
        packageFile,
        updateInfo,
        fileInfo,
        updateFileName,
        isSaveCache
      );
      await taskOptions.done!!({
        ...updateInfo,
        downloadedFile: updateFile
      });
      return packageFile == null ? [updateFile] : [updateFile, packageFile];
    };

    const log = this._logger;
    const cachedUpdateFile = await downloadedUpdateHelper.validateDownloadedPath(
      updateFile,
      updateInfo,
      fileInfo,
      log
    );
    if (cachedUpdateFile != null) {
      updateFile = cachedUpdateFile;
      return await done(false);
    }

    const removeFileIfAny = async () => {
      await downloadedUpdateHelper.clear().catch(() => {
        // ignore
      });
      return await unlink(updateFile).catch(() => {
        // ignore
      });
    };

    const tempUpdateFile = await createTempUpdateFile(
      `temp-${updateFileName}`,
      cacheDir,
      log
    );
    try {
      await taskOptions.task(
        tempUpdateFile,
        downloadOptions,
        packageFile,
        removeFileIfAny
      );
      await rename(tempUpdateFile, updateFile);
    } catch (e) {
      await removeFileIfAny();

      if (e instanceof CancellationError) {
        log.info("cancelled");
        this.emit("update-cancelled", updateInfo);
      }
      throw e;
    }

    log.info(`New version ${version} has been downloaded to ${updateFile}`);
    return await done(true);
  }
}

export interface DownloadUpdateOptions {
  readonly updateInfoAndProvider: UpdateInfoAndProvider;
  readonly requestHeaders: OutgoingHttpHeaders;
  readonly cancellationToken: CancellationToken;
}
export function emitDownloadProgress(
  DOWNLOAD_PROGRESS: string,
  arg1: { progress: string }
) {
  // this.emit(DOWNLOAD_PROGRESS, arg1);
}

function hasPrereleaseComponents(version: SemVer) {
  const versionPrereleaseComponent = getVersionPreleaseComponents(version);
  return (
    versionPrereleaseComponent != null && versionPrereleaseComponent.length > 0
  );
}

/** @private */
export class NoOpLogger implements Logger {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  info(message?: any) {
    // ignore
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  warn(message?: any) {
    // ignore
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  error(message?: any) {
    // ignore
  }
}

export interface UpdateInfoAndProvider {
  info: UpdateInfo;
  provider: any;
}

export interface DownloadExecutorTask {
  readonly fileExtension: string;
  readonly fileInfo: ResolvedUpdateFileInfo;
  readonly downloadUpdateOptions: DownloadUpdateOptions;
  readonly task: (
    destinationFile: string,
    downloadOptions: DownloadOptions,
    packageFile: string | null,
    removeTempDirIfAny: () => Promise<any>
  ) => Promise<any>;

  readonly done?: (event: UpdateDownloadedEvent) => Promise<any>;
}

export interface DownloadNotification {
  body: string;
  title: string;
}

/** @private */
export interface TestOnlyUpdaterOptions {
  platform: ProviderPlatform;

  isUseDifferentialDownload?: boolean;
}