import { themes } from '@storybook/theming';
import { parse, stringify } from 'qs';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { isTest, isDefined } from '../../types';
const statusUpdatesMap = new Map([[undefined, /(unknown|success|failed|pending|running)/], ['unknown', /(success|failed|pending|running)/], ['success', /(failed|pending|running)/], ['failed', /(pending|running)/], ['pending', /running/]]);
function makeEmptySuiteNode() {
  let path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
  return {
    path,
    skip: true,
    opened: false,
    checked: true,
    indeterminate: false,
    children: {}
  };
}
export function calcStatus(oldStatus, newStatus) {
  return newStatus && statusUpdatesMap.get(oldStatus)?.test(newStatus) ? newStatus : oldStatus;
}
export function getTestPath(test) {
  const {
    browser,
    testName,
    storyPath
  } = test;
  return [...storyPath, testName, browser].filter(isDefined);
}
export function getSuiteByPath(suite, path) {
  return path.reduce((suiteOrTest, pathToken) => isTest(suiteOrTest) ? suiteOrTest : suiteOrTest?.children[pathToken], suite);
}
export function getTestByPath(suite, path) {
  const test = getSuiteByPath(suite, path) ?? suite;
  return isTest(test) ? test : null;
}
export function getTestsByStoryId(suite, storyId) {
  return Object.values(suite.children).filter(isDefined).flatMap(suiteOrTest => {
    if (isTest(suiteOrTest)) return suiteOrTest.storyId === storyId ? suiteOrTest : [];
    return getTestsByStoryId(suiteOrTest, storyId);
  }).filter(isDefined);
}
function checkTests(suiteOrTest, checked) {
  suiteOrTest.checked = checked;
  if (!isTest(suiteOrTest)) {
    suiteOrTest.indeterminate = false;
    Object.values(suiteOrTest.children).filter(isDefined).forEach(child => checkTests(child, checked));
  }
}
function updateChecked(suite) {
  const children = Object.values(suite.children).filter(isDefined).filter(child => !child.skip);
  const checkedEvery = children.every(test => test.checked);
  const checkedSome = children.some(test => test.checked);
  const indeterminate = children.some(test => isTest(test) ? false : test.indeterminate) || !checkedEvery && checkedSome;
  const checked = indeterminate || suite.checked == checkedEvery ? suite.checked : checkedEvery;
  suite.checked = checked;
  suite.indeterminate = indeterminate;
}
export function checkSuite(suite, path, checked) {
  const subSuite = getSuiteByPath(suite, path);
  if (subSuite) checkTests(subSuite, checked);
  path.slice(0, -1).map((_, index, tokens) => tokens.slice(0, tokens.length - index)).forEach(parentPath => {
    const parentSuite = getSuiteByPath(suite, parentPath);
    if (isTest(parentSuite)) return;
    if (parentSuite) updateChecked(parentSuite);
  });
  updateChecked(suite);
}
export function treeifyTests(testsById) {
  const rootSuite = makeEmptySuiteNode();
  rootSuite.opened = true;
  Object.values(testsById).forEach(test => {
    if (!test) return;
    const [browser, ...testPath] = getTestPath(test).reverse();
    const lastSuite = testPath.reverse().reduce((suite, token) => {
      const subSuite = suite.children[token] || makeEmptySuiteNode([...suite.path, token]);
      subSuite.status = calcStatus(subSuite.status, test.status);
      if (!test.skip) subSuite.skip = false;
      if (!subSuite.skip) suite.skip = false;
      suite.children[token] = subSuite;
      suite.status = calcStatus(suite.status, subSuite.status);
      if (isTest(subSuite)) {
        throw new Error(`Suite and Test should not have same path '${JSON.stringify(getTestPath(subSuite))}'`);
      }
      return subSuite;
    }, rootSuite);
    lastSuite.children[browser] = {
      ...test,
      checked: true
    };
  });
  return rootSuite;
}
export function getCheckedTests(suite) {
  return Object.values(suite.children).filter(isDefined).flatMap(suiteOrTest => {
    if (isTest(suiteOrTest)) return suiteOrTest.checked ? suiteOrTest : [];
    if (!suiteOrTest.checked && !suiteOrTest.indeterminate) return [];
    return getCheckedTests(suiteOrTest);
  });
}
export function updateTestStatus(suite, path, update) {
  const title = path.shift();
  if (!title) return;
  const suiteOrTest = suite.children[title] ?? (suite.children[title] = {
    ...(path.length == 0 ? update : makeEmptySuiteNode([...suite.path, title])),
    checked: suite.checked
  });
  if (isTest(suiteOrTest)) {
    const test = suiteOrTest;
    const {
      skip,
      status,
      results,
      approved
    } = update;
    if (isDefined(skip)) test.skip = skip;
    if (isDefined(status)) test.status = status;
    if (isDefined(results)) test.results ? test.results.push(...results) : test.results = results;
    if (isDefined(approved)) Object.entries(approved).forEach(_ref => {
      let [image, retry] = _ref;
      return retry !== undefined && ((test.approved = test.approved || {})[image] = retry);
    });
  } else {
    const subSuite = suiteOrTest;
    updateTestStatus(subSuite, path, update);
  }
  suite.skip = Object.values(suite.children).filter(isDefined).map(_ref2 => {
    let {
      skip
    } = _ref2;
    return skip;
  }).every(Boolean);
  suite.status = Object.values(suite.children).filter(isDefined).map(_ref3 => {
    let {
      status
    } = _ref3;
    return status;
  }).reduce(calcStatus);
}
export function removeTests(suite, path) {
  const title = path.shift();
  if (!title) return;
  const suiteOrTest = suite.children[title];
  if (suiteOrTest && !isTest(suiteOrTest)) removeTests(suiteOrTest, path);
  if (isTest(suiteOrTest) || Object.keys(suiteOrTest?.children ?? {}).length == 0) delete suite.children[title];
  if (Object.keys(suite.children).length == 0) return;
  updateChecked(suite);
  suite.skip = Object.values(suite.children).filter(isDefined).map(_ref4 => {
    let {
      skip
    } = _ref4;
    return skip;
  }).every(Boolean);
  suite.status = Object.values(suite.children).filter(isDefined).map(_ref5 => {
    let {
      status
    } = _ref5;
    return status;
  }).reduce(calcStatus);
}
export function filterTests(suite, filter) {
  const {
    status,
    subStrings
  } = filter;
  if (!status && !subStrings.length) return suite;
  const filteredSuite = {
    ...suite,
    children: {}
  };
  Object.entries(suite.children).forEach(_ref6 => {
    let [title, suiteOrTest] = _ref6;
    if (!suiteOrTest || suiteOrTest.skip) return;
    if (!status && subStrings.some(subString => title.toLowerCase().includes(subString))) {
      filteredSuite.children[title] = suiteOrTest;
    } else if (isTest(suiteOrTest)) {
      if (status && suiteOrTest.status && ['pending', 'running', status].includes(suiteOrTest.status)) filteredSuite.children[title] = suiteOrTest;
    } else {
      const filteredSubSuite = filterTests(suiteOrTest, filter);
      if (Object.keys(filteredSubSuite.children).length == 0) return;
      filteredSuite.children[title] = filteredSubSuite;
    }
  });
  return filteredSuite;
}
export function openSuite(suite, path, opened) {
  const subSuite = path.reduce((suiteOrTest, pathToken) => {
    if (suiteOrTest && !isTest(suiteOrTest)) {
      if (opened) suiteOrTest.opened = opened;
      return suiteOrTest.children[pathToken];
    }
  }, suite);
  if (subSuite && !isTest(subSuite)) subSuite.opened = opened;
}
export function flattenSuite(suite) {
  if (!suite.opened) return [];
  return Object.entries(suite.children).flatMap(_ref7 => {
    let [title, subSuite] = _ref7;
    return subSuite ? [{
      title,
      suite: subSuite
    }, ...(isTest(subSuite) ? [] : flattenSuite(subSuite))] : [];
  });
}
export function countTestsStatus(suite) {
  let successCount = 0;
  let failedCount = 0;
  let skippedCount = 0;
  let pendingCount = 0;
  const cases = Object.values(suite.children).filter(isDefined);
  let suiteOrTest;
  while (suiteOrTest = cases.pop()) {
    if (isTest(suiteOrTest)) {
      if (suiteOrTest.skip) skippedCount++;
      if (suiteOrTest.status === 'success') successCount++;
      if (suiteOrTest.status === 'failed') failedCount++;
      if (suiteOrTest.status === 'pending') pendingCount++;
    } else {
      cases.push(...Object.values(suiteOrTest.children).filter(isDefined));
    }
  }
  return {
    successCount,
    failedCount,
    skippedCount,
    pendingCount
  };
}
export function getConnectionUrl() {
  return [typeof __CREEVEY_SERVER_HOST__ == 'undefined' ? window.location.hostname : __CREEVEY_SERVER_HOST__, typeof __CREEVEY_SERVER_PORT__ == 'undefined' ? window.location.port : __CREEVEY_SERVER_PORT__].filter(Boolean).join(':');
}
export function getImageUrl(path, imageName) {
  // path => [kind, story, test, browser]
  const browser = path.slice(-1)[0];
  const imagesUrl = window.location.host ? `${window.location.protocol}//${getConnectionUrl()}${window.location.pathname == '/' ? '/report' : window.location.pathname.split('/').slice(0, -1).join('/')}/${encodeURI(path.slice(0, -1).join('/'))}` : encodeURI(path.slice(0, -1).join('/'));
  return imageName == browser ? imagesUrl : `${imagesUrl}/${encodeURI(browser)}`;
}
export function getBorderSize(element) {
  // NOTE Firefox returns empty string for `borderWidth` prop
  const borderSize = parseFloat(getComputedStyle(element).borderTopWidth);
  return Number.isNaN(borderSize) ? 0 : borderSize;
}
export function useLoadImages(s1, s2, s3) {
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    setLoaded(false);
    void Promise.all([s1, s2, s3].map(url => new Promise(resolve => {
      const image = document.createElement('img');
      image.src = url;
      image.onload = resolve;
      image.onerror = resolve;
    }))).then(() => setLoaded(true));
  }, [s1, s2, s3]);
  return loaded;
}

