/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @emails react-core
 */

'use strict';

let React;
let Scheduler;
// let ReactCache;
let ReactDOM;
let ReactDOMClient;
// let Suspense;
let originalCreateElement;
// let TextResource;
// let textResourceShouldFail;
let originalHTMLImageElementSrcDescriptor;

let images = [];
let onLoadSpy = null;
let actualLoadSpy = null;

let waitForAll;
let waitFor;
let assertLog;

function PhaseMarkers({children}) {
  Scheduler.log('render start');
  React.useLayoutEffect(() => {
    Scheduler.log('last layout');
  });
  React.useEffect(() => {
    Scheduler.log('last passive');
  });
  return children;
}

function last(arr) {
  if (Array.isArray(arr)) {
    if (arr.length) {
      return arr[arr.length - 1];
    }
    return undefined;
  }
  throw new Error('last was passed something that was not an array');
}

function Text(props) {
  Scheduler.log(props.text);
  return props.text;
}

// function AsyncText(props) {
//   const text = props.text;
//   try {
//     TextResource.read([props.text, props.ms]);
//     Scheduler.log(text);
//     return text;
//   } catch (promise) {
//     if (typeof promise.then === 'function') {
//       Scheduler.log(`Suspend! [${text}]`);
//     } else {
//       Scheduler.log(`Error! [${text}]`);
//     }
//     throw promise;
//   }
// }

function Img({src: maybeSrc, onLoad, useImageLoader, ref}) {
  const src = maybeSrc || 'default';
  Scheduler.log('Img ' + src);
  return <img src={src} onLoad={onLoad} />;
}

function Yield() {
  Scheduler.log('Yield');
  Scheduler.unstable_requestPaint();
  return null;
}

function loadImage(element) {
  const event = new Event('load');
  element.__needsDispatch = false;
  element.dispatchEvent(event);
}

