/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {jest} from '@jest/globals';
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import {isMatch} from 'picomatch';
import commander from 'commander';
import webpack from 'webpack';
import {loadContext} from '@docusaurus/core/src/server/index';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
import {sortConfig} from '@docusaurus/core/src/server/plugins/routeConfig';
import {posixPath} from '@docusaurus/utils';
import {normalizePluginOptions} from '@docusaurus/utils-validation';

import pluginContentDocs from '../index';
import {toSidebarsProp} from '../props';
import {DefaultSidebarItemsGenerator} from '../sidebars/generator';
import {DisabledSidebars} from '../sidebars';
import * as cliDocs from '../cli';
import {validateOptions} from '../options';

import type {RouteConfig, Validate, Plugin} from '@docusaurus/types';
import type {
  LoadedVersion,
  Options,
  PluginOptions,
  PropSidebarItemLink,
  PropSidebars,
} from '@docusaurus/plugin-content-docs';
import type {
  SidebarItemsGeneratorOption,
  NormalizedSidebar,
} from '../sidebars/types';

function findDocById(
  version: LoadedVersion | undefined,
  unversionedId: string,
) {
  if (!version) {
    throw new Error('Version not found');
  }
  return version.docs.find((item) => item.unversionedId === unversionedId);
}
function getDocById(version: LoadedVersion | undefined, unversionedId: string) {
  if (!version) {
    throw new Error('Version not found');
  }
  const doc = findDocById(version, unversionedId);
  if (!doc) {
    throw new Error(
      `No doc found with id "${unversionedId}" in version ${
        version.versionName
      }.
Available ids are:\n- ${version.docs.map((d) => d.unversionedId).join('\n- ')}`,
    );
  }
  return doc;
}

const createFakeActions = (contentDir: string) => {
  const routeConfigs: RouteConfig[] = [];
  const dataContainer: {[key: string]: unknown} = {};
  const globalDataContainer: {pluginName?: {pluginId: unknown}} = {};

  const actions = {
    addRoute: (config: RouteConfig) => {
      routeConfigs.push(config);
    },
    createData: (name: string, content: unknown) => {
      dataContainer[name] = content;
      return Promise.resolve(path.join(contentDir, name));
    },
    setGlobalData: (data: unknown) => {
      globalDataContainer.pluginName = {pluginId: data};
    },
  };

  // Query by prefix, because files have a hash at the end so it's not
  // convenient to query by full filename
  function getCreatedDataByPrefix(prefix: string) {
    const entry = Object.entries(dataContainer).find(([key]) =>
      key.startsWith(prefix),
    );
    if (!entry) {
      throw new Error(`No created entry found for prefix "${prefix}".
Entries created:
- ${Object.keys(dataContainer).join('\n- ')}
        `);
    }
    return JSON.parse(entry[1] as string) as PropSidebars;
  }

  // Extra fns useful for tests!
  const utils = {
    getGlobalData: () => globalDataContainer,
    getRouteConfigs: () => routeConfigs,

    checkVersionMetadataPropCreated: (version: LoadedVersion | undefined) => {
      if (!version) {
        throw new Error('Version not found');
      }
      const versionMetadataProp = getCreatedDataByPrefix(
        `version-${_.kebabCase(version.versionName)}-metadata-prop`,
      );
      expect(versionMetadataProp.docsSidebars).toEqual(toSidebarsProp(version));
    },

    expectSnapshot: () => {
      // Sort the route config like in src/server/plugins/index.ts for
      // consistent snapshot ordering
      sortConfig(routeConfigs);
      expect(routeConfigs).not.toEqual([]);
      expect(routeConfigs).toMatchSnapshot('route config');
      expect(dataContainer).toMatchSnapshot('data');
      expect(globalDataContainer).toMatchSnapshot('global data');
    },
  };

  return {
    actions,
    utils,
  };
};

