/** @jsxImportSource ../../lepus */

import { Component, createContext, Fragment } from 'preact';
import { useMemo, useState } from 'preact/hooks';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { elementTree, waitSchedule } from './utils/nativeMethod';
import { globalEnvManager } from './utils/envManager';
import { setupDocument } from '../../src/document';
// import { renderOpcodesInto } from '../../src/opcodes';
import renderToStringBase from '../../src/snapshot/renderToOpcodes';
import { setupPage, SnapshotInstance, snapshotInstanceManager } from '../../src/snapshot';
import { createElement, cloneElement } from '../../lepus';
import { Suspense } from 'preact/compat';
import { createSuspender } from './createSuspender';
import { __root } from '../../src/root';

const renderToString = (element, root = __root) => {
  return renderToStringBase(element, null, root);
};

describe('renderToString', () => {
  beforeAll(() => {
    globalEnvManager.switchToMainThread();
  });

  beforeEach(() => {
    setupPage(__CreatePage('0', 0));
  });

  afterEach(() => {
    vi.clearAllMocks();

    globalEnvManager.resetEnv();
    elementTree.clear();
    snapshotInstanceManager.clear();
  });

  it('should render hello world', () => {
    expect(renderToString(function() {
    })).toMatchInlineSnapshot(`[]`);

    expect(
      renderToString(
        <view>
          <text className={`a`.toLowerCase()}>Hello World</text>
          {'hello world'}
        </view>,
      ),
    ).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -4,
              "type": null,
              "values": [
                "hello world",
              ],
            },
          ],
          "extraProps": undefined,
          "id": -3,
          "type": "__snapshot_a94a8_test_1",
          "values": [
            "a",
          ],
        },
        0,
        2,
        "values",
        [
          "a",
        ],
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -4,
            "type": null,
            "values": [
              "hello world",
            ],
          },
          "hello world",
        ],
        0,
        1,
      ]
    `);
  });

  it('should clear transient named child props when the child is removed', () => {
    const child = createElement('text', null, 'Hello');
    const vnode = createElement('view', { $0: child });

    renderToString(vnode, new SnapshotInstance('root'));

    expect(vnode.props.$0).toBe(child);
    vnode.removeChild(child);

    expect(vnode.props.$0).toBeUndefined();
    expect('$0' in vnode.props).toBe(false);
    expect(vnode.childNodes).toEqual([]);
  });

  it('should clear removed children from nested transient prop arrays', () => {
    const child = createElement('text', null, 'Hello');
    const vnode = createElement('view', { $0: [[child]] });

    renderToString(vnode, new SnapshotInstance('root'));

    expect(vnode.props.$0[0][0]).toBe(child);
    vnode.removeChild(child);

    expect(vnode.props.$0).toEqual([[undefined]]);
    expect(vnode.childNodes).toEqual([]);
  });

  it('should clear all named child props', () => {
    const child = createElement('text', null, 'Hello');
    const vnode = createElement('view', { $0: child, $foo: child });

    renderToString(vnode, new SnapshotInstance('root'));
    vnode.removeChild(child);

    expect(vnode.props.$0).toBeUndefined();
    expect(vnode.props.$foo).toBeUndefined();
  });

  it('should tolerate cycles in transient prop arrays when clearing removed children', () => {
    const child = createElement('text', null, 'Hello');
    const vnode = createElement('view', { $0: child });

    renderToString(vnode, new SnapshotInstance('root'));
    const children = [child];
    children.push(children);
    vnode.props.$0 = children;
    vnode.removeChild(child);

    expect(vnode.props.$0[0]).toBeUndefined();
    expect(vnode.props.$0[1]).toBe(children);
    expect(vnode.childNodes).toEqual([]);
  });

  it('should render Component depth 1', () => {
    function App() {
      const [a, setA] = useState(111);
      useMemo(() => {
        setA(1000);
      }, []);
      return <view>{a}</view>;
    }

    expect(
      renderToString(
        <view>
          <text>Hello World</text>
          <App />
        </view>,
      ),
    ).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": [
                {
                  "children": undefined,
                  "extraProps": undefined,
                  "id": -5,
                  "type": null,
                  "values": [
                    1000,
                  ],
                },
              ],
              "extraProps": undefined,
              "id": -4,
              "type": "__snapshot_a94a8_test_2",
              "values": undefined,
            },
          ],
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_3",
          "values": undefined,
        },
        0,
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -5,
              "type": null,
              "values": [
                1000,
              ],
            },
          ],
          "extraProps": undefined,
          "id": -4,
          "type": "__snapshot_a94a8_test_2",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -5,
            "type": null,
            "values": [
              1000,
            ],
          },
          1000,
        ],
        0,
        1,
        1,
      ]
    `);
  });

  it('should render Class Component depth 1', () => {
    class App extends Component {
      static getDerivedStateFromProps(props, state) {
        return { a: 1 };
      }

      render() {
        return <view>{this.state.a}</view>;
      }
    }

    expect(
      renderToString(
        <view>
          <text>Hello World</text>
          <App />
        </view>,
      ),
    ).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": [
                {
                  "children": undefined,
                  "extraProps": undefined,
                  "id": -4,
                  "type": null,
                  "values": [
                    1,
                  ],
                },
              ],
              "extraProps": undefined,
              "id": -3,
              "type": "__snapshot_a94a8_test_4",
              "values": undefined,
            },
          ],
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_5",
          "values": undefined,
        },
        0,
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -4,
              "type": null,
              "values": [
                1,
              ],
            },
          ],
          "extraProps": undefined,
          "id": -3,
          "type": "__snapshot_a94a8_test_4",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -4,
            "type": null,
            "values": [
              1,
            ],
          },
          1,
        ],
        0,
        1,
        1,
      ]
    `);
  });

  it('should render with attr', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.5);
    const random = Math.random();

    function App() {
      return (
        <view random={random}>
          <text>Hello World</text>
          <raw-text text={'Hello World'.toLowerCase()} />
        </view>
      );
    }

    expect(renderToString(<App />)).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": undefined,
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_6",
          "values": [
            0.5,
            "hello world",
          ],
        },
        0,
        2,
        "values",
        [
          0.5,
          "hello world",
        ],
        1,
      ]
    `);
  });

  it('should render with empty value', () => {
    function App() {
      return (
        <view>
          <text>Hello World</text>
          {[false, '111', '']}
        </view>
      );
    }

    expect(renderToString(<App />)).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -3,
              "type": null,
              "values": [
                "111",
              ],
            },
          ],
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_7",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -3,
            "type": null,
            "values": [
              "111",
            ],
          },
          "111",
        ],
        0,
        1,
      ]
    `);
  });

  it('should render with top-level Fragment', () => {
    function App() {
      return (
        <Fragment>
          <view>
            <text>Hello World</text>
            {[false, '111', '']}
          </view>
        </Fragment>
      );
    }

    expect(renderToString(<App />)).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -3,
              "type": null,
              "values": [
                "111",
              ],
            },
          ],
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_8",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -3,
            "type": null,
            "values": [
              "111",
            ],
          },
          "111",
        ],
        0,
        1,
      ]
    `);
  });

  it('should render with Fragment', () => {
    expect(
      renderToString(
        <Fragment>
          <view>
            <text>Hello World</text>
            {[false, '111', '']}
          </view>
        </Fragment>,
      ),
    ).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -3,
              "type": null,
              "values": [
                "111",
              ],
            },
          ],
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_9",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -3,
            "type": null,
            "values": [
              "111",
            ],
          },
          "111",
        ],
        0,
        1,
      ]
    `);
  });

  it('should render with context', () => {
    const { Provider, Consumer } = createContext(11111);
    expect(
      renderToString(
        <view>
          <text>Hello World</text>
          <Consumer>{v => <text>{v}</text>}</Consumer>
        </view>,
      ),
    ).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": [
                {
                  "children": undefined,
                  "extraProps": undefined,
                  "id": -4,
                  "type": null,
                  "values": [
                    11111,
                  ],
                },
              ],
              "extraProps": undefined,
              "id": -3,
              "type": "__snapshot_a94a8_test_11",
              "values": undefined,
            },
          ],
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_10",
          "values": undefined,
        },
        0,
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -4,
              "type": null,
              "values": [
                11111,
              ],
            },
          ],
          "extraProps": undefined,
          "id": -3,
          "type": "__snapshot_a94a8_test_11",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -4,
            "type": null,
            "values": [
              11111,
            ],
          },
          11111,
        ],
        0,
        1,
        1,
      ]
    `);

    expect(
      renderToString(
        <Provider value={12345}>
          <view>
            <text>Hello World</text>
            <Consumer>{v => <text>{v}</text>}</Consumer>
          </view>
        </Provider>,
      ),
    ).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": [
                {
                  "children": undefined,
                  "extraProps": undefined,
                  "id": -7,
                  "type": null,
                  "values": [
                    12345,
                  ],
                },
              ],
              "extraProps": undefined,
              "id": -6,
              "type": "__snapshot_a94a8_test_13",
              "values": undefined,
            },
          ],
          "extraProps": undefined,
          "id": -5,
          "type": "__snapshot_a94a8_test_12",
          "values": undefined,
        },
        0,
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -7,
              "type": null,
              "values": [
                12345,
              ],
            },
          ],
          "extraProps": undefined,
          "id": -6,
          "type": "__snapshot_a94a8_test_13",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -7,
            "type": null,
            "values": [
              12345,
            ],
          },
          12345,
        ],
        0,
        1,
        1,
      ]
    `);
  });

  it('should throw when error occur', () => {
    function App() {
      undefined();
    }

    expect(
      () =>
        renderToString(
          <view>
            <text>Hello World</text>
            <App />
          </view>,
        ),
    ).toThrowErrorMatchingInlineSnapshot(`[TypeError: (void 0) is not a function]`);

    // renderToString will throw on Error without calling `options[DIFFED]`
    vi.mocked(console.profile).mockClear();
    vi.mocked(console.profileEnd).mockClear();
  });

  it('should throw when error occur - with ErrorBoundary ignored', () => {
    const f = vi.fn();

    class ErrorBoundary extends Component {
      componentDidCatch = f;

      render() {
        return this.props.children;
      }
    }

    function App() {
      undefined();
    }

    expect(
      () =>
        renderToString(
          <view>
            <text>Hello World</text>
            <ErrorBoundary>
              <App />
            </ErrorBoundary>
          </view>,
        ),
    ).toThrowErrorMatchingInlineSnapshot(`[TypeError: (void 0) is not a function]`);
    expect(f).toBeCalledTimes(0);

    class ErrorBoundary2 extends Component {
      static getDerivedStateFromError = f;

      render() {
        return this.props.children;
      }
    }

    expect(
      () =>
        renderToString(
          <view>
            <text>Hello World</text>
            <ErrorBoundary2>
              <App />
            </ErrorBoundary2>
          </view>,
        ),
    ).toThrowErrorMatchingInlineSnapshot(`[TypeError: (void 0) is not a function]`);
    expect(f).toBeCalledTimes(0);

    // renderToString will throw on Error without calling `options[DIFFED]`
    vi.mocked(console.profile).mockClear();
    vi.mocked(console.profileEnd).mockClear();
  });

  it('should render fallback when a direct child suspends', async () => {
    const { Suspender, suspended } = createSuspender();

    const fallbackJsx = <text>loading...</text>;

    const rendered = renderToString(
      <Suspense fallback={fallbackJsx}>
        <Suspender>
          <text className='foo'>bar</text>
        </Suspender>
      </Suspense>,
    );

    expect(rendered.length).toBe(4);
    expect(rendered[0]).toStrictEqual(0);
    expect(rendered[1].type).toStrictEqual(fallbackJsx.type);
    // slotIndex
    expect(rendered[2]).toStrictEqual(0);
    expect(rendered[3]).toStrictEqual(1);
  });

  it('should render fallback when suspended component is not a direct child', async () => {
    const { Suspender, suspended } = createSuspender();

    const fallbackJsx = <text>loading...</text>;

    const rendered = renderToString(
      <Suspense fallback={fallbackJsx}>
        <view attr={Math.random()}>
          <Suspender>
            <text className='foo'>bar</text>
          </Suspender>
        </view>
      </Suspense>,
    );

    expect(rendered.length).toBe(4);
    expect(rendered[0]).toStrictEqual(0);
    expect(rendered[1].type).toStrictEqual(fallbackJsx.type);
    // slotIndex
    expect(rendered[2]).toStrictEqual(0);
    expect(rendered[3]).toStrictEqual(1);
  });

  it('should render a fallback that is a Fragment', async () => {
    const { Suspender, suspended } = createSuspender();

    const fallbackJsx1 = <text>{`loading1...`}</text>;
    const fallbackJsx2 = <text>{`loading2...`}</text>;

    const fallbackJsx = (
      <>
        {fallbackJsx1}
        {fallbackJsx2}
      </>
    );

    const rendered = renderToString(
      <Suspense fallback={fallbackJsx}>
        <view attr={Math.random()}>
          <Suspender>
            <text className='foo'>bar</text>
          </Suspender>
        </view>
      </Suspense>,
    );

    expect(rendered[1].type).toStrictEqual(fallbackJsx1.type);
    expect(rendered[8].type).toStrictEqual(fallbackJsx2.type);
  });

  it('should render outer fallback when nested child suspends', async () => {
    const { Suspender: Suspender1, suspended: suspended1 } = createSuspender();
    const { Suspender: Suspender2, suspended: suspended2 } = createSuspender();

    const fallbackJsx1 = <text>loading1...</text>;
    const fallbackJsx2 = <text>loading2...</text>;

    const rendered = renderToString(
      <Suspense fallback={fallbackJsx1}>
        <view attr={Math.random()}>
          <Suspender1>
            <text className='foo'>bar</text>
            <Suspense fallback={fallbackJsx2}>
              <Suspender2>
                <text className='foo'>bar</text>
              </Suspender2>
            </Suspense>
          </Suspender1>
        </view>
      </Suspense>,
    );

    expect(rendered.length).toBe(4);
    expect(rendered[0]).toStrictEqual(0);
    expect(rendered[1].type).toStrictEqual(fallbackJsx1.type);
    // slotIndex
    expect(rendered[2]).toStrictEqual(0);
    expect(rendered[3]).toStrictEqual(1);
  });

  it('should render inner fallback and resolved content when outer suspense is resolved', async () => {
    const { Suspender: Suspender1, suspended: suspended1 } = createSuspender();
    const { Suspender: Suspender2, suspended: suspended2 } = createSuspender();
    suspended1.resolve();
    await waitSchedule();

    const fallbackJsx1 = <text>loading1...</text>;
    const fallbackJsx2 = <text>loading2...</text>;
    const resolvedJsx1 = <text className='foo'>bar</text>;
    const resolvedJsx2 = <text className='foo'>bar</text>;

    const rendered = renderToString(
      <Suspense fallback={fallbackJsx1}>
        <Suspender1>
          <>
            {resolvedJsx1}
            <Suspense fallback={fallbackJsx2}>
              <Suspender2>
                <text className='foo' attr={Math.random()}>bar</text>
              </Suspender2>
            </Suspense>
            {resolvedJsx2}
          </>
        </Suspender1>
      </Suspense>,
    );

    expect(rendered.length).toBe(12);
    expect(rendered[1].type).toStrictEqual(resolvedJsx1.type);
    expect(rendered[5].type).toStrictEqual(fallbackJsx2.type);
    expect(rendered[9].type).toStrictEqual(resolvedJsx2.type);
  });

  it('should render text with resolved suspense', async () => {
    const { Suspender, suspended } = createSuspender();
    suspended.resolve();
    await waitSchedule();

    const resolvedJsx = <text className='foo'>bar</text>;

    const rendered = renderToString(
      <Suspense fallback={<text>loading...</text>}>
        <Suspender>
          {resolvedJsx}
        </Suspender>
      </Suspense>,
    );

    expect(rendered.length).toBe(4);
    expect(rendered[0]).toStrictEqual(0);
    expect(rendered[1].type).toStrictEqual(resolvedJsx.type);
    // slotIndex
    expect(rendered[2]).toStrictEqual(0);
    expect(rendered[3]).toStrictEqual(1);
  });

  it('should render text with nested suspense', async () => {
    const { Suspender: Suspender1, suspended: suspended1 } = createSuspender();
    const { Suspender: Suspender2, suspended: suspended2 } = createSuspender();
    const { Suspender: Suspender3, suspended: suspended3 } = createSuspender();
    suspended1.resolve();
    suspended2.resolve();
    suspended3.resolve();
    await waitSchedule();

    const resolvedJsx1 = <text className='foo'>bar1</text>;
    const resolvedJsx2 = <text className='foo'>bar2</text>;
    const resolvedJsx3 = <text className='foo'>bar3</text>;
    const resolvedJsx4 = <text className='foo'>bar4</text>;
    const resolvedJsx5 = <text className='foo'>bar5</text>;

    const rendered = renderToString(
      <view>
        <Suspense fallback={<text>loading...</text>}>
          <Suspender1>
            {resolvedJsx1}
            <Suspense fallback={<text>loading...</text>}>
              <Suspender2>
                {resolvedJsx2}
                <Suspense fallback={<text>loading...</text>}>
                  <Suspender3>
                    {resolvedJsx3}
                  </Suspender3>
                </Suspense>
                {resolvedJsx4}
              </Suspender2>
            </Suspense>
            {resolvedJsx5}
          </Suspender1>
        </Suspense>
      </view>,
    );

    expect(rendered.length).toBe(24);
    expect(rendered[4].type).toStrictEqual(resolvedJsx1.type);
    expect(rendered[8].type).toStrictEqual(resolvedJsx2.type);
    expect(rendered[12].type).toStrictEqual(resolvedJsx3.type);
    expect(rendered[16].type).toStrictEqual(resolvedJsx4.type);
    expect(rendered[20].type).toStrictEqual(resolvedJsx5.type);
  });

  it('should only render text when it is not empty', () => {
    expect(renderToString(
      <view>
        <text></text>
        <text>{''}</text>
        <text>Static Text</text>
      </view>,
    )).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": undefined,
          "extraProps": undefined,
          "id": -2,
          "type": "__snapshot_a94a8_test_47",
          "values": undefined,
        },
        0,
        1,
      ]
    `);
    expect(renderToString(
      <view>
        <text></text>
        <text>{'Dynamic Text'}</text>
        <text>Static Text</text>
      </view>,
    )).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": [
            {
              "children": undefined,
              "extraProps": undefined,
              "id": -4,
              "type": null,
              "values": [
                "Dynamic Text",
              ],
            },
          ],
          "extraProps": undefined,
          "id": -3,
          "type": "__snapshot_a94a8_test_48",
          "values": undefined,
        },
        0,
        3,
        [
          {
            "children": undefined,
            "extraProps": undefined,
            "id": -4,
            "type": null,
            "values": [
              "Dynamic Text",
            ],
          },
          "Dynamic Text",
        ],
        0,
        1,
      ]
    `);
  });
});

describe('renderOpcodesInto', () => {
  /** @type {SnapshotInstance} */
  let scratch;

  beforeAll(() => {
    setupDocument();
  });

  beforeEach(() => {
    setupPage(__CreatePage('0', 0));
    scratch = document.createElement('root');
  });

  afterEach(() => {
    elementTree.clear();
    snapshotInstanceManager.clear();
  });

  it('should render hello world', () => {
    scratch.ensureElements();

    renderToString(
      <view>
        <text>Hello World</text>
      </view>,
      scratch,
    );

    // renderOpcodesInto(opcodes, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text>
            <raw-text
              text="Hello World"
            />
          </text>
        </view>
      </page>
    `);
  });

  it('should render attr', () => {
    scratch.ensureElements();

    renderToString(
      <view>
        <text>Hello World</text>
        <raw-text text={'Hello World'.toLowerCase()} />
      </view>,
      scratch,
    );

    // renderOpcodesInto(opcodes, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text>
            <raw-text
              text="Hello World"
            />
          </text>
          <raw-text
            text="hello world"
          />
        </view>
      </page>
    `);
  });

  it('should render string', () => {
    scratch.ensureElements();

    renderToString(
      <view>
        <text>Hello World</text>
        {'aaaa'.toUpperCase()}
      </view>,
      scratch,
    );

    // renderOpcodesInto(opcodes, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text>
            <raw-text
              text="Hello World"
            />
          </text>
          <wrapper>
            <raw-text
              text="AAAA"
            />
          </wrapper>
        </view>
      </page>
    `);
  });

  it('should render with multi-children', () => {
    scratch.ensureElements();

    renderToString(
      <view>
        <text>Hello World</text>
        {[<view>A</view>, <view>B</view>, <view>C</view>]}
      </view>,
      scratch,
    );

    // renderOpcodesInto(opcodes, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text>
            <raw-text
              text="Hello World"
            />
          </text>
          <wrapper>
            <view>
              <raw-text
                text="A"
              />
            </view>
            <view>
              <raw-text
                text="B"
              />
            </view>
            <view>
              <raw-text
                text="C"
              />
            </view>
          </wrapper>
        </view>
      </page>
    `);
  });

  it('should render when jsx is reused', () => {
    scratch.ensureElements();

    const reuse = (
      <view>
        {
          <view>
            <text>Hello</text>
          </view>
        }
        <text>World</text>
      </view>
    );

    renderToString(
      <view>
        <text>Hello World</text>
        {[reuse, reuse]}
      </view>,
      scratch,
    );

    // renderOpcodesInto(opcodes, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text>
            <raw-text
              text="Hello World"
            />
          </text>
          <wrapper>
            <view>
              <wrapper>
                <view>
                  <text>
                    <raw-text
                      text="Hello"
                    />
                  </text>
                </view>
              </wrapper>
              <text>
                <raw-text
                  text="World"
                />
              </text>
            </view>
            <view>
              <wrapper>
                <view>
                  <text>
                    <raw-text
                      text="Hello"
                    />
                  </text>
                </view>
              </wrapper>
              <text>
                <raw-text
                  text="World"
                />
              </text>
            </view>
          </wrapper>
        </view>
      </page>
    `);
  });

  it('should render component with ref', () => {
    scratch.ensureElements();

    const ref = vi.fn();

    function Counter({ ref: r, count: _ }) {
      expect(r).toBe(ref);
      return <view />;
    }

    renderToString(<Counter ref={ref} count={1} />, scratch);
    // renderOpcodesInto(opcodes, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view />
      </page>
    `);
  });

  it('should render component with defaultProps', () => {
    scratch.ensureElements();

    function Counter({ count }) {
      expect(count).toBe(1);
      return <view />;
    }

    Counter.defaultProps = { count: 1 };

    renderToString(<Counter />, scratch);
    // renderOpcodesInto(opcodes, scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view />
      </page>
    `);
  });

  it('should render empty array', () => {
    scratch.ensureElements();

    renderToString([], scratch);
    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      />
    `);
  });

  it('opcodes will not ref FiberElement', () => {
    scratch.ensureElements();

    function Fragment({ children }) {
      return children;
    }

    renderToString(
      <view>
        <text>Hello World</text>
        {[
          <view>A</view>,
          <view>B</view>,
          <view>C</view>,
          <view>
            C
            <Fragment>
              <text>C1</text>
              <text>C2</text>
              <text>C3</text>
              <text>C4</text>
            </Fragment>
          </view>,
          <view>D{[<text>D1</text>, <text>D2</text>, <text>D3</text>, <text>D4</text>]}</view>,
        ]}
      </view>,
      scratch,
    );

    // renderOpcodesInto(opcodes, scratch);

    scratch.__firstChild.removeChild(scratch.__firstChild.__firstChild);
    scratch.__firstChild.removeChild(scratch.__firstChild.__lastChild);
    scratch.__firstChild.removeChild(scratch.__firstChild.__lastChild);

    expect(scratch.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      >
        <view>
          <text>
            <raw-text
              text="Hello World"
            />
          </text>
          <wrapper>
            <view>
              <raw-text
                text="B"
              />
            </view>
            <view>
              <raw-text
                text="C"
              />
            </view>
          </wrapper>
        </view>
      </page>
    `);

    const [vnodeA, vnodeB, vnodeC, vnodeC2, vnodeD] = scratch.__firstChild.props.$0;

    expect(vnodeA).toBeUndefined();
    expect(vnodeB).toHaveProperty('__elements');
    expect(vnodeB).toHaveProperty('__element_root');
    expect(vnodeC).toHaveProperty('__elements');
    expect(vnodeC).toHaveProperty('__element_root');
    expect(vnodeC2).toBeUndefined();
    expect(vnodeD).toBeUndefined();
  });
});

