{ lib, pkgs, ... }:
let
  pluginStubs = pkgs.callPackage ../../../utils/plugin-stubs.nix { };

  # Assertion for a number of plugins of given type defined in nvimPackage.packpathDirs
  expectNPlugins =
    config: type: n:
    let
      # 'build.extraFiles' must not be combined, so exclude it from counting
      plugins = builtins.filter (
        p: lib.getName p != lib.getName config.build.extraFiles
      ) config.build.nvimPackage.packpathDirs.myNeovimPackages.${type};
      numPlugins = builtins.length plugins;
    in
    {
      assertion = numPlugins == n;
      message = "Expected ${toString n} '${type}' plugins defined in 'nvimPackage.packpathDirs', got ${toString numPlugins}: ${
        lib.concatMapStringsSep ", " lib.getName plugins
      }.";
    };
  # Assertion that exactly one start plugin is defined in nvimPackage.packpathDirs
  expectOneStartPlugin = config: expectNPlugins config "start" 1;
in
{
  # Test basic functionality
  default =
    { config, ... }:
    {
      performance.combinePlugins.enable = true;
      extraPlugins = pluginStubs.pluginPack;
      extraConfigLuaPost = ''
        ${pluginStubs.pluginChecks}

        -- No separate plugin entry in vim.api.nvim_list_runtime_paths()
        ${lib.concatMapStrings (
          name: # lua
          ''
            assert(not vim.iter(vim.api.nvim_list_runtime_paths()):any(function(entry)
              return entry:find("${name}", 1, true)
            end), "plugin '${name}' found in runtime, expected to be combined")
          '') pluginStubs.pluginNames}
      '';
      assertions = [
        (expectOneStartPlugin config)
      ];
    };

  # Test disabled option
  disabled =
    { config, ... }:
    {
      performance.combinePlugins.enable = false;
      extraPlugins = pluginStubs.pluginPack;
      extraConfigLuaPost = lib.concatMapStringsSep "\n" (
        name:
        # lua
        ''
          -- Separate plugin entry in vim.api.nvim_list_runtime_paths()
          assert(vim.iter(vim.api.nvim_list_runtime_paths()):any(function(entry)
            return entry:find("${name}", 1, true)
          end), "plugin '${name}' isn't found in runtime as a separate entry, expected not to be combined")
        '') pluginStubs.pluginNames;
      assertions = [
        (expectNPlugins config "start" (builtins.length pluginStubs.pluginPack))
      ];
    };

  # Test that pathsToLink option works
  paths-to-link =
    { config, ... }:
    {
      performance.combinePlugins = {
        enable = true;
        pathsToLink = [ "/_extra" ];
      };
      extraPlugins = [
        # A plugin with extra directory
        (pluginStubs.mkPlugin "extra" {
          postInstall = ''
            mkdir $out/_extra
            touch $out/_extra/test
          '';
        })
      ];
      extraConfigLuaPost = ''
        ${pluginStubs.pluginChecksFor [ "extra" ]}

        -- Test file is in runtime
        assert(
          vim.api.nvim_get_runtime_file("_extra/test", false)[1],
          "'_extra/test' file isn't found in runtime, expected to be found"
        )
      '';
      assertions = [
        (expectOneStartPlugin config)
      ];
    };

  # Test that optional plugins are handled
  optional-plugins =
    { config, ... }:
    {
      performance.combinePlugins.enable = true;
      extraPlugins = with pluginStubs; [
        # Start plugins
        plugin1
        plugin3
        # Optional plugin
        {
          plugin = plugin2;
          optional = true;
        }
        # Optional plugin with dependencies on plugin3 and plugin4
        # Dependencies should not be duplicated
        {
          plugin = pluginWithDep4;
          optional = true;
        }
      ];
      extraConfigLuaPost = ''
        -- Start plugins are working. Dependencies of the optional plugins are also available.
        ${pluginStubs.pluginChecksFor [
          "plugin1"
          "plugin3"
          "plugin4" # Dependency of the optional plugin
        ]}

        -- Lua libraries are available. Libs of the optional plugins are also available.
        ${pluginStubs.libChecksFor [
          "lib1"
          "lib2" # Dependency of the optional plugin
          "lib3"
        ]}

        ${lib.concatMapStrings
          (
            name: # lua
            ''
              -- Optional plugin is not loadable
              local ok = pcall(require, "${name}")
              assert(not ok, "${name} is loadable, expected it to be an opt plugin")

              -- Load plugin
              vim.cmd.packadd("${name}")

              -- Now opt plugin is working
              ${pluginStubs.pluginChecksFor [ name ]}
            '')
          [
            "plugin2"
            "plugin_with_dep4"
          ]
        }

        -- Only one copy of dependent plugin should be available
        ${lib.concatMapStrings
          (
            name: # lua
            ''
              local num_plugins = #vim.api.nvim_get_runtime_file("lua/${name}/init.lua", true)
              assert(num_plugins == 1, "expected 1 copy of ${name}, got " .. num_plugins)
            '')
          [
            "plugin3"
            "plugin4"
          ]
        }
      '';
      assertions = [
        (expectOneStartPlugin config)
        # plugin2 plugin_with_dep4
        (expectNPlugins config "opt" 2)
      ];
    };

  # Test that plugin configs are handled
  configs =
    { config, ... }:
    {
      performance.combinePlugins.enable = true;
      extraPlugins = with pluginStubs; [
        # A plugin without config
        plugin1
        # A plugin with config
        {
          plugin = plugin2;
          config = "let g:plugin2_var = 1";
        }
        # Optional plugin with config
        {
          plugin = plugin3;
          optional = true;
          config = "let g:plugin3_var = 1";
        }
      ];
      extraConfigLuaPost = ''
        -- Configs are evaluated
        assert(vim.g.plugin2_var == 1, "plugin2's config isn't evaluated")
        assert(vim.g.plugin3_var == 1, "plugin3's config isn't evaluated")
      '';
      assertions = [
        (expectOneStartPlugin config)
      ];
    };

  # Test if plenary.filetype is working
  plenary-nvim = {
    performance.combinePlugins.enable = true;
    extraPlugins = [ pkgs.vimPlugins.plenary-nvim ];
    extraConfigLuaPost = ''
      -- Plenary filetype detection is usable
      assert(require("plenary.filetype").detect(".bashrc") == "sh", "plenary.filetype is not working")
    '';
  };
}
//
  # Test that config.build.extraFiles is not combined
  # with or without byteCompileLua.enable = true
  lib.genAttrs [ "files-plugin" "files-plugin-byte-compiled" ] (
    name:
    { config, ... }:
    {
      performance = {
        combinePlugins.enable = true;
        byteCompileLua = lib.optionalAttrs (lib.hasSuffix "byte-compiled" name) {
          enable = true;
          plugins = true;
        };
      };
      extraPlugins = with pluginStubs; [
        plugin1
        plugin2
      ];
      # Ensure that build.extraFiles is added to extraPlugins
      wrapRc = true;
      # Extra user files colliding with plugins
      extraFiles = {
        "lua/plugin1/init.lua".text = "return 1";
      };
      # Another form of user files
      files = {
        "lua/plugin2/init.lua" = {
          extraConfigLua = "return 1";
        };
      };
      extraConfigLuaPost = ''
        for _, file in ipairs({"lua/plugin1/init.lua", "lua/plugin2/init.lua"}) do
          local paths_found = vim.api.nvim_get_runtime_file(file, true)
          local num_found = #paths_found

          -- Both plugin and user version are available
          assert(num_found == 2, "expected exactly 2 versions of '" .. file .. "', got " .. num_found)

          -- First found file is from build.extraFiles
          assert(
            paths_found[1]:find("${lib.getName config.build.extraFiles}", 1, true),
            "expected first found '" .. file .. "' to be from build.extraFiles, got " .. paths_found[1]
          )
        end
      '';
      assertions = [
        (expectOneStartPlugin config)
      ];
    }
  )