describe('sidebar', () => {
  it('site with wrong sidebar content', async () => {
    const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
    const context = await loadContext({siteDir});
    const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          sidebarPath,
        },
      }),
    );
    await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot();
  });

  it('site with wrong sidebar file path', async () => {
    const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
    const context = await loadContext({siteDir});

    await expect(async () => {
      const plugin = await pluginContentDocs(
        context,
        validateOptions({
          validate: normalizePluginOptions as Validate<Options, PluginOptions>,
          options: {
            sidebarPath: 'wrong-path-sidebar.json',
          },
        }),
      );

      await plugin.loadContent!();
    }).rejects.toThrowErrorMatchingInlineSnapshot(`
            "The path to the sidebar file does not exist at "wrong-path-sidebar.json".
            Please set the docs "sidebarPath" field in your config file to:
            - a sidebars path that exists
            - false: to disable the sidebar
            - undefined: for Docusaurus to generate it automatically"
          `);
  });

  it('site with undefined sidebar', async () => {
    const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
    const context = await loadContext({siteDir});
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          sidebarPath: undefined,
        },
      }),
    );
    const result = await plugin.loadContent!();

    expect(result.loadedVersions).toHaveLength(1);
    expect(result.loadedVersions[0]!.sidebars).toMatchSnapshot();
  });

  it('site with disabled sidebar', async () => {
    const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
    const context = await loadContext({siteDir});
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          sidebarPath: false,
        },
      }),
    );
    const result = await plugin.loadContent!();

    expect(result.loadedVersions).toHaveLength(1);
    expect(result.loadedVersions[0]!.sidebars).toEqual(DisabledSidebars);
  });
});

describe('empty/no docs website', () => {
  const siteDir = path.join(__dirname, '__fixtures__', 'empty-site');

  it('no files in docs folder', async () => {
    const context = await loadContext({siteDir});
    await fs.ensureDir(path.join(siteDir, 'docs'));
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {},
      }),
    );
    await expect(
      plugin.loadContent!(),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      `"Docs version "current" has no docs! At least one doc should exist at "docs"."`,
    );
  });

  it('docs folder does not exist', async () => {
    const context = await loadContext({siteDir});
    await expect(
      pluginContentDocs(
        context,
        validateOptions({
          validate: normalizePluginOptions as Validate<Options, PluginOptions>,
          options: {
            path: 'path/does/not/exist',
          },
        }),
      ),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      `"The docs folder does not exist for version "current". A docs folder is expected to be found at path/does/not/exist."`,
    );
  });
});

describe('simple website', () => {
  async function loadSite() {
    const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
    const context = await loadContext({siteDir});
    const sidebarPath = path.join(siteDir, 'sidebars.json');
    const options = validateOptions({
      validate: normalizePluginOptions as Validate<Options, PluginOptions>,
      options: {
        path: 'docs',
        sidebarPath,
      },
    });
    const plugin = await pluginContentDocs(context, options);
    const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);

    return {siteDir, context, sidebarPath, plugin, options, pluginContentDir};
  }

  it('extendCli - docsVersion', async () => {
    const {plugin, options, context} = await loadSite();
    const mock = jest
      .spyOn(cliDocs, 'cliDocsVersionCommand')
      .mockImplementation(async () => {});
    const cli = new commander.Command();
    // @ts-expect-error: in actual usage, we pass the static commander instead
    // of the new command
    plugin.extendCli!(cli);
    cli.parse(['node', 'test', 'docs:version', '1.0.0']);
    expect(mock).toHaveBeenCalledTimes(1);
    expect(mock).toHaveBeenCalledWith('1.0.0', options, context);
    mock.mockRestore();
  });

  it('getPathToWatch', async () => {
    const {siteDir, plugin} = await loadSite();

    const pathToWatch = plugin.getPathsToWatch!();
    const matchPattern = pathToWatch.map((filepath) =>
      posixPath(path.relative(siteDir, filepath)),
    );
    expect(matchPattern).toMatchSnapshot();
    expect(isMatch('docs/hello.md', matchPattern)).toBe(true);
    expect(isMatch('docs/hello.mdx', matchPattern)).toBe(true);
    expect(isMatch('docs/foo/bar.md', matchPattern)).toBe(true);
    expect(isMatch('docs/hello.js', matchPattern)).toBe(false);
    expect(isMatch('docs/super.mdl', matchPattern)).toBe(false);
    expect(isMatch('docs/mdx', matchPattern)).toBe(false);
    expect(isMatch('docs/headingAsTitle.md', matchPattern)).toBe(true);
    expect(isMatch('sidebars.json', matchPattern)).toBe(true);
    expect(isMatch('versioned_docs/hello.md', matchPattern)).toBe(false);
    expect(isMatch('hello.md', matchPattern)).toBe(false);
    expect(isMatch('super/docs/hello.md', matchPattern)).toBe(false);
  });

  it('configureWebpack', async () => {
    const {plugin} = await loadSite();

    const content = await plugin.loadContent?.();

    const config = applyConfigureWebpack(
      plugin.configureWebpack as NonNullable<Plugin['configureWebpack']>,
      {
        entry: './src/index.js',
        output: {
          filename: 'main.js',
          path: path.resolve(__dirname, 'dist'),
        },
      },
      false,
      undefined,
      content,
    );
    const errors = webpack.validate(config);
    expect(errors).toBeUndefined();
  });

  it('content', async () => {
    const {plugin, pluginContentDir} = await loadSite();
    const content = await plugin.loadContent!();
    expect(content.loadedVersions).toHaveLength(1);
    const [currentVersion] = content.loadedVersions;

    expect(findDocById(currentVersion, 'foo/baz')).toMatchSnapshot();

    expect(findDocById(currentVersion, 'hello')).toMatchSnapshot();

    expect(getDocById(currentVersion, 'foo/bar')).toMatchSnapshot();

    expect(currentVersion!.sidebars).toMatchSnapshot();

    const {actions, utils} = createFakeActions(pluginContentDir);

    await plugin.contentLoaded!({
      content,
      actions,
      allContent: {},
    });

    utils.checkVersionMetadataPropCreated(currentVersion);

    utils.expectSnapshot();

    expect(utils.getGlobalData()).toMatchSnapshot();
  });
});