it('should compile @jsxImportSource', () => {
  expect(<view />).toBeInstanceOf(SnapshotInstance);
});

describe('createElement', () => {
  const s = { __dummy: 1 };

  it('key should not be accessible to developer', () => {
    function Key({ key }) {
      expect(key).toBe(undefined);
      return <view />;
    }

    const opcodes1 = renderToString(<Key key={1} {...s} />);
    expect(opcodes1).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": undefined,
          "extraProps": undefined,
          "id": -41,
          "type": "__snapshot_a94a8_test_76",
          "values": undefined,
        },
        0,
        1,
      ]
    `);
    const opcodes2 = renderToString(<Key {...s} key={1} />);
    expect(opcodes2).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": undefined,
          "extraProps": undefined,
          "id": -42,
          "type": "__snapshot_a94a8_test_76",
          "values": undefined,
        },
        0,
        1,
      ]
    `);
  });

  it('ref should be accessible to developer', () => {
    const ref = vi.fn();

    function Key({ ref: r }) {
      expect(r).toBe(ref);
      return <view />;
    }

    const opcodes1 = renderToString(<Key key={1} {...s} ref={ref} />);
    expect(opcodes1).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": undefined,
          "extraProps": undefined,
          "id": -43,
          "type": "__snapshot_a94a8_test_77",
          "values": undefined,
        },
        0,
        1,
      ]
    `);
    const opcodes2 = renderToString(<Key {...s} key={1} ref={ref} />);
    expect(opcodes2).toMatchInlineSnapshot(`
      [
        0,
        {
          "children": undefined,
          "extraProps": undefined,
          "id": -44,
          "type": "__snapshot_a94a8_test_77",
          "values": undefined,
        },
        0,
        1,
      ]
    `);
  });

  it('children can be pass as extra arguments (length > 3)', () => {
    function Key({ key, children }) {
      expect(key).toBe(undefined);
      expect(children).toMatchInlineSnapshot(`
        [
          1,
          1,
        ]
      `);
      return <view />;
    }

    renderToString(createElement(Key, { key: 1 }, 1, 1));
  });

  it('children can be pass as extra arguments (length > 3) (JSX)', () => {
    function Key({ key, children }) {
      expect(key).toBe(undefined);
      expect(children).toMatchInlineSnapshot(`
        [
          1,
          1,
        ]
      `);
      return <view />;
    }

    renderToString(<Key {...s} key={1}>{1}{1}</Key>);
  });

  it('children can be pass as extra arguments (length > 2)', () => {
    function Key({ key, children }) {
      expect(children).toMatchInlineSnapshot(`1`);
      return <view />;
    }

    renderToString(<Key {...s} key={1}>{1}</Key>);
  });

  it('should render component with defaultProps', () => {
    function Counter({ count }) {
      expect(count).toBe(1);
      return <view />;
    }

    Counter.defaultProps = { count: 1 };

    renderToString(<Counter {...s} key={1} />);
  });
});

describe('cloneElement', () => {
  it('should clone component', () => {
    function Comp() {
    }

    const instance = <Comp prop1={1}>hello</Comp>;
    const clone = cloneElement(instance);

    // expect(clone.type).to.equal(instance.type);
    // expect(clone.props).not.to.equal(instance.props); // Should be a different object...
    // expect(clone.props).to.deep.equal(instance.props); // with the same properties

    expect(clone.type).toBe(instance.type);
    expect(clone.props).not.toBe(instance.props); // Should be a different object...
    expect(clone.props).toEqual(instance.props); // with the same properties
  });

  it('should merge new props', () => {
    function Foo() {
    }

    const instance = <Foo prop1={1} prop2={2} />;
    const clone = cloneElement(instance, { prop1: -1, newProp: -2 });

    expect(clone.type).toBe(instance.type);
    expect(clone.props).not.toBe(instance.props);
    expect(clone.props).not.toEqual(instance.props);
    expect(clone.props.prop1).toBe(-1);
    expect(clone.props.prop2).toBe(2);
    expect(clone.props.newProp).toBe(-2);
  });

  it('should override children if specified', () => {
    function Foo() {
    }

    const instance = <Foo>hello</Foo>;
    const clone = cloneElement(instance, null, 'world', '!');

    expect(clone.type).toBe(instance.type);
    expect(clone.props).not.toBe(instance.props);
    expect(clone.props.children).toEqual(['world', '!']);
  });

  it('should override children if null is provided as an argument', () => {
    function Foo() {
    }

    const instance = <Foo>hello</Foo>;
    const clone = cloneElement(instance, { children: 'bar' }, null);

    expect(clone.type).toBe(instance.type);
    expect(clone.props).not.toBe(instance.props);
    expect(clone.props.children).toBeNull();
  });

  it('should override key if specified', () => {
    function Foo() {
    }

    const instance = <Foo key='1'>hello</Foo>;

    let clone = cloneElement(instance);
    // key is omit in lepus vnode
    // expect(clone.key).toBe('1');

    clone = cloneElement(instance, { key: '2' });
    // key is omit in lepus vnode
    // expect(clone.key).toBe('2');
  });

  it('should override ref if specified', () => {
    function a() {
    }

    function b() {
    }

    function Foo() {
    }

    const instance = <Foo ref={a}>hello</Foo>;

    let clone = cloneElement(instance);
    // ref is omit in lepus vnode
    // expect(clone.ref).toBe(a);

    clone = cloneElement(instance, { ref: b });
    // ref is omit in lepus vnode
    // expect(clone.ref).toBe(b);
  });

  it('should prevent undefined properties from overriding default props', () => {
    class Example extends Component {
      render(props) {
        return <div style={{ color: props.color }}>thing</div>;
      }
    }

    Example.defaultProps = { color: 'blue' };

    const element = <Example color='red' />;
    const clone = cloneElement(element, { color: undefined });
    expect(clone.props.color).toBe('blue');
  });
});

describe('renderMainThread', () => {
  it('should not throw if error - instead it will render an empty page', () => {
    function App() {
      undefined();
    }

    __root.__jsx = (
      <view>
        <text>Hello World</text>
        <App />
      </view>
    );

    expect(() => renderPage()).not.toThrow();
    expect(__root.__element_root).toMatchInlineSnapshot(`
      <page
        cssId="default-entry-from-native:0"
      />
    `);
  });
});
