import path from 'path';
import React from 'react';
// Reference: https://github.com/ant-design/ant-design/pull/24003#discussion_r427267386
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import { extractStaticStyle } from 'antd-style';
import dayjs from 'dayjs';
import fse from 'fs-extra';
import { globSync } from 'glob';
import { JSDOM } from 'jsdom';
import MockDate from 'mockdate';
import type { HTTPRequest } from 'puppeteer';
import ReactDOMServer from 'react-dom/server';

import { App, ConfigProvider, theme } from '../../components';
import { fillWindowEnv } from '../setup';
import { render } from '../utils';
import { TriggerMockContext } from './demoTestContext';

jest.mock('../../components/grid/hooks/useBreakpoint', () => () => ({}));

const snapshotPath = path.join(process.cwd(), 'imageSnapshots');
fse.ensureDirSync(snapshotPath);

const themes = {
  default: theme.defaultAlgorithm,
  dark: theme.darkAlgorithm,
  compact: theme.compactAlgorithm,
};

interface ImageTestOptions {
  onlyViewport?: boolean;
  ssr?: boolean;
  openTriggerClassName?: string;
}

// eslint-disable-next-line jest/no-export
export default function imageTest(
  component: React.ReactElement,
  identifier: string,
  options: ImageTestOptions,
) {
  let doc: Document;
  let container: HTMLDivElement;

  beforeAll(async () => {
    const dom = new JSDOM('<!DOCTYPE html><body></body></html>', {
      url: 'http://localhost/',
    });
    const win = dom.window;
    doc = win.document;

    (global as any).window = win;

    // Fill env
    const keys = [
      ...Object.keys(win),
      'HTMLElement',
      'SVGElement',
      'ShadowRoot',
      'Element',
      'File',
      'Blob',
    ].filter((key) => !(global as any)[key]);

    keys.forEach((key) => {
      (global as any)[key] = win[key];
    });

    // Fake Resize Observer
    global.ResizeObserver = function FakeResizeObserver() {
      return {
        observe() {},
        unobserve() {},
        disconnect() {},
      };
    } as unknown as typeof ResizeObserver;

    // Fake promise not called
    global.fetch = function mockFetch() {
      return {
        then() {
          return this;
        },
        catch() {
          return this;
        },
        finally() {
          return this;
        },
      };
    } as unknown as typeof fetch;

    // Fake matchMedia
    win.matchMedia = (() => ({
      matches: false,
      addListener: jest.fn(),
      removeListener: jest.fn(),
    })) as unknown as typeof matchMedia;

    // Fill window
    fillWindowEnv(win);

    await page.setRequestInterception(true);
  });

  beforeEach(() => {
    doc.body.innerHTML = `<div id="root"></div>`;
    container = doc.querySelector<HTMLDivElement>('#root')!;
  });

  function test(name: string, suffix: string, themedComponent: React.ReactElement) {
    it(name, async () => {
      await page.setViewport({ width: 800, height: 600 });

      const onRequestHandle = (request: HTTPRequest) => {
        if (['image'].includes(request.resourceType())) {
          request.abort();
        } else {
          request.continue();
        }
      };

      const { openTriggerClassName } = options;

      const requestListener = (request: any) => onRequestHandle(request as HTTPRequest);

      MockDate.set(dayjs('2016-11-22').valueOf());
      page.on('request', requestListener);
      await page.goto(`file://${process.cwd()}/tests/index.html`);
      await page.addStyleTag({ path: `${process.cwd()}/components/style/reset.css` });
      await page.addStyleTag({ content: '*{animation: none!important;}' });

      const cache = createCache();

      const emptyStyleHolder = doc.createElement('div');

      let element = (
        <StyleProvider cache={cache} container={emptyStyleHolder}>
          <App>{themedComponent}</App>
        </StyleProvider>
      );

      // Do inject open trigger
      if (openTriggerClassName) {
        element = (
          <TriggerMockContext.Provider value={{ popupVisible: true }}>
            {element}
          </TriggerMockContext.Provider>
        );
      }

      let html: string;
      let styleStr: string;

      if (options.ssr) {
        html = ReactDOMServer.renderToString(element);
        styleStr = extractStyle(cache) + extractStaticStyle(html).map((item) => item.tag);
      } else {
        const { unmount } = render(element, {
          container,
        });
        html = container.innerHTML;
        styleStr = extractStyle(cache) + extractStaticStyle(html).map((item) => item.tag);
        // We should extract style before unmount
        unmount();
      }

      if (openTriggerClassName) {
        styleStr += `<style>
          .${openTriggerClassName} {
            position: relative !important;
            left: 0 !important;
            top: 0 !important;
            opacity: 1 !important;
            display: inline-block !important;
            vertical-align: top !important;
          }
        </style>`;
      }

      await page.evaluate(
        (innerHTML: string, ssrStyle: string, triggerClassName?: string) => {
          const root = document.querySelector<HTMLDivElement>('#root')!;
          root.innerHTML = innerHTML;
          const head = document.querySelector<HTMLElement>('head')!;
          head.innerHTML += ssrStyle;
          // Inject open trigger with block style
          if (triggerClassName) {
            document.querySelectorAll<HTMLElement>(`.${triggerClassName}`).forEach((node) => {
              const blockStart = document.createElement('div');
              const blockEnd = document.createElement('div');
              node.parentNode?.insertBefore(blockStart, node);
              node.parentNode?.insertBefore(blockEnd, node.nextSibling);
            });
          }
        },
        html,
        styleStr,
        openTriggerClassName || '',
      );

      if (!options.onlyViewport) {
        // Get scroll height of the rendered page and set viewport
        const bodyHeight = await page.evaluate(() => document.body.scrollHeight);
        await page.setViewport({ width: 800, height: bodyHeight });
      }

      const image = await page.screenshot({
        fullPage: !options.onlyViewport,
      });

      await fse.writeFile(path.join(snapshotPath, `${identifier}${suffix}.png`), image);

      MockDate.reset();
      page.off('request', requestListener);
    });
  }

  Object.entries(themes).forEach(([key, algorithm]) => {
    const configTheme = {
      algorithm,
      token: {
        fontFamily: 'Arial',
      },
    };

    test(
      `component image screenshot should correct ${key}`,
      `.${key}`,
      <div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}>
        <ConfigProvider theme={configTheme}>{component}</ConfigProvider>
      </div>,
    );
    test(
      `[CSS Var] component image screenshot should correct ${key}`,
      `.${key}.css-var`,
      <div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}>
        <ConfigProvider theme={{ ...configTheme, cssVar: true }}>{component}</ConfigProvider>
      </div>,
    );
  });
}