describe('versioned website', () => {
  async function loadSite() {
    const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
    const context = await loadContext({siteDir});
    const sidebarPath = path.join(siteDir, 'sidebars.json');
    const routeBasePath = 'docs';
    const options = validateOptions({
      validate: normalizePluginOptions as Validate<Options, PluginOptions>,
      options: {
        routeBasePath,
        sidebarPath,
        versions: {
          '1.0.1': {
            noIndex: true,
          },
        },
      },
    });
    const plugin = await pluginContentDocs(context, options);
    const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
    return {
      siteDir,
      context,
      routeBasePath,
      sidebarPath,
      options,
      plugin,
      pluginContentDir,
    };
  }

  it('extendCli - docsVersion', async () => {
    const {plugin, context, options} = await loadSite();
    const mock = jest
      .spyOn(cliDocs, 'cliDocsVersionCommand')
      .mockImplementation(async () => {});
    const cli = new commander.Command();
    // @ts-expect-error: in actual usage, we pass the static commander instead
    // of the new command
    plugin.extendCli!(cli);
    cli.parse(['node', 'test', 'docs:version', '2.0.0']);
    expect(mock).toHaveBeenCalledTimes(1);
    expect(mock).toHaveBeenCalledWith('2.0.0', options, context);
    mock.mockRestore();
  });

  it('getPathToWatch', async () => {
    const {siteDir, plugin} = await loadSite();
    const pathToWatch = plugin.getPathsToWatch!();
    const matchPattern = pathToWatch.map((filepath) =>
      posixPath(path.relative(siteDir, filepath)),
    );
    expect(matchPattern).not.toEqual([]);
    expect(matchPattern).toMatchSnapshot();
    expect(isMatch('docs/hello.md', matchPattern)).toBe(true);
    expect(isMatch('docs/hello.mdx', matchPattern)).toBe(true);
    expect(isMatch('docs/foo/bar.md', matchPattern)).toBe(true);
    expect(isMatch('sidebars.json', matchPattern)).toBe(true);
    expect(isMatch('versioned_docs/version-1.0.0/hello.md', matchPattern)).toBe(
      true,
    );
    expect(
      isMatch('versioned_docs/version-1.0.0/foo/bar.md', matchPattern),
    ).toBe(true);
    expect(
      isMatch('versioned_sidebars/version-1.0.0-sidebars.json', matchPattern),
    ).toBe(true);

    // Non existing version
    expect(
      isMatch('versioned_docs/version-2.0.0/foo/bar.md', matchPattern),
    ).toBe(false);
    expect(isMatch('versioned_docs/version-2.0.0/hello.md', matchPattern)).toBe(
      false,
    );
    expect(
      isMatch('versioned_sidebars/version-2.0.0-sidebars.json', matchPattern),
    ).toBe(false);

    expect(isMatch('docs/hello.js', matchPattern)).toBe(false);
    expect(isMatch('docs/super.mdl', matchPattern)).toBe(false);
    expect(isMatch('docs/mdx', matchPattern)).toBe(false);
    expect(isMatch('hello.md', matchPattern)).toBe(false);
    expect(isMatch('super/docs/hello.md', matchPattern)).toBe(false);
  });

  it('content', async () => {
    const {plugin, pluginContentDir} = await loadSite();
    const content = await plugin.loadContent!();
    expect(content.loadedVersions).toHaveLength(4);
    const [currentVersion, version101, version100, versionWithSlugs] =
      content.loadedVersions;

    // foo/baz.md only exists in version -1.0.0
    expect(findDocById(currentVersion, 'foo/baz')).toBeUndefined();
    expect(findDocById(version101, 'foo/baz')).toBeUndefined();
    expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined();

    expect(getDocById(currentVersion, 'foo/bar')).toMatchSnapshot();
    expect(getDocById(version101, 'foo/bar')).toMatchSnapshot();

    expect(getDocById(currentVersion, 'hello')).toMatchSnapshot();
    expect(getDocById(version101, 'hello')).toMatchSnapshot();
    expect(getDocById(version100, 'foo/baz')).toMatchSnapshot();

    expect(currentVersion!.sidebars).toMatchSnapshot(
      'current version sidebars',
    );
    expect(version101!.sidebars).toMatchSnapshot('101 version sidebars');
    expect(version100!.sidebars).toMatchSnapshot('100 version sidebars');
    expect(versionWithSlugs!.sidebars).toMatchSnapshot(
      'withSlugs version sidebars',
    );

    const {actions, utils} = createFakeActions(pluginContentDir);
    await plugin.contentLoaded!({
      content,
      actions,
      allContent: {},
    });

    utils.checkVersionMetadataPropCreated(currentVersion);
    utils.checkVersionMetadataPropCreated(version101);
    utils.checkVersionMetadataPropCreated(version100);
    utils.checkVersionMetadataPropCreated(versionWithSlugs);

    utils.expectSnapshot();
  });
});

