/**
 * Names of clock methods that may be faked by install.
 */
export type FakeMethod =
    | "setTimeout"
    | "clearTimeout"
    | "setImmediate"
    | "clearImmediate"
    | "setInterval"
    | "clearInterval"
    | "Date"
    | "nextTick"
    | "hrtime"
    | "requestAnimationFrame"
    | "cancelAnimationFrame"
    | "requestIdleCallback"
    | "cancelIdleCallback"
    | "performance"
    | "queueMicrotask";

/**
 * Global methods available to every clock and also as standalone methods (inside `timers` global object).
 */
export interface GlobalTimers<TTimerId extends TimerId> {
    /**
     * Schedules a callback to be fired once timeout milliseconds have ticked by.
     *
     * @param callback   Callback to be fired.
     * @param timeout   How many ticks to wait to run the callback.
     * @param args   Any extra arguments to pass to the callback.
     * @returns Time identifier for cancellation.
     */
    setTimeout: (callback: (...args: any[]) => void, timeout: number, ...args: any[]) => TTimerId;

    /**
     * Clears a timer, as long as it was created using setTimeout.
     *
     * @param id   Timer ID or object.
     */
    clearTimeout: (id: TTimerId) => void;

    /**
     * Schedules a callback to be fired every time timeout milliseconds have ticked by.
     *
     * @param callback   Callback to be fired.
     * @param timeout   How many ticks to wait between callbacks.
     * @param args   Any extra arguments to pass to the callback.
     * @returns Time identifier for cancellation.
     */
    setInterval: (callback: (...args: any[]) => void, timeout: number, ...args: any[]) => TTimerId;

    /**
     * Clears a timer, as long as it was created using setInterval.
     *
     * @param id   Timer ID or object.
     */
    clearInterval: (id: TTimerId) => void;

    /**
     * Schedules the callback to be fired once 0 milliseconds have ticked by.
     *
     * @param callback   Callback to be fired.
     * @param args   Any extra arguments to pass to the callback.
     * @remarks You'll still have to call clock.tick() for the callback to fire.
     * @remarks If called during a tick the callback won't fire until 1 millisecond has ticked by.
     */
    setImmediate: (callback: (...args: any[]) => void, ...args: any[]) => TTimerId;

    /**
     * Clears a timer, as long as it was created using setImmediate.
     *
     * @param id   Timer ID or object.
     */
    clearImmediate: (id: TTimerId) => void;

    /**
     * Implements the Date object but using this clock to provide the correct time.
     */
    Date: typeof Date;

    /**
     * Mimics process.nextTick() explicitly dropping additional arguments.
     */
    queueMicrotask: (callback: () => void) => void;
}

/**
 * Timer object used in node.
 */
export interface NodeTimer {
    /**
     * Marks the timer as referenced.
     */
    ref(): NodeTimer;

    /**
     * Marks the timer as unreferenced.
     */
    unref(): NodeTimer;

    /**
     * Refreshes the timer.
     */
    refresh(): NodeTimer;

    /**
     * Returns true if the timer will keep the event loop active.
     */
    hasRef(): boolean;
}

/**
 * Timer identifier for clock scheduling.
 */
export type TimerId = number | NodeTimer;

/**
 * Allows configuring how the clock advances time, automatically or manually.
 *
 * - `manual`: Timers do not advance without explicit, manual calls to the tick APIs (`clock.nextAsync`, `clock.runAllAsync`, etc).
 * - `nextAsync`: The clock will continuously break the event loop, then run the next timer until the mode changes.
 * - `interval`: This is the same as specifying `shouldAdvanceTime: true` with an `advanceTimeDelta`. If the delta is not specified, 20 will be used by default.
 */
export type TimerTickMode =
    | { mode: "manual" }
    | { mode: "nextAsync" }
    | { mode: "interval"; delta?: number };

/**
 * Controls the flow of time.
 */
export interface FakeClock<TTimerId extends TimerId> extends GlobalTimers<TTimerId> {
    /**
     * Current clock time.
     */
    now: number;

    /**
     * Mimics performance.now().
     */
    performance: {
        now: () => number;
    };

