import {
  Fragment as Component,
  Fragment as Signal,
  component$,
  componentQrl,
  getDomContainer,
  getPlatform,
  inlinedQrl,
  render,
  setPlatform,
  useLexicalScope,
  useOn,
  useAsync$,
  useServerData,
  useSignal,
  useTask$,
  type JSXOutput,
} from '@qwik.dev/core';
import type { GlobalInjections, QwikManifest } from '@qwik.dev/core/optimizer';
import { renderToStream, renderToString } from '@qwik.dev/core/server';
import {
  createDocument,
  emulateExecutionOfQwikFuncs,
  getTestPlatform,
  trigger,
} from '@qwik.dev/core/testing';
import { cleanupAttrs } from 'packages/qwik/src/testing/element-fixture';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type {
  RenderToStreamOptions,
  RenderToStreamResult,
  RenderToStringOptions,
  RenderToStringResult,
  StreamingOptions,
  StreamWriter,
} from '../../server/types';
import { whenContainerDataReady } from '../client/dom-container';
import { vnode_getFirstChild } from '../client/vnode-utils';
import { _fnSignal, type _ContainerElement } from '../internal';
import { QContainerValue } from '../shared/types';
import { QContainerAttr, QwikEvContainerReady, QStatePrewarmAttr } from '../shared/utils/markers';

vi.hoisted(() => {
  vi.stubGlobal('QWIK_LOADER_DEFAULT_MINIFIED', 'min');
  vi.stubGlobal('QWIK_LOADER_DEFAULT_DEBUG', 'debug');
});

const mapping = {
  click: 'click.js',
  s_counter: 's_counter.js',
  s_click: 's_click.js',
  // Our internal symbols
  _run: 'core',
  _task: 'core',
};

const defaultManifest: QwikManifest = {
  manifestHash: 'manifest-hash',
  symbols: {},
  bundles: {},
  mapping,
  version: '1',
  preloader: 'preloader.js',
};
const manifestWithHelpers = {
  ...defaultManifest,
  core: 'core.js',
  preloader: 'preloader.js',
  qwikLoader: 'qwik-loader.js',
  bundleGraphAsset: 'assets/bundle-graph.json',
};

const ManyEventsComponent = component$(() => {
  useOn(
    'focus',
    inlinedQrl(() => {}, 's_useOnFocus')
  );
  useOn(
    'My-Custom',
    inlinedQrl(() => {}, 's_useOnMyCustom')
  );
  return (
    <div>
      <button
        onClick$={inlinedQrl(() => {}, 's_click1')}
        onDblClick$={inlinedQrl(() => {}, 's_dblclick1')}
      >
        click
      </button>
      <button
        onClick$={inlinedQrl(() => {}, 's_click2')}
        onBlur$={inlinedQrl(() => {}, 's_blur1')}
        on-anotherCustom$={inlinedQrl(() => {}, 's_anotherCustom1')}
        document:onFocus$={inlinedQrl(() => {}, 's_documentFocus1')}
        window:onClick$={inlinedQrl(() => {}, 's_windowClick1')}
      >
        click
      </button>
    </div>
  );
});

const Counter = componentQrl(
  inlinedQrl(() => {
    const count = useSignal(123);
    return (
      <button
        onClick$={inlinedQrl(
          () => {
            useLexicalScope()[0].value += 1;
          },
          's_click4',
          [count]
        )}
      >
        {count.value}
      </button>
    );
  }, 's_counter1')
);

const renderToStringAndSetPlatform = async (jsx: JSXOutput, opts: RenderToStringOptions = {}) => {
  const platform = getPlatform();
  let result: RenderToStringResult;
  try {
    result = await renderToString(jsx, { qwikLoader: 'never', ...opts });
  } finally {
    setPlatform(platform);
  }
  return result;
};

const renderToStreamAndSetPlatform = async (jsx: JSXOutput, opts: RenderToStreamOptions) => {
  const platform = getPlatform();
  let result: RenderToStreamResult;
  try {
    result = await renderToStream(jsx, { qwikLoader: 'never', ...opts });
  } finally {
    setPlatform(platform);
  }
  return result;
};