describe('versioned website (community)', () => {
  async function loadSite() {
    const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
    const context = await loadContext({siteDir});
    const sidebarPath = path.join(siteDir, 'community_sidebars.json');
    const routeBasePath = 'community';
    const pluginId = 'community';
    const options = validateOptions({
      validate: normalizePluginOptions as Validate<Options, PluginOptions>,
      options: {
        id: 'community',
        path: 'community',
        routeBasePath,
        sidebarPath,
      },
    });
    const plugin = await pluginContentDocs(context, options);
    const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
    return {
      siteDir,
      context,
      routeBasePath,
      sidebarPath,
      pluginId,
      options,
      plugin,
      pluginContentDir,
    };
  }

  it('extendCli - docsVersion', async () => {
    const {pluginId, plugin, options, context} = await loadSite();
    const mock = jest
      .spyOn(cliDocs, 'cliDocsVersionCommand')
      .mockImplementation(async () => {});
    const cli = new commander.Command();
    // @ts-expect-error: in actual usage, we pass the static commander instead
    // of the new command
    plugin.extendCli!(cli);
    cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']);
    expect(mock).toHaveBeenCalledTimes(1);
    expect(mock).toHaveBeenCalledWith('2.0.0', options, context);
    mock.mockRestore();
  });

  it('getPathToWatch', async () => {
    const {siteDir, plugin} = await loadSite();
    const pathToWatch = plugin.getPathsToWatch!();
    const matchPattern = pathToWatch.map((filepath) =>
      posixPath(path.relative(siteDir, filepath)),
    );
    expect(matchPattern).not.toEqual([]);
    expect(matchPattern).toMatchSnapshot();
    expect(isMatch('community/team.md', matchPattern)).toBe(true);
    expect(
      isMatch('community_versioned_docs/version-1.0.0/team.md', matchPattern),
    ).toBe(true);

    // Non existing version
    expect(
      isMatch('community_versioned_docs/version-2.0.0/team.md', matchPattern),
    ).toBe(false);
    expect(
      isMatch(
        'community_versioned_sidebars/version-2.0.0-sidebars.json',
        matchPattern,
      ),
    ).toBe(false);

    expect(isMatch('community/team.js', matchPattern)).toBe(false);
    expect(
      isMatch('community_versioned_docs/version-1.0.0/team.js', matchPattern),
    ).toBe(false);
  });

  it('content', async () => {
    const {plugin, pluginContentDir} = await loadSite();
    const content = await plugin.loadContent!();
    expect(content.loadedVersions).toHaveLength(2);
    const [currentVersion, version100] = content.loadedVersions;

    expect(getDocById(currentVersion, 'team')).toMatchSnapshot();
    expect(getDocById(version100, 'team')).toMatchSnapshot();

    expect(currentVersion!.sidebars).toMatchSnapshot(
      'current version sidebars',
    );
    expect(version100!.sidebars).toMatchSnapshot('100 version sidebars');

    const {actions, utils} = createFakeActions(pluginContentDir);
    await plugin.contentLoaded!({
      content,
      actions,
      allContent: {},
    });

    utils.checkVersionMetadataPropCreated(currentVersion);
    utils.checkVersionMetadataPropCreated(version100);

    utils.expectSnapshot();
  });
});