    /**
     * Don't know what this prop is for, but it was included in the clocks that `createClock` or
     * `install` return (it is never used in the code, for now).
     */
    timeouts: {};

    /**
     * Maximum number of timers that will be run when calling runAll().
     */
    loopLimit: number;

    /**
     * Schedule callback to run in the next animation frame.
     *
     * @param callback   Callback to be fired.
     * @returns Request id.
     */
    requestAnimationFrame: (callback: (time: number) => void) => TTimerId;

    /**
     * Cancel animation frame request.
     *
     * @param id   The id returned from requestAnimationFrame method.
     */
    cancelAnimationFrame: (id: TTimerId) => void;

    /**
     * Queues the callback to be fired during idle periods to perform background and low priority work on the main event loop.
     *
     * @param callback   Callback to be fired.
     * @param timeout   The maximum number of ticks before the callback must be fired.
     * @remarks Callbacks which have a timeout option will be fired no later than time in milliseconds.
     */
    requestIdleCallback: (callback: () => void, timeout?: number) => TTimerId;

    /**
     * Clears a timer, as long as it was created using requestIdleCallback.
     *
     * @param id   Timer ID or object.
     */
    cancelIdleCallback: (id: TTimerId) => void;

    /**
     * Get the number of waiting timers.
     *
     * @returns number of waiting timers.
     */
    countTimers: () => number;

    /**
     * Advances the clock to the the moment of the first scheduled timer, firing it.
     * @returns Fake milliseconds since the unix epoch.
     */
    next: () => number;

    /**
     * Advances the clock to the the moment of the first scheduled timer, firing it.
     *
     * Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
     * @returns Fake milliseconds since the unix epoch.
     */
    nextAsync: () => Promise<number>;

    /**
     * Advance the clock, firing callbacks if necessary.
     *
     * @param time   How many ticks to advance by.
     * @returns Fake milliseconds since the unix epoch.
     */
    tick: (time: number | string) => number;

    /**
     * Advance the clock, firing callbacks if necessary.
     *
     * Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
     *
     * @param time   How many ticks to advance by.
     * @returns Fake milliseconds since the unix epoch.
     */
    tickAsync: (time: number | string) => Promise<number>;

    /**
     * Removes all timers and tick without firing them and restore now to its original value.
     */
    reset: () => void;

    /**
     * Runs all pending timers until there are none remaining.
     *
     * @remarks  If new timers are added while it is executing they will be run as well.
     * @returns Fake milliseconds since the unix epoch.
     */
    runAll: () => number;

    /**
     * Runs all pending timers until there are none remaining.
     *
     * Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
     *
     * @remarks  If new timers are added while it is executing they will be run as well.
     * @returns Fake milliseconds since the unix epoch.
     */
    runAllAsync: () => Promise<number>;

    /**
     * Advanced the clock to the next animation frame while firing all scheduled callbacks.
     * @returns Fake milliseconds since the unix epoch.
     */
    runToFrame: () => number;

    /**
     * Takes note of the last scheduled timer when it is run, and advances the clock to
     * that time firing callbacks as necessary.
     * @returns Fake milliseconds since the unix epoch.
     */
    runToLast: () => number;

    /**
     * Takes note of the last scheduled timer when it is run, and advances the clock to
     * that time firing callbacks as necessary.
     *
     * Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
     * @returns Fake milliseconds since the unix epoch.
     */
    runToLastAsync: () => Promise<number>;

    /**
     * Advances the clock by `time` milliseconds.
     *
     * Any timers within the affected range will be moved to the end of the range.
     * Then, the clock will be advanced by `time` milliseconds, firing any timers
     * that are now due.
     *
     * @param time   How many ticks to advance by.
     * @returns Fake milliseconds since the unix epoch.
     */
    jump: (time: number | string) => number;

    /**
     * Simulates a user changing the system clock.
     *
     * @param now   New system time.
     * @remarks This affects the current time but it does not in itself cause timers to fire.
     */
    setSystemTime: (now?: number | Date) => void;

