import {
  Fragment as Component,
  Fragment,
  Fragment as InlineComponent,
  Fragment as Projection,
  Fragment as Signal,
  Slot,
  component$,
  createContextId,
  jsx,
  useContext,
  useContextProvider,
  useSignal,
  useStore,
  useVisibleTask$,
  type Signal as SignalType,
  type FunctionComponent,
  type PublicProps,
} from '@qwik.dev/core';
import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
import { describe, expect, it, vi } from 'vitest';
import * as logUtils from '../shared/utils/log';

const debug = false; //true;
Error.stackTraceLimit = 100;

const MyComp = () => {
  return (
    <>
      <h1>Test</h1>
      <p>Lorem</p>
      <h2>ipsum</h2>
      <p>foo</p>
      <h2>bar</h2>
    </>
  );
};

const InlineWrapper = () => {
  return <MyComp />;
};

const ChildrenWrapper = (props: { children?: any }) => props.children;

const Id = (props: any) => <div>Id: {props.id}</div>;

const inlineProjectionContext = createContextId<{ value: string }>('inline-projection');
const InlineReadsContext = () => {
  const ctx = useContext(inlineProjectionContext, { value: 'default' });
  return <span>{ctx.value}</span>;
};

const ChildInline = () => {
  return <div>Child inline</div>;
};

const OwnKeys = {
  HAccordionRoot: (props: any) => {
    return jsx('div', props);
  },
  Root: (props: any) => <OwnKeys.HAccordionRoot {...props} accordionItemComponent={OwnKeys.Item} />,
  Item: component$(() => {
    return <></>;
  }),
};