describe('site with doc label', () => {
  async function loadSite() {
    const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
    const context = await loadContext({siteDir});
    const sidebarPath = path.join(siteDir, 'sidebars.json');
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          path: 'docs',
          sidebarPath,
        },
      }),
    );

    const content = (await plugin.loadContent?.())!;

    return {content};
  }

  it('label in sidebar.json is used', async () => {
    const {content} = await loadSite();
    const loadedVersion = content.loadedVersions[0]!;
    const sidebarProps = toSidebarsProp(loadedVersion);

    expect((sidebarProps.docs![0] as PropSidebarItemLink).label).toBe(
      'Hello One',
    );
  });

  it('sidebar_label in doc has higher precedence over label in sidebar.json', async () => {
    const {content} = await loadSite();
    const loadedVersion = content.loadedVersions[0]!;
    const sidebarProps = toSidebarsProp(loadedVersion);

    expect((sidebarProps.docs![1] as PropSidebarItemLink).label).toBe(
      'Hello 2 From Doc',
    );
  });
});

describe('site with full autogenerated sidebar', () => {
  async function loadSite() {
    const siteDir = path.join(
      __dirname,
      '__fixtures__',
      'site-with-autogenerated-sidebar',
    );
    const context = await loadContext({siteDir});
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          path: 'docs',
        },
      }),
    );

    const content = (await plugin.loadContent?.())!;

    return {content, siteDir};
  }

  it('sidebar is fully autogenerated', async () => {
    const {content} = await loadSite();
    const version = content.loadedVersions[0]!;

    expect(version.sidebars).toMatchSnapshot();
  });

  it('docs in fully generated sidebar have correct metadata', async () => {
    const {content} = await loadSite();
    const version = content.loadedVersions[0]!;

    expect(getDocById(version, 'getting-started')).toMatchSnapshot();
    expect(getDocById(version, 'installation')).toMatchSnapshot();
    expect(getDocById(version, 'Guides/guide1')).toMatchSnapshot();
    expect(getDocById(version, 'Guides/guide2')).toMatchSnapshot();
    expect(getDocById(version, 'Guides/guide2.5')).toMatchSnapshot();
    expect(getDocById(version, 'Guides/guide3')).toMatchSnapshot();
    expect(getDocById(version, 'Guides/guide4')).toMatchSnapshot();
    expect(getDocById(version, 'Guides/guide5')).toMatchSnapshot();
    expect(getDocById(version, 'API/api-overview')).toMatchSnapshot();
    expect(getDocById(version, 'API/Core APIs/Client API')).toMatchSnapshot();
    expect(getDocById(version, 'API/Core APIs/Server API')).toMatchSnapshot();
    expect(
      getDocById(version, 'API/Extension APIs/Plugin API'),
    ).toMatchSnapshot();
    expect(
      getDocById(version, 'API/Extension APIs/Theme API'),
    ).toMatchSnapshot();
    expect(getDocById(version, 'API/api-end')).toMatchSnapshot();
  });
});

describe('site with partial autogenerated sidebars', () => {
  async function loadSite() {
    const siteDir = path.join(
      __dirname,
      '__fixtures__',
      'site-with-autogenerated-sidebar',
    );
    const context = await loadContext({siteDir});
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          path: 'docs',
          sidebarPath: path.join(
            __dirname,
            '__fixtures__',
            'site-with-autogenerated-sidebar',
            'partialAutogeneratedSidebars.js',
          ),
        },
      }),
    );

    const content = (await plugin.loadContent?.())!;

    return {content, siteDir};
  }

  it('sidebar is partially autogenerated', async () => {
    const {content} = await loadSite();
    const version = content.loadedVersions[0]!;

    expect(version.sidebars).toMatchSnapshot();
  });

  it('docs in partially generated sidebar have correct metadata', async () => {
    const {content} = await loadSite();
    const version = content.loadedVersions[0]!;

    // Only looking at the docs of the autogen sidebar, others metadata should
    // not be affected

    expect(getDocById(version, 'API/api-end')).toMatchSnapshot();
    expect(getDocById(version, 'API/api-overview')).toMatchSnapshot();
    expect(
      getDocById(version, 'API/Extension APIs/Plugin API'),
    ).toMatchSnapshot();
    expect(
      getDocById(version, 'API/Extension APIs/Theme API'),
    ).toMatchSnapshot();
  });
});