const createDeferred = () => {
  let resolve!: () => void;
  const promise = new Promise<void>((res) => {
    resolve = res;
  });
  return { promise, resolve };
};

const createTestStream = (write: StreamWriter['write']): StreamWriter => ({ write });

describe('render api', () => {
  let document: Document;
  beforeEach(() => {
    document = createDocument();
  });

  describe('render()', () => {
    it('should render counter', async () => {
      await render(document.body, <Counter />);
      await getTestPlatform().flush();
      const container = getDomContainer(document.body);
      const vNode = vnode_getFirstChild(container.rootVNode);
      expect(vNode).toMatchVDOM(
        <Component>
          <button>
            <Signal>123</Signal>
          </button>
        </Component>
      );
      await trigger(container.element, 'button', 'click');
      expect(vNode).toMatchVDOM(
        <Component>
          <button>
            <Signal>124</Signal>
          </button>
        </Component>
      );
    });
    it('should pass serverData', async () => {
      const TestCmp = component$(() => {
        const myKey = useServerData<string>('my-key');
        const defaultKey = useServerData<string>('other-key', 'default-value');
        return (
          <span>
            {myKey}/{defaultKey}
          </span>
        );
      });
      await render(document.body, <TestCmp />, { serverData: { 'my-key': 'my-value' } });
      await getTestPlatform().flush();
      const container = getDomContainer(document.body);
      const vNode = vnode_getFirstChild(container.rootVNode);
      expect(vNode).toMatchVDOM(
        <Component>
          <span>
            {'my-value'}/{'default-value'}
          </span>
        </Component>
      );
    });
    it('should cleanup', async () => {
      const log: string[] = [];
      const TestCmp = component$(() => {
        useTask$(() => {
          log.push('task');
          return () => {
            log.push('cleanup');
          };
        });
        return <span />;
      });
      const { cleanup } = await render(document.body, <TestCmp />, {
        serverData: { 'my-key': 'my-value' },
      });
      await getTestPlatform().flush();
      expect(log).toEqual(['task']);
      cleanup();
      expect(log).toEqual(['task', 'cleanup']);
    });
  });
  describe('renderToString()', () => {
    it('should renderToString', async () => {
      const TestCmp = component$(() => {
        const count = useSignal(123);
        return (
          <button
            onClick$={inlinedQrl(
              () => {
                useLexicalScope()[0].value += 2;
              },
              's_click5',
              [count]
            )}
          >
            {count.value}
          </button>
        );
      });
      const result = await renderToStringAndSetPlatform(<TestCmp />, {
        containerTagName: 'div',
      });
      document = createDocument({ html: result.html });
      emulateExecutionOfQwikFuncs(document);
      const container = getDomContainer(document.body.firstChild as HTMLElement);
      await whenContainerDataReady(container, () => undefined);
      const vNode = vnode_getFirstChild(container.rootVNode);
      expect(vNode).toMatchVDOM(
        <button>
          <Signal ssr-required>123</Signal>
        </button>
      );
      await trigger(container.element, 'button', 'click');
      expect(vNode).toMatchVDOM(
        <button>
          <Signal ssr-required>125</Signal>
        </button>
      );
    });
    describe('render result', () => {
      it('should render', async () => {
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          manifest: defaultManifest,
        });
        expect(result).toMatchObject({
          isStatic: false,
          timing: expect.any(Object),
          manifest: expect.any(Object),
          html: expect.any(String),
        });
      });

      it('should have timings greater than 0', async () => {
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
        });
        const timing = result.timing;
        expect(timing).toMatchObject({
          firstFlush: expect.any(Number),
          render: expect.any(Number),
          snapshot: expect.any(Number),
        });
        expect(timing.firstFlush).toBeGreaterThan(0);
        expect(timing.render).toBeGreaterThan(0);
        expect(timing.snapshot).toBeGreaterThan(0);
      });

      it('should escape invalid characters', async () => {
        const Cmp = componentQrl(
          inlinedQrl(() => {
            const obj = {
              a: '123',
              b: '<script />',
              c: '&foo',
            };
            return (
              <div data-amp="foo&bar" data-lt="foo<bar" data-gt="foo>bar" data-a='"' data-b="'">
                {JSON.stringify(obj)}
              </div>
            );
          }, 's_counter2')
        );
        const result = await renderToStringAndSetPlatform(<Cmp />, {
          containerTagName: 'div',
          manifest: defaultManifest,
        });
        expect(cleanupAttrs(result.html)).toContain(
          `<div data-amp="foo&amp;bar" data-lt="foo&lt;bar" data-gt="foo&gt;bar" data-a="&quot;" data-b="&#39;">{&quot;a&quot;:&quot;123&quot;,&quot;b&quot;:&quot;&lt;script /&gt;&quot;,&quot;c&quot;:&quot;&amp;foo&quot;}</div>`
        );
      });
    });
    describe('version', () => {
      afterEach(async () => {
        // restore default value
        const version = await import('../version');
        vi.spyOn(version, 'version', 'get').mockReturnValue('dev');
      });

      it('should render', async () => {
        const testVersion = 'qwik-v-test123';
        const version = await import('../version');
        vi.spyOn(version, 'version', 'get').mockReturnValue(testVersion);
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
        });
        expect(result.html).toContain(`q:version="${testVersion}"`);
        vi.clearAllMocks();
      });
    });
    describe('container', () => {
      it('should render', async () => {
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
        });
        const document = createDocument({ html: result.html });
        const containerElement = document.querySelector('[q\\:container]') as _ContainerElement;
        emulateExecutionOfQwikFuncs(document);

        expect(containerElement.getAttribute(QContainerAttr)).toEqual(QContainerValue.PAUSED);
        await trigger(containerElement, 'button', 'click');
        expect(containerElement.getAttribute(QContainerAttr)).toEqual(QContainerValue.RESUMED);
      });
    });
    describe('base', () => {
      it('should render', async () => {
        const testBase = '/abcd/123-test/';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          base: testBase,
        });
        expect(result.html).toContain(`q:base="${testBase}"`);
      });
    });
    describe('locale', () => {
      it('should render', async () => {
        const testLocale = 'pl';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          locale: testLocale,
        });
        expect(result.html).toContain(`q:locale="${testLocale}"`);
      });
      it('should render for function', async () => {
        const testLocale = 'pl';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          locale: () => testLocale,
        });
        expect(result.html).toContain(`q:locale="${testLocale}"`);
      });
      it('should render from serverData', async () => {
        const testLocale = 'pl';
        const testServerDataLocale = 'en';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          locale: testLocale,
          serverData: {
            locale: testServerDataLocale,
          },
        });
        expect(result.html).toContain(`q:locale="${testServerDataLocale}"`);
      });
      it('should render from containerAttributes', async () => {
        const testLocale = 'en-us';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          containerAttributes: {
            locale: testLocale,
          },
        });
        expect(result.html).toContain(`q:locale="${testLocale}"`);
      });
    });
    describe('qwikLoader', () => {
      describe('inline', () => {
        it('should render at bottom as fallback', async () => {
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
            qwikLoader: 'inline',
          });
          const document = createDocument({ html: result.html });
          // qwik loader is one before last
          const qwikLoaderScriptElement = document.body.firstChild?.lastChild
            ?.previousSibling as HTMLElement;
          expect(qwikLoaderScriptElement?.tagName.toLowerCase()).toEqual('script');
          expect(qwikLoaderScriptElement?.getAttribute('id')).toEqual('qwikloader');
          // qwik events should be the last script of body
          const eventsScriptElement = document.body.lastChild as HTMLElement;
          expect(eventsScriptElement.textContent).toContain(
            '(window._qwikEv||(window._qwikEv=[]))'
          );
        });
        it('should not render for static content and auto include', async () => {
          const result = await renderToStringAndSetPlatform(<div>static</div>, {
            containerTagName: 'div',
            qwikLoader: 'inline',
          });
          const document = createDocument({ html: result.html });
          // should not contain qwik events script for top position
          expect(document.head.lastChild?.textContent ?? '').not.toContain('window._qwikEv.push');
          expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(0);
        });
        it('should render after 30kB of SSR', async () => {
          const bigText = 'hello world '.repeat(3000); // ~30kB of text
          const result = await renderToStringAndSetPlatform(
            <div>
              <Counter />
              <div>{bigText}</div>
              <div>{bigText}</div>
            </div>,
            {
              containerTagName: 'div',
              qwikLoader: 'inline',
            }
          );
          const document = createDocument({ html: result.html });
          expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(1);
          const notQwikLoaderScriptElement = document.body.firstChild?.lastChild
            ?.previousSibling as HTMLElement;
          expect(notQwikLoaderScriptElement?.id).not.toEqual('qwikloader');
          // qwik events should still be the last script of body
          const eventsScriptElement = document.body.lastChild as HTMLElement;
          expect(eventsScriptElement.textContent).toContain(
            '(window._qwikEv||(window._qwikEv=[]))'
          );
        });
        it('should only render inside body', async () => {
          const bigText = 'hello world '.repeat(4000);
          const result = await renderToStringAndSetPlatform(
            <>
              <head>
                <script>{bigText}</script>
              </head>
              <body>
                <Counter />
                <div>{bigText}</div>
              </body>
            </>,
            {
              containerTagName: 'html',
              qwikLoader: 'inline',
            }
          );
          const document = createDocument({ html: result.html });
          const qwikLoaderScriptElements = document.querySelectorAll('script[id=qwikloader]');
          expect(qwikLoaderScriptElements).toHaveLength(1);
          expect(qwikLoaderScriptElements[0]?.parentNode?.nodeName.toLowerCase()).toEqual('body');
        });
        it('should not render inside template', async () => {
          const bigText = 'hello world '.repeat(3000); // ~30kB of text
          const result = await renderToStringAndSetPlatform(
            <div>
              <Counter />
              <table>
                <tbody>
                  <tr>
                    <td>
                      <template>
                        <div>{bigText}</div>
                        {bigText}
                      </template>
                    </td>
                  </tr>
                  <tr>
                    <td>Before here is safe</td>
                  </tr>
                </tbody>
              </table>
            </div>,
            {
              containerTagName: 'div',
              qwikLoader: 'inline',
            }
          );
          const document = createDocument({ html: result.html });
          expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(1);
          const notQwikLoaderScriptElement = document.body.firstChild?.lastChild
            ?.previousSibling as HTMLElement;
          expect(notQwikLoaderScriptElement?.id).not.toEqual('qwikloader');
          // qwik events should still be the last script of body
          const eventsScriptElement = document.body.lastChild as HTMLElement;
          expect(eventsScriptElement.textContent).toContain(
            '(window._qwikEv||(window._qwikEv=[]))'
          );
        });
      });
      it('should support never render', async () => {
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          qwikLoader: 'never',
        });
        const document = createDocument({ html: result.html });
        expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(0);
      });
    });
    describe('_qwikEv', () => {
      it('should render', async () => {
        const result = await renderToStringAndSetPlatform(<ManyEventsComponent />, {
          containerTagName: 'div',
          qwikLoader: 'module',
        });
        expect(result.html).toMatch(
          new RegExp(
            `\\(window\\._qwikEv\\|\\|\\(window\\._qwikEv=\\[\\]\\)\\)\\.push\\("e:focus","e:-my---custom","e:click","e:dblclick","d:focus","e:another-custom","e:blur","w:click",${QwikEvContainerReady},"[^"]+"\\)`
          )
        );
      });
    });
    describe('qwikFuncs', () => {
      it('should render', async () => {
        const CounterDerived = component$((props: { initial: number }) => {
          const count = useSignal(props.initial);
          return (
            <button
              onClick$={inlinedQrl(() => (useLexicalScope()[0].value += 3), 's_onClick', [count])}
            >
              Count: {_fnSignal((p0) => p0.value, [count], 'p0.value')}!
            </button>
          );
        });
        const result = await renderToStringAndSetPlatform(<CounterDerived initial={123} />, {
          containerTagName: 'div',
        });
        const document = createDocument({ html: result.html });
        const qwikFuncScriptElements = document.querySelectorAll('script[q\\:func=qwik/json]');
        expect(qwikFuncScriptElements).toHaveLength(1);
        expect(qwikFuncScriptElements[0].textContent).toMatch(
          /document\["qFuncs_(\w+)"\]=\[\(p0\)=>p0\.value\]/
        );
      });
    });
    describe('preloader', () => {
      // we need a test with a built manifest
      it('should not render in dev mode', async () => {
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          manifest: defaultManifest,
        });
        const document = createDocument({ html: result.html });
        const preloadScript = document.querySelectorAll('script[q\\:type=preload]');
        expect(preloadScript).toHaveLength(0);
        expect(document.querySelectorAll('link')).toHaveLength(0);
      });
    });
    describe('containerTagName/containerAttributes', () => {
      it('should render correct container tag name', async () => {
        const testTag = 'test-tag';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: testTag,
        });
        const document = createDocument({ html: result.html });
        expect(document.body.firstChild?.nodeName.toLowerCase()).toEqual(testTag);
      });
      it('should render qwik loader but no preloader in dev mode for custom tag name', async () => {
        const testTag = 'test-tag';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: testTag,
          manifest: manifestWithHelpers,
          qwikLoader: 'module',
        });
        const document = createDocument({ html: result.html });
        const containerElement = document.body.firstChild;
        expect(containerElement?.nodeName.toLowerCase()).toEqual(testTag);
        expect(containerElement?.lastChild?.textContent ?? '').toContain('window._qwikEv');
        const scripts = document.querySelectorAll('script');
        expect(scripts[0]?.getAttribute('src')).toEqual('/build/qwik-loader.js');
        for (let i = 0; i < scripts.length; i++) {
          expect(scripts[i]?.innerHTML).not.toContain('/build/preloader.js');
        }
        const links = document.querySelectorAll('link');
        expect(links[0]?.getAttribute('href')).toEqual('/build/qwik-loader.js');
        expect(links[0]?.getAttribute('rel')).toEqual('modulepreload');
        expect(links).toHaveLength(1);
      });
      it('should render custom container attributes', async () => {
        const testAttrName = 'test-attr';
        const testAttrValue = 'test-value';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          containerAttributes: {
            [testAttrName]: testAttrValue,
          },
        });
        expect(result.html.includes(`${testAttrName}="${testAttrValue}"`)).toBeTruthy();
      });
      describe('statePrewarm', () => {
        it('should omit state prewarm attribute by default', async () => {
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
          });

          expect(result.html.includes(QStatePrewarmAttr)).toBe(false);
        });

        it('should omit state prewarm attribute when disabled explicitly', async () => {
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
            statePrewarm: false,
          });

          expect(result.html.includes(QStatePrewarmAttr)).toBe(false);
        });

        it('should render numeric state prewarm threshold', async () => {
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
            statePrewarm: 512,
          });

          expect(result.html.includes(`${QStatePrewarmAttr}="512"`)).toBe(true);
        });

        it('should prefer disabled state prewarm option over custom container attribute', async () => {
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
            containerAttributes: {
              [QStatePrewarmAttr]: '512',
            },
            statePrewarm: false,
          });

          expect(result.html.includes(QStatePrewarmAttr)).toBe(false);
        });
      });
      describe('qRender', () => {
        afterEach(async () => {
          // restore default value
          const qDev = await import('../shared/utils/qdev');
          vi.spyOn(qDev, 'qDev', 'get').mockReturnValue(true);
        });
        it('should render qRender with "ssr-dev" value in dev mode', async () => {
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
          });
          expect(result.html.includes('q:render="ssr-dev"')).toBeTruthy();
        });
        it('should render qRender with "ssr" value in prod mode', async () => {
          const qDev = await import('../shared/utils/qdev');
          vi.spyOn(qDev, 'qDev', 'get').mockReturnValue(false);

          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
          });
          expect(result.html.includes('q:render="ssr-dev"')).toBeTruthy();
        });
        it('should render qRender with custom value in dev mode', async () => {
          const testRender = 'ssr-test';
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
            containerAttributes: {
              'q:render': testRender,
            },
          });
          expect(result.html.includes(`q:render="${testRender}-ssr-dev"`)).toBeTruthy();
        });
        it('should render qRender with custom value in prod mode', async () => {
          const qDev = await import('../shared/utils/qdev');
          vi.spyOn(qDev, 'qDev', 'get').mockReturnValue(false);

          const testRender = 'ssr-test';
          const result = await renderToStringAndSetPlatform(<Counter />, {
            containerTagName: 'div',
            containerAttributes: {
              'q:render': testRender,
            },
          });
          expect(result.html.includes(`q:render="${testRender}-ssr-dev"`)).toBeTruthy();
        });
      });
    });
    describe('serverData', () => {
      it('should render', async () => {
        const TestCmp = componentQrl(
          inlinedQrl(() => {
            const myKey = useServerData<string>('my-key');
            const defaultKey = useServerData<string>('other-key', 'default-value');
            return (
              <span>
                {myKey}/{defaultKey}
              </span>
            );
          }, 's_cmp1')
        );

        const result = await renderToStringAndSetPlatform(<TestCmp />, {
          containerTagName: 'div',
          serverData: { 'my-key': 'my-value' },
        });
        const document = createDocument({ html: result.html });
        const componentElement = document.body.firstChild;
        expect(componentElement?.firstChild?.textContent).toEqual('my-value/default-value');
      });
    });
    describe('manifest/symbolMapper', () => {
      it('should render', async () => {
        const testManifest: QwikManifest = {
          manifestHash: 'test-manifest-hash',
          symbols: {
            symbol1: {
              canonicalFilename: 'symbol1filename',
              captures: false,
              ctxKind: 'eventHandler',
              ctxName: 'symbol1ctxname',
              displayName: 'symbol1displayname',
              hash: 'symbol1hash',
              loc: [0, 0],
              origin: 'symbol1origin',
              parent: null,
            },
          },
          bundles: {
            bundle1: {
              size: 1,
              total: 1,
              dynamicImports: [],
            },
          },
          mapping,
          version: '1',
        };
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          manifest: testManifest,
        });
        expect(result.manifest).toEqual(testManifest);
      });
      it('should render manifest hash attribute', async () => {
        const testManifestHash = 'testManifestHash';
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          manifest: {
            ...defaultManifest,
            manifestHash: testManifestHash,
          },
        });
        expect(result.html.includes(`q:manifest-hash="${testManifestHash}"`)).toBeTruthy();
      });
      it('should render manifest injections', async () => {
        const cssContent = 'body { color: red; }';
        const cssPath = '/path/to/style.css';
        const scriptPath = '/path/to/script.js';
        const result = await renderToStringAndSetPlatform(
          <>
            <head></head>
            <body>test</body>
          </>,
          {
            containerTagName: 'html',
            manifest: {
              ...defaultManifest,
              injections: [
                {
                  location: 'head',
                  tag: 'style',
                  attributes: {
                    'data-src': cssPath,
                    dangerouslySetInnerHTML: cssContent,
                  },
                },
                {
                  location: 'body',
                  tag: 'script',
                  attributes: {
                    id: 'script123',
                    src: scriptPath,
                  },
                },
              ] as GlobalInjections[],
            },
          }
        );
        const document = createDocument({ html: result.html });
        const style = document.head.querySelector('style');
        expect(style?.getAttribute('data-src')).toEqual(cssPath);
        expect(style?.innerHTML).toEqual(cssContent);
        expect(style?.parentNode?.nodeName.toLowerCase()).toEqual('head');

        const script = document.body.querySelector('#script123');
        expect(script?.getAttribute('src')).toEqual(scriptPath);
        expect(script?.parentNode?.nodeName.toLowerCase()).toEqual('body');
      });
    });
    describe('debug', () => {
      it('should emit qwik loader with debug mode', async () => {
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          debug: true,
          qwikLoader: 'inline',
        });
        expect(cleanupAttrs(result.html)).toContain(
          '<script id="qwikloader" async type="module">debug</script>'
        );
      });

      it('should emit qwik loader without debug mode', async () => {
        const result = await renderToStringAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          debug: false,
          qwikLoader: 'inline',
        });
        expect(cleanupAttrs(result.html)).toContain(
          '<script id="qwikloader" async type="module">min</script>'
        );
      });
    });
  });

  describe('renderToStream()', () => {
    describe('render result', () => {
      it('should renderToStream', async () => {
        const chunks: string[] = [];
        const write = (chunk: string) => {
          chunks.push(chunk);
        };
        const stream = createTestStream(write);
        await renderToStreamAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          stream,
        });
        document = createDocument({ html: chunks.join('') });
        emulateExecutionOfQwikFuncs(document);
        const container = getDomContainer(document.body.firstChild as HTMLElement);
        await whenContainerDataReady(container, () => undefined);
        const vNode = vnode_getFirstChild(container.rootVNode);
        expect(vNode).toMatchVDOM(
          <button>
            <Signal ssr-required>123</Signal>
          </button>
        );
        await trigger(container.element, 'button', 'click');
        expect(vNode).toMatchVDOM(
          <button>
            <Signal ssr-required>124</Signal>
          </button>
        );
      });
    });
    describe('stream', () => {
      it('should render', async () => {
        const stream = createTestStream(vi.fn());
        await renderToStreamAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          stream,
        });
        expect(stream.write).toHaveBeenCalled();
      });
    });
    describe('streaming', () => {
      it('should render all at once', async () => {
        const write = vi.fn();
        const stream = createTestStream(write);
        const streaming: StreamingOptions = {
          inOrder: {
            strategy: 'disabled',
          },
        };
        await renderToStreamAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          stream,
          streaming,
        });
        expect(stream.write).toHaveBeenCalledTimes(1);
      });
      it('should render by direct streaming', async () => {
        const write = vi.fn();
        const stream = createTestStream(write);
        const streaming: StreamingOptions = {
          inOrder: {
            strategy: 'direct',
          },
        };
        await renderToStreamAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          stream,
          streaming,
        });
        expect(write.mock.calls.length).toBeGreaterThan(100);
      });
      it('should handle jsx promise rejection while a flush is pending', async () => {
        const firstWrite = createDeferred();
        let writeCount = 0;
        const unhandledRejections: unknown[] = [];
        const onUnhandledRejection = (reason: unknown) => {
          unhandledRejections.push(reason);
        };
        const stream = createTestStream(() => {
          writeCount++;
          if (writeCount === 1) {
            return firstWrite.promise;
          }
        });

        process.on('unhandledRejection', onUnhandledRejection);
        try {
          const renderPromise = renderToStreamAndSetPlatform(
            <div>
              prefix
              {Promise.reject(new Error('jsx promise failed'))}
            </div>,
            {
              containerTagName: 'div',
              stream,
              streaming: {
                inOrder: { strategy: 'disabled' },
              },
            }
          );
          await new Promise((resolve) => setImmediate(resolve));

          expect(unhandledRejections).toEqual([]);

          firstWrite.resolve();
          await expect(renderPromise).rejects.toThrow('jsx promise failed');
        } finally {
          process.off('unhandledRejection', onUnhandledRejection);
        }
      });
      it('should handle async component rejection while a flush is pending', async () => {
        const firstWrite = createDeferred();
        let writeCount = 0;
        const unhandledRejections: unknown[] = [];
        const onUnhandledRejection = (reason: unknown) => {
          unhandledRejections.push(reason);
        };
        const AsyncReject = component$(() => {
          const result = useAsync$<JSXOutput>(() =>
            Promise.reject(new Error('async component failed'))
          );
          return result.value;
        });
        const stream = createTestStream(() => {
          writeCount++;
          if (writeCount === 1) {
            return firstWrite.promise;
          }
        });

        process.on('unhandledRejection', onUnhandledRejection);
        try {
          const renderPromise = renderToStreamAndSetPlatform(
            <div>
              prefix
              <AsyncReject />
            </div>,
            {
              containerTagName: 'div',
              stream,
              streaming: {
                inOrder: { strategy: 'disabled' },
              },
            }
          );
          await new Promise((resolve) => setImmediate(resolve));

          expect(unhandledRejections).toEqual([]);

          firstWrite.resolve();
          await expect(renderPromise).rejects.toThrow('async component failed');
        } finally {
          process.off('unhandledRejection', onUnhandledRejection);
        }
      });
      it('should wait for an async direct write before emitting the next one', async () => {
        const firstWrite = createDeferred();
        let writeCount = 0;
        const stream = createTestStream(() => {
          writeCount++;
          if (writeCount === 1) {
            return firstWrite.promise;
          }
          return Promise.resolve();
        });
        const streaming: StreamingOptions = {
          inOrder: {
            strategy: 'direct',
          },
        };
        const LargeOutput = component$(() => (
          <div>
            {Array.from({ length: 40 }, (_, i) => (
              <span key={i}>{`chunk-${i}-` + 'x'.repeat(20)}</span>
            ))}
          </div>
        ));

        const renderPromise = renderToStreamAndSetPlatform(<LargeOutput />, {
          containerTagName: 'div',
          stream,
          streaming,
        });

        await vi.waitFor(() => expect(writeCount).toBeGreaterThan(0));
        await Promise.resolve();
        await Promise.resolve();
        // Direct mode should also serialize async writes. If this becomes > 1
        // before the first promise resolves, direct streaming is still
        // ignoring downstream backpressure.
        expect(writeCount).toBe(1);

        firstWrite.resolve();
        await renderPromise;

        expect(writeCount).toBeGreaterThan(1);
      });
      it('should render chunk by chunk with auto streaming', async () => {
        const stream = createTestStream(vi.fn());
        const streaming: StreamingOptions = {
          inOrder: {
            strategy: 'auto',
            maximumInitialChunk: 200,
            maximumChunk: 100,
          },
        };
        await renderToStreamAndSetPlatform(<Counter />, {
          containerTagName: 'div',
          stream,
          streaming,
        });
        // This can change when the size of the output changes
        expect(stream.write).toHaveBeenCalledTimes(5);
      });

      it('should wait for an async flush before emitting the next chunk', async () => {
        const firstWrite = createDeferred();
        let writeCount = 0;
        const stream = createTestStream(() => {
          writeCount++;
          if (writeCount === 1) {
            return firstWrite.promise;
          }
          return Promise.resolve();
        });
        const streaming: StreamingOptions = {
          inOrder: {
            strategy: 'auto',
            maximumInitialChunk: 50,
            maximumChunk: 50,
          },
        };
        const LargeOutput = component$(() => (
          <div>
            {Array.from({ length: 40 }, (_, i) => (
              <span key={i}>{`chunk-${i}-` + 'x'.repeat(20)}</span>
            ))}
          </div>
        ));

        const renderPromise = renderToStreamAndSetPlatform(<LargeOutput />, {
          containerTagName: 'div',
          stream,
          streaming,
        });

        await vi.waitFor(() => expect(writeCount).toBeGreaterThan(0));
        await Promise.resolve();
        await Promise.resolve();
        // The first async write is still unresolved here, so starting a second
        // write would mean the renderer ignored downstream backpressure and let
        // multiple flushes overlap in flight.
        expect(writeCount).toBe(1);

        firstWrite.resolve();
        await renderPromise;

        expect(writeCount).toBeGreaterThan(1);
      });
    });
  });
});
