/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */

import { waitFor, waitForElementToBeRemoved, screen, render, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

beforeEach(() => {
  jest.clearAllMocks();
});

describe('Basic scenario', () => {
  beforeEach(() => {
    simpleTimer.callCount = 0;
  });

  async function simpleTimer(callback: any) {
    simpleTimer.callCount += 1;

    if (simpleTimer.callCount > 4) {
      return;
    }

    // await is the microtask boundary before scheduling the next timer
    // jest fake timer calls do not advance the microtask queue
    // only macrotasks (setTimeout, setInterval) are advanced
    // even if you use runAllTimers or advanceTimersByTime
    // so the await here means the next setTimeout is never scheduled
    await callback();
    setTimeout(() => {
      simpleTimer(callback);
    }, 1000);
  }

  simpleTimer.callCount = 0;

  const callback = jest.fn();

  it('fakeTimers dont work well with promise microtasks queues', async () => {
    jest.useFakeTimers();

    await simpleTimer(callback);
    jest.advanceTimersByTime(4000);

    expect(callback).toHaveBeenCalledTimes(4);

    jest.runOnlyPendingTimers();
    jest.useRealTimers();
  });

  it('real timers with waitFor work fine with microtasks', async () => {
    await simpleTimer(callback);

    await waitFor(() => {
      expect(callback).toHaveBeenCalledTimes(4);
    });
  });
});

describe('Component scenarios', () => {
  const api = {
    save: jest.fn().mockResolvedValue(null),
  };

  function AutoSaveForm() {
    const [status, setStatus] = React.useState<'idle' | 'saving' | 'saved'>('idle');

    async function onChange(value: string) {
      setStatus('saving');
      await api.save(value); // microtask boundary before scheduling the next timer
      setStatus('saved');
      setTimeout(() => setStatus('idle'), 1000);
    }

    return (
      <>
        <label htmlFor="name">Name</label>
        <input id="name" onChange={(e) => onChange(e.target.value)} />
        {status !== 'idle' && <output>{status}</output>}
      </>
    );
  }

  describe('Failures with fake timers', () => {
    beforeEach(() => {
      jest.clearAllMocks();
      jest.useFakeTimers();
    });

    afterEach(() => {
      jest.runOnlyPendingTimers();
      jest.useRealTimers();
    });

    it('shows and hides saved toasts', async () => {
      render(<AutoSaveForm />);

      await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc');

      act(() => {
        jest.runAllTimers();
      });

      // Still here: because microtasks are not handled
      expect(screen.getByRole('status')).toHaveTextContent(/saved/i);
    });

    it('shows and hides saved toasts (fake timers + waitForElementToBeRemoved)', async () => {
      render(<AutoSaveForm />);

      await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc');

      act(() => {
        jest.runAllTimers();
      });

      // Even waiting for it to be removed times out
      // Because fake timers mess up the async scheduling
      await waitForElementToBeRemoved(() => screen.queryByRole('status')); // times out
    });

    it('shows and hides saved toats (fake timers + waitFor)', async () => {
      render(<AutoSaveForm />);

      await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc');

      act(() => {
        jest.runAllTimers();
      });

      // Even waitFor(() => ...) times out when paired with fake timers
      // Because of the same reason as above
      await waitFor(() => expect(screen.getByRole('status')).not.toHaveTextContent(/saved/i));
    });
  });

  describe('Success', () => {
    it('shows and hides saved toast after async save (real timers)', async () => {
      render(<AutoSaveForm />);

      await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc');

      // or waitFor(() => expect(screen.getByText('saved', { selector: 'output' })).toBeInTheDocument())
      // same thing
      expect(await screen.findByText('saved', { selector: 'output' })).toBeInTheDocument();

      await waitForElementToBeRemoved(() => screen.queryByRole('status'));
    });
  });
});