describe('site with partial autogenerated sidebars 2 (fix #4638)', () => {
  // Test added for edge case https://github.com/facebook/docusaurus/issues/4638

  async function loadSite() {
    const siteDir = path.join(
      __dirname,
      '__fixtures__',
      'site-with-autogenerated-sidebar',
    );
    const context = await loadContext({siteDir});
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          path: 'docs',
          sidebarPath: path.join(
            __dirname,
            '__fixtures__',
            'site-with-autogenerated-sidebar',
            'partialAutogeneratedSidebars2.js',
          ),
        },
      }),
    );

    const content = (await plugin.loadContent?.())!;

    return {content, siteDir};
  }

  it('sidebar is partially autogenerated', async () => {
    const {content} = await loadSite();
    const version = content.loadedVersions[0]!;

    expect(version.sidebars).toMatchSnapshot();
  });
});

describe('site with custom sidebar items generator', () => {
  async function loadSite(sidebarItemsGenerator: SidebarItemsGeneratorOption) {
    const siteDir = path.join(
      __dirname,
      '__fixtures__',
      'site-with-autogenerated-sidebar',
    );
    const context = await loadContext({siteDir});
    const plugin = await pluginContentDocs(
      context,
      validateOptions({
        validate: normalizePluginOptions as Validate<Options, PluginOptions>,
        options: {
          path: 'docs',
          sidebarItemsGenerator,
        },
      }),
    );
    const content = (await plugin.loadContent?.())!;
    return {content, siteDir};
  }

  it('sidebarItemsGenerator is called with appropriate data', async () => {
    const customSidebarItemsGeneratorMock = jest.fn(async () => []);
    const {siteDir} = await loadSite(customSidebarItemsGeneratorMock);

    const generatorArg = (
      customSidebarItemsGeneratorMock.mock
        .calls[0] as unknown as Parameters<SidebarItemsGeneratorOption>
    )[0];

    // Make test pass even if docs are in different order and paths are
    // absolutes
    function makeDeterministic(
      arg: Parameters<SidebarItemsGeneratorOption>[0],
    ): Parameters<SidebarItemsGeneratorOption>[0] {
      return {
        ...arg,
        docs: _.orderBy(arg.docs, 'id'),
        version: {
          ...arg.version,
          contentPath: path.relative(siteDir, arg.version.contentPath),
        },
      };
    }

    expect(makeDeterministic(generatorArg)).toMatchSnapshot();
    expect(generatorArg.defaultSidebarItemsGenerator).toEqual(
      DefaultSidebarItemsGenerator,
    );
  });

  it('sidebar is autogenerated according to a custom sidebarItemsGenerator', async () => {
    const customSidebarItemsGenerator: SidebarItemsGeneratorOption =
      async () => [
        {type: 'doc', id: 'API/api-overview'},
        {type: 'doc', id: 'API/api-end'},
      ];

    const {content} = await loadSite(customSidebarItemsGenerator);
    const version = content.loadedVersions[0];

    expect(version!.sidebars).toMatchSnapshot();
  });

  it('sidebarItemsGenerator can wrap/enhance/sort/reverse the default sidebar generator', async () => {
    function reverseSidebarItems(items: NormalizedSidebar): NormalizedSidebar {
      const result: NormalizedSidebar = items.map((item) => {
        if (item.type === 'category') {
          return {...item, items: reverseSidebarItems(item.items)};
        }
        return item;
      });
      result.reverse();
      return result;
    }

    const reversedSidebarItemsGenerator: SidebarItemsGeneratorOption = async ({
      defaultSidebarItemsGenerator,
      ...args
    }) => {
      const sidebarItems = await defaultSidebarItemsGenerator(args);
      return reverseSidebarItems(sidebarItems);
    };

    const {content} = await loadSite(reversedSidebarItemsGenerator);
    const version = content.loadedVersions[0]!;

    expect(version.sidebars).toMatchSnapshot();
  });
});