type Options = {
  skip?: boolean | string[];
  onlyViewport?: boolean | string[];
  /** Use SSR render instead. Only used when the third part deps component */
  ssr?: boolean;
  /** Open Trigger to check the popup render */
  openTriggerClassName?: string;
};

// eslint-disable-next-line jest/no-export
export function imageDemoTest(component: string, options: Options = {}) {
  let describeMethod = options.skip === true ? describe.skip : describe;
  const files = globSync(`./components/${component}/demo/*.tsx`).filter(
    (file) => !file.includes('_semantic'),
  );

  files.forEach((file) => {
    if (Array.isArray(options.skip) && options.skip.some((c) => file.endsWith(c))) {
      describeMethod = describe.skip;
    } else {
      describeMethod = describe;
    }
    describeMethod(`Test ${file} image`, () => {
      let Demo = require(`../../${file}`).default;
      if (typeof Demo === 'function') {
        Demo = <Demo />;
      }
      imageTest(Demo, `${component}-${path.basename(file, '.tsx')}`, {
        onlyViewport:
          options.onlyViewport === true ||
          (Array.isArray(options.onlyViewport) &&
            options.onlyViewport.some((c) => file.endsWith(c))),
        ssr: options.ssr,
        openTriggerClassName: options.openTriggerClassName,
      });
    });
  });
}