    /**
     * Allows configuring how the clock advances time, automatically or manually.
     * @param tickModeConfig The new configuration for how the clock should tick.
     */
    setTickMode: (tickModeConfig: TimerTickMode) => void;

    /**
     * Run all pending microtasks scheduled with `queueMicrotask`.
     */
    runMicrotasks: () => void;
}

/**
 * Fake clock for a browser environment.
 */
export type BrowserClock = FakeClock<number>;

/**
 * Fake clock for a Node environment.
 */
export type NodeClock = FakeClock<NodeTimer> & {
    /**
     * Mimics process.hrtime().
     *
     * @param prevTime   Previous system time to calculate time elapsed.
     * @returns High resolution real time as [seconds, nanoseconds].
     */
    hrtime(prevTime?: [number, number]): [number, number];

    /**
     * Simulates process.nextTick().
     */
    nextTick: (callback: (...args: any[]) => void, ...args: any[]) => void;
};

/**
 * Clock object created by @sinonjs/fake-timers.
 */
export type Clock = BrowserClock | NodeClock;

/**
 * Additional methods that installed clock have.
 */
export interface InstalledMethods {
    /**
     * Restores the original methods on the context that was passed to FakeTimers.install,
     * or the native timers if no context was given.
     */
    uninstall: () => void;

    methods: FakeMethod[];
}

/**
 * Clock object created by calling `install();`.
 */
export type InstalledClock = Clock & InstalledMethods;

/**
 * Creates a clock.
 *
 * @param now   Current time for the clock.
 * @param loopLimit    Maximum number of timers that will be run when calling runAll()
 *                     before assuming that we have an infinite loop and throwing an error
 *                     (by default, 1000).
 * @remarks The default epoch is 0.
 */
export function createClock(now?: number | Date, loopLimit?: number): Clock;

export interface FakeTimerInstallOpts {
    /**
     * Installs fake timers with the specified unix epoch (default: 0)
     */
    now?: number | Date | undefined;

    /**
     * An array with names of global methods and APIs to fake. By default, `@sinonjs/fake-timers` does not replace `nextTick()` and `queueMicrotask()`.
     * For instance, `FakeTimers.install({ toFake: ['setTimeout', 'nextTick'] })` will fake only `setTimeout()` and `nextTick()`
     */
    toFake?: FakeMethod[] | undefined;

    /**
     * The maximum number of timers that will be run when calling runAll() (default: 1000)
     */
    loopLimit?: number | undefined;

    /**
     * Tells @sinonjs/fake-timers to increment mocked time automatically based on the real system time shift (e.g. the mocked time will be incremented by
     * 20ms for every 20ms change in the real system time) (default: false)
     */
    shouldAdvanceTime?: boolean | undefined;

    /**
     * Relevant only when using with shouldAdvanceTime: true. increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change
     * in the real system time (default: 20)
     */
    advanceTimeDelta?: number | undefined;

    /**
     * Tells FakeTimers to clear 'native' (i.e. not fake) timers by delegating to their respective handlers. These are not cleared by
     * default, leading to potentially unexpected behavior if timers existed prior to installing FakeTimers. (default: false)
     */
    shouldClearNativeTimers?: boolean | undefined;

    /**
     * Tells FakeTimers to not throw an error when faking a timer that does not exist in the global object. (default: false)
     */
    ignoreMissingTimers?: boolean | undefined;
}

/**
 * Creates a clock and installs it globally.
 *
 * @param [opts]   Options for the fake timer.
 */
export function install(opts?: FakeTimerInstallOpts): InstalledClock;

export interface FakeTimerWithContext {
    timers: GlobalTimers<TimerId>;
    createClock: (now?: number | Date, loopLimit?: number) => Clock;
    install: (opts?: FakeTimerInstallOpts) => InstalledClock;
    withGlobal: (global: object) => FakeTimerWithContext;
}

/**
 * Apply new context to fake timers.
 *
 * @param global   New context to apply like `window` (in browsers) or `global` (in node).
 */
export function withGlobal(global: object): FakeTimerWithContext;

export const timers: GlobalTimers<TimerId>;