describe('ReactDOMImageLoad', () => {
  beforeEach(() => {
    jest.resetModules();
    React = require('react');
    Scheduler = require('scheduler');
    // ReactCache = require('react-cache');
    ReactDOM = require('react-dom');
    ReactDOMClient = require('react-dom/client');
    // Suspense = React.Suspense;

    const InternalTestUtils = require('internal-test-utils');
    waitForAll = InternalTestUtils.waitForAll;
    waitFor = InternalTestUtils.waitFor;
    assertLog = InternalTestUtils.assertLog;

    onLoadSpy = jest.fn(reactEvent => {
      const src = reactEvent.target.getAttribute('src');
      Scheduler.log('onLoadSpy [' + src + ']');
    });

    actualLoadSpy = jest.fn(nativeEvent => {
      const src = nativeEvent.target.getAttribute('src');
      Scheduler.log('actualLoadSpy [' + src + ']');
      nativeEvent.__originalDispatch = false;
    });

    // TextResource = ReactCache.unstable_createResource(
    //   ([text, ms = 0]) => {
    //     let listeners = null;
    //     let status = 'pending';
    //     let value = null;
    //     return {
    //       then(resolve, reject) {
    //         switch (status) {
    //           case 'pending': {
    //             if (listeners === null) {
    //               listeners = [{resolve, reject}];
    //               setTimeout(() => {
    //                 if (textResourceShouldFail) {
    //                   Scheduler.log(
    //                     `Promise rejected [${text}]`,
    //                   );
    //                   status = 'rejected';
    //                   value = new Error('Failed to load: ' + text);
    //                   listeners.forEach(listener => listener.reject(value));
    //                 } else {
    //                   Scheduler.log(
    //                     `Promise resolved [${text}]`,
    //                   );
    //                   status = 'resolved';
    //                   value = text;
    //                   listeners.forEach(listener => listener.resolve(value));
    //                 }
    //               }, ms);
    //             } else {
    //               listeners.push({resolve, reject});
    //             }
    //             break;
    //           }
    //           case 'resolved': {
    //             resolve(value);
    //             break;
    //           }
    //           case 'rejected': {
    //             reject(value);
    //             break;
    //           }
    //         }
    //       },
    //     };
    //   },
    //   ([text, ms]) => text,
    // );
    // textResourceShouldFail = false;

    images = [];

    originalCreateElement = document.createElement;
    document.createElement = function createElement(tagName, options) {
      const element = originalCreateElement.call(document, tagName, options);
      if (tagName === 'img') {
        element.addEventListener('load', actualLoadSpy);
        images.push(element);
      }
      return element;
    };

    originalHTMLImageElementSrcDescriptor = Object.getOwnPropertyDescriptor(
      HTMLImageElement.prototype,
      'src',
    );

    Object.defineProperty(HTMLImageElement.prototype, 'src', {
      get() {
        return this.getAttribute('src');
      },
      set(value) {
        Scheduler.log('load triggered');
        this.__needsDispatch = true;
        this.setAttribute('src', value);
      },
    });
  });

  afterEach(() => {
    document.createElement = originalCreateElement;
    Object.defineProperty(
      HTMLImageElement.prototype,
      'src',
      originalHTMLImageElementSrcDescriptor,
    );
  });

  it('captures the load event if it happens before commit phase and replays it between layout and passive effects', async function () {
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    React.startTransition(() =>
      root.render(
        <PhaseMarkers>
          <Img onLoad={onLoadSpy} />
          <Yield />
          <Text text={'a'} />
        </PhaseMarkers>,
      ),
    );

    await waitFor(['render start', 'Img default', 'Yield']);
    const img = last(images);
    loadImage(img);
    assertLog([
      'actualLoadSpy [default]',
      // no onLoadSpy since we have not completed render
    ]);
    await waitForAll(['a', 'load triggered', 'last layout', 'last passive']);
    expect(img.__needsDispatch).toBe(true);
    loadImage(img);
    assertLog([
      'actualLoadSpy [default]', // the browser reloading of the image causes this to yield again
      'onLoadSpy [default]',
    ]);
    expect(onLoadSpy).toHaveBeenCalled();
  });

  it('captures the load event if it happens after commit phase and replays it', async function () {
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    React.startTransition(() =>
      root.render(
        <PhaseMarkers>
          <Img onLoad={onLoadSpy} />
        </PhaseMarkers>,
      ),
    );

    await waitFor([
      'render start',
      'Img default',
      'load triggered',
      'last layout',
    ]);
    Scheduler.unstable_requestPaint();
    const img = last(images);
    loadImage(img);
    assertLog(['actualLoadSpy [default]', 'onLoadSpy [default]']);
    await waitForAll(['last passive']);
    expect(img.__needsDispatch).toBe(false);
    expect(onLoadSpy).toHaveBeenCalledTimes(1);
  });

  it('replays the last load event when more than one fire before the end of the layout phase completes', async function () {
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function Base() {
      const [src, setSrc] = React.useState('a');
      return (
        <PhaseMarkers>
          <Img src={src} onLoad={onLoadSpy} />
          <Yield />
          <UpdateSrc setSrc={setSrc} />
        </PhaseMarkers>
      );
    }

    function UpdateSrc({setSrc}) {
      React.useLayoutEffect(() => {
        setSrc('b');
      }, [setSrc]);
      return null;
    }

    React.startTransition(() => root.render(<Base />));

    await waitFor(['render start', 'Img a', 'Yield']);
    const img = last(images);
    loadImage(img);
    assertLog(['actualLoadSpy [a]']);

    await waitFor([
      'load triggered',
      'last layout',
      // the update in layout causes a passive effects flush before a sync render
      'last passive',
      'render start',
      'Img b',
      'Yield',
      // yield is ignored becasue we are sync rendering
      'last layout',
      'last passive',
    ]);
    expect(images.length).toBe(1);
    loadImage(img);
    assertLog(['actualLoadSpy [b]', 'onLoadSpy [b]']);
    expect(onLoadSpy).toHaveBeenCalledTimes(1);
  });

  it('replays load events that happen in passive phase after the passive phase.', async function () {
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    root.render(
      <PhaseMarkers>
        <Img onLoad={onLoadSpy} />
      </PhaseMarkers>,
    );

    await waitForAll([
      'render start',
      'Img default',
      'load triggered',
      'last layout',
      'last passive',
    ]);
    const img = last(images);
    loadImage(img);
    assertLog(['actualLoadSpy [default]', 'onLoadSpy [default]']);
    expect(onLoadSpy).toHaveBeenCalledTimes(1);
  });

  it('captures and suppresses the load event if it happens before passive effects and a cascading update causes the img to be removed', async function () {
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function ChildSuppressing({children}) {
      const [showChildren, update] = React.useState(true);
      React.useLayoutEffect(() => {
        if (showChildren) {
          update(false);
        }
      }, [showChildren]);
      return showChildren ? children : null;
    }

    React.startTransition(() =>
      root.render(
        <PhaseMarkers>
          <ChildSuppressing>
            <Img onLoad={onLoadSpy} />
            <Yield />
            <Text text={'a'} />
          </ChildSuppressing>
        </PhaseMarkers>,
      ),
    );

    await waitFor(['render start', 'Img default', 'Yield']);
    const img = last(images);
    loadImage(img);
    assertLog(['actualLoadSpy [default]']);
    await waitForAll(['a', 'load triggered', 'last layout', 'last passive']);
    expect(img.__needsDispatch).toBe(true);
    loadImage(img);
    // we expect the browser to load the image again but since we are no longer rendering
    // the img there will be no onLoad called
    assertLog(['actualLoadSpy [default]']);
    await waitForAll([]);
    expect(onLoadSpy).not.toHaveBeenCalled();
  });

  it('captures and suppresses the load event if it happens before passive effects and a cascading update causes the img to be removed, alternate', async function () {
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function Switch({children}) {
      const [shouldShow, updateShow] = React.useState(true);
      return children(shouldShow, updateShow);
    }

    function UpdateSwitchInLayout({updateShow}) {
      React.useLayoutEffect(() => {
        updateShow(false);
      }, []);
      return null;
    }

    React.startTransition(() =>
      root.render(
        <Switch>
          {(shouldShow, updateShow) => (
            <PhaseMarkers>
              <>
                {shouldShow === true ? (
                  <>
                    <Img onLoad={onLoadSpy} />
                    <Yield />
                    <Text text={'a'} />
                  </>
                ) : null}
                ,
                <UpdateSwitchInLayout updateShow={updateShow} />
              </>
            </PhaseMarkers>
          )}
        </Switch>,
      ),
    );

    await waitFor([
      // initial render
      'render start',
      'Img default',
      'Yield',
    ]);
    const img = last(images);
    loadImage(img);
    assertLog(['actualLoadSpy [default]']);
    await waitForAll([
      'a',
      'load triggered',
      // img is present at first
      'last layout',
      'last passive',
      // sync re-render where the img is suppressed
      'render start',
      'last layout',
      'last passive',
    ]);
    expect(img.__needsDispatch).toBe(true);
    loadImage(img);
    // we expect the browser to load the image again but since we are no longer rendering
    // the img there will be no onLoad called
    assertLog(['actualLoadSpy [default]']);
    await waitForAll([]);
    expect(onLoadSpy).not.toHaveBeenCalled();
  });

  // eslint-disable-next-line jest/no-commented-out-tests
  // it('captures the load event if it happens in a suspended subtree and replays it between layout and passive effects on resumption', async function() {
  //   function SuspendingWithImage() {
  //     Scheduler.log('SuspendingWithImage');
  //     return (
  //       <Suspense fallback={<Text text="Loading..." />}>
  //         <AsyncText text="A" ms={100} />
  //         <PhaseMarkers>
  //           <Img onLoad={onLoadSpy} />
  //         </PhaseMarkers>
  //       </Suspense>
  //     );
  //   }

  //   const container = document.createElement('div');
  //   const root = ReactDOMClient.createRoot(container);

  //   React.startTransition(() => root.render(<SuspendingWithImage />));

  //   expect(Scheduler).toFlushAndYield([
  //     'SuspendingWithImage',
  //     'Suspend! [A]',
  //     'render start',
  //     'Img default',
  //     'Loading...',
  //   ]);
  //   let img = last(images);
  //   loadImage(img);
  //   expect(Scheduler).toHaveYielded(['actualLoadSpy [default]']);
  //   expect(onLoadSpy).not.toHaveBeenCalled();

  //   // Flush some of the time
  //   jest.advanceTimersByTime(50);
  //   // Still nothing...
  //   expect(Scheduler).toFlushWithoutYielding();

  //   // Flush the promise completely
  //   jest.advanceTimersByTime(50);
  //   // Renders successfully
  //   expect(Scheduler).toHaveYielded(['Promise resolved [A]']);

  //   expect(Scheduler).toFlushAndYieldThrough([
  //     'A',
  //     // img was recreated on unsuspended tree causing new load event
  //     'render start',
  //     'Img default',
  //     'last layout',
  //   ]);

  //   expect(images.length).toBe(2);
  //   img = last(images);
  //   expect(img.__needsDispatch).toBe(true);
  //   loadImage(img);
  //   expect(Scheduler).toHaveYielded([
  //     'actualLoadSpy [default]',
  //     'onLoadSpy [default]',
  //   ]);

  //   expect(Scheduler).toFlushAndYield(['last passive']);

  //   expect(onLoadSpy).toHaveBeenCalledTimes(1);
  // });

  it('correctly replays the last img load even when a yield + update causes the host element to change', async function () {
    let externalSetSrc = null;
    let externalSetSrcAlt = null;

    function Base() {
      const [src, setSrc] = React.useState(null);
      const [srcAlt, setSrcAlt] = React.useState(null);
      externalSetSrc = setSrc;
      externalSetSrcAlt = setSrcAlt;
      return srcAlt || src ? <YieldingWithImage src={srcAlt || src} /> : null;
    }

    function YieldingWithImage({src}) {
      Scheduler.log('YieldingWithImage');
      React.useEffect(() => {
        Scheduler.log('Committed');
      });
      return (
        <>
          <Img src={src} onLoad={onLoadSpy} />
          <Yield />
          <Text text={src} />
        </>
      );
    }

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    root.render(<Base />);

    await waitForAll([]);

    React.startTransition(() => externalSetSrc('a'));

    await waitFor(['YieldingWithImage', 'Img a', 'Yield']);
    let img = last(images);
    loadImage(img);
    assertLog(['actualLoadSpy [a]']);

    ReactDOM.flushSync(() => externalSetSrcAlt('b'));

    assertLog([
      'YieldingWithImage',
      'Img b',
      'Yield',
      'b',
      'load triggered',
      'Committed',
    ]);
    expect(images.length).toBe(2);
    img = last(images);
    expect(img.__needsDispatch).toBe(true);
    loadImage(img);

    assertLog(['actualLoadSpy [b]', 'onLoadSpy [b]']);
    // why is there another update here?
    await waitForAll(['YieldingWithImage', 'Img b', 'Yield', 'b', 'Committed']);
  });

  it('preserves the src property / attribute when triggering a potential new load event', async () => {
    // this test covers a regression identified in https://github.com/mui/material-ui/pull/31263
    // where the resetting of the src property caused the property to change from relative to fully qualified

    // make sure we are not using the patched src setter
    Object.defineProperty(
      HTMLImageElement.prototype,
      'src',
      originalHTMLImageElementSrcDescriptor,
    );

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    React.startTransition(() =>
      root.render(
        <PhaseMarkers>
          <Img onLoad={onLoadSpy} />
          <Yield />
          <Text text={'a'} />
        </PhaseMarkers>,
      ),
    );

    // render to yield to capture state of img src attribute and property before commit
    await waitFor(['render start', 'Img default', 'Yield']);
    const img = last(images);
    const renderSrcProperty = img.src;
    const renderSrcAttr = img.getAttribute('src');

    // finish render and commit causing the src property to be rewritten
    await waitForAll(['a', 'last layout', 'last passive']);
    const commitSrcProperty = img.src;
    const commitSrcAttr = img.getAttribute('src');

    // ensure attribute and properties agree
    expect(renderSrcProperty).toBe(commitSrcProperty);
    expect(renderSrcAttr).toBe(commitSrcAttr);
  });
});