/**
 * Uses the ResizeObserver API to observe changes within the given HTML Element DOM Rect.
 *
 * @returns dimensions of element's content box (which means without paddings and border width)
 */
export function useResizeObserver(elementRef, onResize) {
  let debounceTimeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 16;
  const observerRef = useRef(null);
  useEffect(() => {
    if (!elementRef.current) return;
    observerRef.current = new ResizeObserver(onResize);
    observerRef.current.observe(elementRef.current);
    return () => observerRef.current?.disconnect();
  }, [debounceTimeout, elementRef, onResize]);
}
export function useApplyScale(imageRef, scale, dependency) {
  useLayoutEffect(() => {
    if (!imageRef.current) return;
    const image = imageRef.current;
    const borderSize = getBorderSize(image);
    image.style.height = `${image.naturalHeight * scale + borderSize * 2}px`;
  }, [imageRef, scale, dependency]);
}
export function useCalcScale(diffImageRef, loaded) {
  const [scale, setScale] = useState(1);
  const calcScale = useCallback(() => {
    const diffImage = diffImageRef.current;
    if (!diffImage || !loaded) return setScale(1);
    const borderSize = getBorderSize(diffImage);
    const ratio = (diffImage.getBoundingClientRect().width - borderSize * 2) / diffImage.naturalWidth;
    setScale(Math.min(1, ratio));
  }, [diffImageRef, loaded]);
  useResizeObserver(diffImageRef, calcScale);
  useLayoutEffect(calcScale, [calcScale]);
  return scale;
}
const CREEVEY_THEME = 'Creevey_theme';
function isTheme(theme) {
  return isDefined(theme) && Object.prototype.hasOwnProperty.call(themes, theme);
}
function initialTheme() {
  const theme = localStorage.getItem(CREEVEY_THEME);
  return isTheme(theme) ? theme : 'light';
}
export function useTheme() {
  const [theme, setTheme] = useState(initialTheme());
  useEffect(() => {
    localStorage.setItem(CREEVEY_THEME, theme);
  }, [theme]);
  return [theme, setTheme];
}
export function setSearchParams(testPath) {
  const pageUrl = `?${stringify({
    testPath
  })}`;
  window.history.pushState({
    testPath
  }, '', pageUrl);
}
export function getTestPathFromSearch() {
  const {
    testPath
  } = parse(window.location.search.slice(1));
  //@ts-expect-error: This expression is not callable.
  if (Array.isArray(testPath) && testPath.every(token => typeof token == 'string')) {
    return testPath;
  }
  return [];
}
export function useForceUpdate() {
  const [, update] = useState({});
  return useCallback(() => update({}), []);
}