//
  # Test that standalonePlugins option works
  # with or without byteCompileLua.enable = true
  lib.genAttrs [ "standalone-plugins" "standalone-plugins-byte-compiled" ] (
    name:
    { config, ... }:
    let
      standalonePlugins = [
        # By plugin name
        "plugin1"
        # By package itself. Its dependency, plugin4, not in this list, so will be combined
        pluginStubs.pluginWithDep4
        # Dependency of other plugin
        "plugin5"
        # Both dependency and top-level plugin
        "plugin3"
      ];
    in
    {
      performance = {
        combinePlugins = {
          enable = true;
          inherit standalonePlugins;
        };
        byteCompileLua = lib.optionalAttrs (lib.hasSuffix "byte-compiled" name) {
          enable = true;
          plugins = true;
        };
      };
      extraPlugins = pluginStubs.pluginPack;
      extraConfigLuaPost = ''
        ${pluginStubs.pluginChecks}

        ${lib.concatMapStringsSep "\n" (
          name:
          let
            isStandalone = builtins.elem name (
              map (x: if builtins.isString x then x else lib.getName x) standalonePlugins
            );
            expectedText = if isStandalone then "standalone" else "combined";
          in
          # lua
          ''
            -- Check that ${name} plugin is ${expectedText}
            local paths = vim.api.nvim_get_runtime_file("lua/${name}", true)
            -- Plugins shouldn't be duplicated
            assert(#paths == 1, "expected exactly 1 copy of '${name}' in runtime, got ", #paths)
            -- Test if plugin is standalone. This matches directory name before '/lua/'.
            local is_standalone = paths[1]:match("^(.+)/lua/"):find("${name}", 1, true) ~= nil
            assert(
                is_standalone == ${lib.nixvim.toLuaObject isStandalone},
                "expected '${name}' to be ${expectedText}, found path: " .. paths[1]
            )
          ''
        ) pluginStubs.pluginNames}
      '';
      assertions = [
        # plugin-pack and 'standalonePlugins'
        (expectNPlugins config "start" (builtins.length standalonePlugins + 1))
      ];
    }
  )