describe.each([
  { render: ssrRenderToDom }, //
  { render: domRender }, //
])('$render.name: inline component', ({ render }) => {
  it('should render inline component', async () => {
    const MyComp = () => {
      return <>Hello World!</>;
    };

    const { vNode } = await render(<MyComp />, { debug });
    expect(vNode).toMatchVDOM(
      <>
        <>Hello World!</>
      </>
    );
  });

  it('should render nested component', async () => {
    const Child = (props: { name: string }) => {
      return <>{props.name}</>;
    };

    const Parent = (props: { salutation: string; name: string }) => {
      return (
        <>
          {props.salutation} <Child name={props.name} />
        </>
      );
    };

    const { vNode } = await render(<Parent salutation="Hello" name="World" />, {
      debug,
    });
    expect(vNode).toMatchVDOM(
      <Component>
        <Fragment>
          {'Hello'}{' '}
          <Component>
            <Fragment>World</Fragment>
          </Component>
        </Fragment>
      </Component>
    );
  });

  it('should render inline component with null key', async () => {
    const { vNode } = await render(<MyComp key={null} />, { debug });
    expect(vNode).toMatchVDOM(
      <InlineComponent>
        <Fragment>
          <h1>Test</h1>
          <p>Lorem</p>
          <h2>ipsum</h2>
          <p>foo</p>
          <h2>bar</h2>
        </Fragment>
      </InlineComponent>
    );
  });

  it('should toggle component$ and inline wrapper', async () => {
    const Test = component$(() => {
      return <div>Test</div>;
    });
    const Wrapper = component$(() => {
      const toggle = useSignal(true);
      return (
        <>
          <button onClick$={() => (toggle.value = !toggle.value)}></button>
          {toggle.value ? <Test /> : <InlineWrapper />}
        </>
      );
    });

    const { vNode, document } = await render(<Wrapper />, { debug });
    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <Component ssr-required>
            <div>Test</div>
          </Component>
        </Fragment>
      </Component>
    );
    await trigger(document.body, 'button', 'click');
    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <InlineComponent ssr-required>
            <InlineComponent ssr-required>
              <Fragment ssr-required>
                <h1>Test</h1>
                <p>Lorem</p>
                <h2>ipsum</h2>
                <p>foo</p>
                <h2>bar</h2>
              </Fragment>
            </InlineComponent>
          </InlineComponent>
        </Fragment>
      </Component>
    );
    await trigger(document.body, 'button', 'click');
    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <Component>
            <div>Test</div>
          </Component>
        </Fragment>
      </Component>
    );
    await trigger(document.body, 'button', 'click');
    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <InlineComponent ssr-required>
            <InlineComponent ssr-required>
              <Fragment ssr-required>
                <h1>Test</h1>
                <p>Lorem</p>
                <h2>ipsum</h2>
                <p>foo</p>
                <h2>bar</h2>
              </Fragment>
            </InlineComponent>
          </InlineComponent>
        </Fragment>
      </Component>
    );
  });

  it('should not rerender component', async () => {
    const Child = component$((props: { id: number }) => {
      const renders = useStore(
        {
          count: 0,
        },
        { reactive: false }
      );
      renders.count++;
      const rerenders = renders.count + 0;
      return (
        <>
          <Id id={props.id} />
          {rerenders}
        </>
      );
    });
    const Cmp = component$(() => {
      const id = useSignal(0);
      return (
        <>
          <button
            onClick$={() => {
              id.value++;
            }}
          >
            Increment ID
          </button>
          <Child id={id.value} />
        </>
      );
    });

    const { vNode, container } = await render(<Cmp />, { debug });

    expect(vNode).toMatchVDOM(
      <Component>
        <>
          <button>Increment ID</button>
          <Component>
            <Fragment>
              <InlineComponent>
                <div>
                  {'Id: '}
                  <Fragment ssr-required>{'0'}</Fragment>
                </div>
              </InlineComponent>
              1
            </Fragment>
          </Component>
        </>
      </Component>
    );

    await trigger(container.document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component>
        <>
          <button>Increment ID</button>
          <Component>
            <Fragment>
              <InlineComponent>
                <div>
                  {'Id: '}
                  <Fragment ssr-required>{'1'}</Fragment>
                </div>
              </InlineComponent>
              1
            </Fragment>
          </Component>
        </>
      </Component>
    );
  });

  it('should render array of inline components inside normal component', async () => {
    const Cmp = component$(() => {
      const items = useStore(['qwik', 'foo', 'bar']);

      const Item = (props: { name: string }) => {
        return <div>{props.name}</div>;
      };

      return (
        <footer>
          <button onClick$={() => items.sort()}></button>
          {items.map((item, key) => (
            <Item name={item} key={key} />
          ))}
        </footer>
      );
    });

    const { vNode, document } = await render(<Cmp />, { debug });

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <footer>
          <button></button>
          <InlineComponent>
            <div>
              <Signal>qwik</Signal>
            </div>
          </InlineComponent>
          <InlineComponent>
            <div>
              <Signal>foo</Signal>
            </div>
          </InlineComponent>
          <InlineComponent>
            <div>
              <Signal>bar</Signal>
            </div>
          </InlineComponent>
        </footer>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component>
        <footer>
          <button></button>
          <InlineComponent>
            <div>
              <Signal>bar</Signal>
            </div>
          </InlineComponent>
          <InlineComponent>
            <div>
              <Signal>foo</Signal>
            </div>
          </InlineComponent>
          <InlineComponent>
            <div>
              <Signal>qwik</Signal>
            </div>
          </InlineComponent>
        </footer>
      </Component>
    );
  });

  it('should conditionally render different inline component', async () => {
    const Cmp = component$(() => {
      const show = useSignal(true);

      const Item = (props: { name: string }) => {
        return <div>{props.name}</div>;
      };

      const Item2 = (props: { name: string }) => {
        return <span>{props.name}</span>;
      };

      return (
        <footer>
          <button onClick$={() => (show.value = !show.value)}></button>
          {show.value ? <Item name={'foo'} /> : <Item2 name={'bar'} />}
        </footer>
      );
    });

    const { vNode, document } = await render(<Cmp />, { debug });

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <footer>
          <button></button>
          <InlineComponent>
            <div>foo</div>
          </InlineComponent>
        </footer>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <footer>
          <button></button>
          <InlineComponent ssr-required>
            <span>bar</span>
          </InlineComponent>
        </footer>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <footer>
          <button></button>
          <InlineComponent ssr-required>
            <div>foo</div>
          </InlineComponent>
        </footer>
      </Component>
    );
  });

  it('should conditionally render different inline component inside inline component', async () => {
    const Cmp = component$(() => {
      const show = useSignal(true);

      const Item = (props: { name: string }) => {
        return <div>{props.name}</div>;
      };

      const Item2 = (props: { name: string }) => {
        return <span>{props.name}</span>;
      };

      const Wrapper = () => {
        return <>{show.value ? <Item name={'foo'} /> : <Item2 name={'bar'} />}</>;
      };

      return (
        <footer>
          <button onClick$={() => (show.value = !show.value)}></button>
          <Wrapper />
        </footer>
      );
    });

    const { vNode, document } = await render(<Cmp />, { debug });

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <footer>
          <button></button>
          <InlineComponent ssr-required>
            <Fragment ssr-required>
              <InlineComponent ssr-required>
                <div>foo</div>
              </InlineComponent>
            </Fragment>
          </InlineComponent>
        </footer>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <footer>
          <button></button>
          <InlineComponent ssr-required>
            <Fragment ssr-required>
              <InlineComponent ssr-required>
                <span>bar</span>
              </InlineComponent>
            </Fragment>
          </InlineComponent>
        </footer>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <footer>
          <button></button>
          <InlineComponent ssr-required>
            <Fragment ssr-required>
              <InlineComponent ssr-required>
                <div>foo</div>
              </InlineComponent>
            </Fragment>
          </InlineComponent>
        </footer>
      </Component>
    );
  });

  it('should rerender without locate error', async () => {
    const errorSpy = vi.spyOn(logUtils, 'throwErrorAndStop');
    const ReplOutputPanel = component$(() => {
      const store = useStore<any>({
        selectedOutputPanel: 'app',
        serverUrl: undefined,
      });

      const ReplTabButton = (props: any) => {
        return (
          <button id={props.id} class={{ 'active-tab': props.isActive }} onClick$={props.onClick$}>
            {props.text}
          </button>
        );
      };

      useVisibleTask$(() => {
        store.serverUrl = 'test';
      });

      return (
        <div>
          <ReplTabButton
            text="App"
            id="1"
            isActive={store.selectedOutputPanel === 'app'}
            onClick$={async () => {
              store.selectedOutputPanel = 'app';
            }}
          />

          <ReplTabButton
            text="HTML"
            id="2"
            isActive={store.selectedOutputPanel === 'html'}
            onClick$={async () => {
              store.selectedOutputPanel = 'html';
            }}
          />

          {store.serverUrl && <div>test</div>}
        </div>
      );
    });

    const { vNode, document } = await render(<ReplOutputPanel />, { debug });

    if (render === ssrRenderToDom) {
      await trigger(document.body, 'div', 'qvisible');
    }

    expect(vNode).toMatchVDOM(
      <Component>
        <div>
          <Component>
            <button id="1" class="active-tab">
              App
            </button>
          </Component>
          <Component>
            <button id="2" class="">
              HTML
            </button>
          </Component>
          <div>test</div>
        </div>
      </Component>
    );

    await trigger(document.body, 'button[id="2"]', 'click');

    expect(errorSpy).not.toHaveBeenCalled();

    expect(vNode).toMatchVDOM(
      <Component>
        <div>
          <Component>
            <button id="1" class="">
              App
            </button>
          </Component>
          <Component>
            <button id="2" class="active-tab">
              HTML
            </button>
          </Component>
          <div>test</div>
        </div>
      </Component>
    );
  });

  it('should render component$ inside inlined wrapper', async () => {
    interface ComplexWrapperProps {
      foo: string;
    }

    const ComplexWrapper = (
      props: PublicProps<ComplexWrapperProps>,
      key: string | null,
      flags: number
    ) => {
      const cmpFn = component$<ComplexWrapperProps>(({ foo }) => {
        return (
          <div>
            {foo}: <Slot />
          </div>
        );
      });
      return cmpFn(props, key, flags);
    };

    const { vNode } = await render(
      <ComplexWrapper foo="aaa">
        <div>
          bar: <div id="1">Test</div>
        </div>
      </ComplexWrapper>,
      { debug }
    );
    expect(vNode).toMatchVDOM(
      <InlineComponent>
        <Component>
          <div>
            <Signal>aaa</Signal>
            {': '}
            <Projection>
              <div>
                {'bar: '}
                <div id="1">Test</div>
              </div>
            </Projection>
          </div>
        </Component>
      </InlineComponent>
    );
  });

  it('should render component$ inside inlined wrapper - case 2', async () => {
    interface ComplexWrapperProps {
      foo: string;
      aaa: string;
    }

    const ComplexWrapper = (
      props: PublicProps<ComplexWrapperProps>,
      key: string | null,
      flags: number
    ) => {
      const cmpFn = component$<ComplexWrapperProps>(({ foo }) => {
        const cmpFn2 = component$<ComplexWrapperProps>(({ aaa }) => {
          return (
            <div>
              {aaa}: <Slot />
            </div>
          );
        });
        return (
          <div>
            {foo}: <Slot />
            {cmpFn2({ ...props, children: 'Test2' }, key, flags)}
          </div>
        );
      });
      return cmpFn(props, key, flags);
    };

    const { vNode } = await render(
      <ComplexWrapper foo="bar" aaa="bbb">
        Test
      </ComplexWrapper>,
      { debug }
    );

    expect(vNode).toMatchVDOM(
      <InlineComponent>
        <Component>
          <div>
            <Signal>{'bar'}</Signal>
            {': '}
            <Projection>Test</Projection>
            <Component>
              <div>
                <Signal>{'bbb'}</Signal>
                {': '}
                <Projection>Test2</Projection>
              </div>
            </Component>
          </div>
        </Component>
      </InlineComponent>
    );
  });

  it('should render swap component$ and inline component with the same key', async () => {
    const Child = component$(() => {
      return <div>Child component$</div>;
    });

    const Parent = component$(() => {
      const toggle = useSignal(true);
      return (
        <>
          <button onClick$={() => (toggle.value = !toggle.value)}></button>
          {/* same key, simulate different routes and files, but the same keys at the same place */}
          {toggle.value ? <ChildInline key="samekey" /> : <Child key="samekey" />}
        </>
      );
    });

    const { vNode, document } = await render(<Parent />, { debug });

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <InlineComponent ssr-required>
            <div>Child inline</div>
          </InlineComponent>
        </Fragment>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <Component ssr-required>
            <div>Child component$</div>
          </Component>
        </Fragment>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <InlineComponent ssr-required>
            <div>Child inline</div>
          </InlineComponent>
        </Fragment>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Fragment ssr-required>
          <button></button>
          <Component ssr-required>
            <div>Child component$</div>
          </Component>
        </Fragment>
      </Component>
    );
  });

  it('should preserve table cell spans after inline component rerender', async () => {
    const Cmp = component$(() => {
      const sig = useSignal(true);
      const valA = [3, 6, 12, 24];
      const valB = [12, 24];
      sig.value;

      return (
        <div>
          <ChildrenWrapper>
            <table>
              <thead>
                <tr>
                  <th rowSpan={3}>ZZZ</th>
                  <th colSpan={1 + valA.length + valB.length}>AAA</th>
                </tr>
                <tr>
                  <th colSpan={valA.length + 1}>CCC</th>
                  <th colSpan={valB.length}>DDD</th>
                </tr>
                <tr>
                  <th>EEE</th>
                  {valA.map((value, index) => (
                    <th key={index}>{value}</th>
                  ))}
                  {valB.map((value, index) => (
                    <th key={index}>{value}</th>
                  ))}
                </tr>
              </thead>
              <tbody></tbody>
            </table>
            <button onClick$={() => (sig.value = !sig.value)}>submit</button>
          </ChildrenWrapper>
        </div>
      );
    });

    const { document } = await render(<Cmp />, { debug });
    const getColSpans = () =>
      Array.from(
        document.querySelectorAll<HTMLTableCellElement>('th[colspan]'),
        (cell) => cell.colSpan
      );

    expect(getColSpans()).toEqual([7, 5, 2]);
    await trigger(document.body, 'button', 'click');
    expect(getColSpans()).toEqual([7, 5, 2]);
  });

  it('should allow to modify children component props', async () => {
    const Child = component$((props: { name: string }) => {
      return <>{props.name}</>;
    });

    const Parent: FunctionComponent = (props) => {
      (props as any).children.props.name = 'World';
      return (props as any).children;
    };

    const { vNode } = await render(
      <Parent>
        <Child name="Hello" />
      </Parent>,
      { debug }
    );
    expect(vNode).toMatchVDOM(
      <InlineComponent>
        <Component>
          <Fragment>
            <Signal>World</Signal>
          </Fragment>
        </Component>
      </InlineComponent>
    );
  });

  it('should remove children from varProps and not throw', async () => {
    const Cmp = component$(() => {
      return (
        <OwnKeys.Root class="w-96">
          <OwnKeys.Item />
        </OwnKeys.Root>
      );
    });

    await expect(render(<Cmp />, { debug })).resolves.not.toThrow();
  });

  it('should rerender inline component in projection', async () => {
    const Link = component$(() => {
      return (
        <a>
          <Slot />
        </a>
      );
    });

    const Cmp = component$(() => {
      const bool = useSignal(false);

      const FnComp: FunctionComponent<{ bool: SignalType<boolean> }> = (props) => {
        return (
          <button
            onClick$={() => {
              props.bool.value = !props.bool.value;
            }}
          >
            Change {JSON.stringify(props.bool.value)}
          </button>
        );
      };

      return (
        <Link>
          <FnComp bool={bool} />
        </Link>
      );
    });

    const { vNode, document } = await render(<Cmp />, { debug });

    expect(vNode).toMatchVDOM(
      <Component>
        <Component>
          <a>
            <Projection>
              <InlineComponent>
                <button>{'Change false'}</button>
              </InlineComponent>
            </Projection>
          </a>
        </Component>
      </Component>
    );

    await trigger(document.body, 'button', 'click');

    expect(vNode).toMatchVDOM(
      <Component>
        <Component>
          <a>
            <Projection>
              <InlineComponent>
                <button>{'Change true'}</button>
              </InlineComponent>
            </Projection>
          </a>
        </Component>
      </Component>
    );
  });

  it('should resolve context when projected into a context provider', async () => {
    const Provider = component$(() => {
      useContextProvider(inlineProjectionContext, { value: 'provided' });
      return <Slot />;
    });
    const Layout = component$(() => {
      return (
        <Provider>
          <Slot />
        </Provider>
      );
    });
    const App = component$(() => {
      return (
        <Layout>
          <InlineReadsContext />
        </Layout>
      );
    });

    const { vNode } = await render(<App />, { debug });
    expect(vNode).toMatchVDOM(
      <Component ssr-required>
        <Component ssr-required>
          <Component ssr-required>
            <Projection ssr-required>
              <Projection ssr-required>
                <InlineComponent ssr-required>
                  <span>provided</span>
                </InlineComponent>
              </Projection>
            </Projection>
          </Component>
        </Component>
      </Component>
    );
  });
});
