import type { DirResult } from "tmp";
import { dirSync } from "tmp";
import { ensureDir, ensureFile, pathExists, writeFile } from "fs-extra";
import { join } from "path";
import type { ExtensionContext } from "vscode";
import { Uri, workspace } from "vscode";

import type {
  DatabaseContentsWithDbScheme,
  DatabaseItemImpl,
  FullDatabaseOptions,
} from "../../../../src/databases/local-databases";
import {
  DatabaseEventKind,
  DatabaseManager,
  DatabaseResolver,
} from "../../../../src/databases/local-databases";
import type { Logger } from "../../../../src/common/logging";
import type { CodeQLCliServer, DbInfo } from "../../../../src/codeql-cli/cli";
import {
  encodeArchiveBasePath,
  encodeSourceArchiveUri,
} from "../../../../src/common/vscode/archive-filesystem-provider";
import { testDisposeHandler } from "../../test-dispose-handler";
import type { QueryRunner } from "../../../../src/query-server/query-runner";
import * as dialog from "../../../../src/common/vscode/dialog";
import * as config from "../../../../src/config";
import { QlPackGenerator } from "../../../../src/local-queries/qlpack-generator";
import { mockedObject } from "../../utils/mocking.helpers";
import { createMockApp } from "../../../__mocks__/appMock";
import {
  createMockDB,
  dbLocationUri,
  mockDbOptions,
  sourceLocationUri,
} from "../../../factories/databases/databases";
import { findSourceArchive } from "../../../../src/databases/local-databases/database-resolver";
import { LanguageContextStore } from "../../../../src/language-context-store";

describe("local databases", () => {
  let databaseManager: DatabaseManager;
  let secondQueryRunner: QueryRunner;
  let extensionContext: ExtensionContext;

  let updateSpy: jest.Mock<Promise<void>, []>;
  let registerSpy: jest.Mock<Promise<void>, []>;
  let deregisterSpy: jest.Mock<Promise<void>, []>;
  let registerSpy2: jest.Mock<Promise<void>, []>;
  let deregisterSpy2: jest.Mock<Promise<void>, []>;
  let resolveDatabaseSpy: jest.Mock<Promise<DbInfo>, []>;
  let packAddSpy: jest.Mock<any, []>;
  let logSpy: jest.Mock<any, []>;

  let showNeverAskAgainDialogSpy: jest.SpiedFunction<
    typeof dialog.showNeverAskAgainDialog
  >;

  let dir: DirResult;
  let extensionContextStoragePath: string;

  beforeEach(() => {
    dir = dirSync({
      unsafeCleanup: true,
    });

    updateSpy = jest.fn(() => Promise.resolve(undefined));
    registerSpy = jest.fn(() => Promise.resolve(undefined));
    deregisterSpy = jest.fn(() => Promise.resolve(undefined));
    registerSpy2 = jest.fn(() => Promise.resolve(undefined));
    deregisterSpy2 = jest.fn(() => Promise.resolve(undefined));
    resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo));
    packAddSpy = jest.fn();
    logSpy = jest.fn(() => {
      /* */
    });

    showNeverAskAgainDialogSpy = jest
      .spyOn(dialog, "showNeverAskAgainDialog")
      .mockResolvedValue("Yes");

    extensionContextStoragePath = dir.name;

    extensionContext = mockedObject<ExtensionContext>(
      {
        workspaceState: {
          update: updateSpy,
          get: () => [],
        },
      },
      {
        dynamicProperties: {
          // pretend like databases added in the temp dir are controlled by the extension
          // so that they are deleted upon removal
          storageUri: () => Uri.file(extensionContextStoragePath),
        },
      },
    );

    const mockApp = createMockApp({});
    databaseManager = new DatabaseManager(
      extensionContext,
      mockApp,
      mockedObject<QueryRunner>({
        registerDatabase: registerSpy,
        deregisterDatabase: deregisterSpy,
        onStart: () => {
          /**/
        },
        onQueryRunStarting: () => {
          /**/
        },
      }),
      mockedObject<CodeQLCliServer>({
        resolveDatabase: resolveDatabaseSpy,
        packAdd: packAddSpy,
      }),
      new LanguageContextStore(mockApp),
      mockedObject<Logger>({
        log: logSpy,
      }),
    );

    secondQueryRunner = mockedObject<QueryRunner>({
      registerDatabase: registerSpy2,
      deregisterDatabase: deregisterSpy2,
      onStart: () => {
        /**/
      },
      onQueryRunStarting: () => {
        /**/
      },
    });

    // Unfortunately, during a test it is not possible to convert from
    // a single root workspace to multi-root, so must stub out relevant
    // functions
    jest.spyOn(workspace, "updateWorkspaceFolders").mockReturnValue(true);
  });

  afterEach(async () => {
    dir.removeCallback();
    databaseManager.dispose(testDisposeHandler);
  });

  it("should fire events when adding and removing a db item", async () => {
    const mockDbItem = createMockDB(dir);
    const onDidChangeDatabaseItem = jest.fn();
    databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
    await (databaseManager as any).addDatabaseItem(mockDbItem);

    expect((databaseManager as any)._databaseItems).toEqual([mockDbItem]);
    expect(updateSpy).toHaveBeenCalledWith("databaseList", [
      {
        options: mockDbOptions(),
        uri: dbLocationUri(dir).toString(true),
      },
    ]);
    expect(onDidChangeDatabaseItem).toHaveBeenCalledWith({
      fullRefresh: true,
      item: mockDbItem,
      kind: DatabaseEventKind.Add,
    });

    updateSpy.mockClear();
    onDidChangeDatabaseItem.mockClear();

    // now remove the item
    await databaseManager.removeDatabaseItem(mockDbItem);
    expect((databaseManager as any)._databaseItems).toEqual([]);
    expect(updateSpy).toHaveBeenCalledWith("databaseList", []);
    expect(onDidChangeDatabaseItem).toHaveBeenCalledWith({
      fullRefresh: true,
      item: mockDbItem,
      kind: DatabaseEventKind.Remove,
    });
  });

  describe("renameDatabaseItem", () => {
    it("should rename a db item and emit an event", async () => {
      const mockDbItem = createMockDB(dir);
      const onDidChangeDatabaseItem = jest.fn();
      databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
      await (databaseManager as any).addDatabaseItem(mockDbItem);

      await databaseManager.renameDatabaseItem(mockDbItem, "new name");

      expect(mockDbItem.name).toBe("new name");
      expect(updateSpy).toHaveBeenCalledWith("databaseList", [
        {
          options: { ...mockDbOptions(), displayName: "new name" },
          uri: dbLocationUri(dir).toString(true),
        },
      ]);

      expect(onDidChangeDatabaseItem).toHaveBeenCalledWith({
        fullRefresh: true,
        item: mockDbItem,
        kind: DatabaseEventKind.Rename,
      });
    });
  });

  describe("add / remove database items", () => {
    it("should add a database item", async () => {
      const onDidChangeDatabaseItem = jest.fn();
      databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
      const mockDbItem = createMockDB(dir);

      await (databaseManager as any).addDatabaseItem(mockDbItem);

      expect(databaseManager.databaseItems).toEqual([mockDbItem]);
      expect(updateSpy).toHaveBeenCalledWith("databaseList", [
        {
          uri: dbLocationUri(dir).toString(true),
          options: mockDbOptions(),
        },
      ]);

      const mockEvent = {
        fullRefresh: true,
        item: mockDbItem,
        kind: DatabaseEventKind.Add,
      };
      expect(onDidChangeDatabaseItem).toHaveBeenCalledWith(mockEvent);
    });

    it("should add a database item source archive", async () => {
      const mockDbItem = createMockDB(dir);
      mockDbItem.name = "xxx";
      await databaseManager.addDatabaseSourceArchiveFolder(mockDbItem);

      // workspace folders should be updated. We can only check the mocks since
      // when running as a test, we are not allowed to update the workspace folders
      expect(workspace.updateWorkspaceFolders).toHaveBeenCalledWith(1, 0, {
        name: "[xxx source archive]",
        // must use a matcher here since vscode URIs with the same path
        // are not always equal due to internal state.
        uri: expect.objectContaining({
          fsPath: encodeArchiveBasePath(sourceLocationUri(dir).fsPath).fsPath,
        }),
      });
    });

    it("should remove a database item", async () => {
      const mockDbItem = createMockDB(dir);
      await ensureDir(mockDbItem.databaseUri.fsPath);

      // pretend that this item is the first workspace folder in the list
      jest
        .spyOn(mockDbItem, "belongsToSourceArchiveExplorerUri")
        .mockReturnValue(true);

      await (databaseManager as any).addDatabaseItem(mockDbItem);

      updateSpy.mockClear();

      await databaseManager.removeDatabaseItem(mockDbItem);

      expect(databaseManager.databaseItems).toEqual([]);
      expect(updateSpy).toHaveBeenCalledWith("databaseList", []);
      // should remove the folder
      expect(workspace.updateWorkspaceFolders).toHaveBeenCalledWith(0, 1);

      // should also delete the db contents
      await expect(pathExists(mockDbItem.databaseUri.fsPath)).resolves.toBe(
        false,
      );
      await expect(pathExists(dir.name)).resolves.toBe(true);
    });

    it("should remove a database item with an extension managed location", async () => {
      const dbLocation = join(dir.name, "org-repo-12");
      await ensureDir(dbLocation);

      const mockDbItem = createMockDB(dbLocation, {
        ...mockDbOptions(),
        extensionManagedLocation: dbLocation,
      });
      await ensureDir(mockDbItem.databaseUri.fsPath);

      // pretend that this item is the first workspace folder in the list
      jest
        .spyOn(mockDbItem, "belongsToSourceArchiveExplorerUri")
        .mockReturnValue(true);

      await (databaseManager as any).addDatabaseItem(mockDbItem);

      updateSpy.mockClear();

      await databaseManager.removeDatabaseItem(mockDbItem);

      expect(databaseManager.databaseItems).toEqual([]);
      expect(updateSpy).toHaveBeenCalledWith("databaseList", []);
      // should remove the folder
      expect(workspace.updateWorkspaceFolders).toHaveBeenCalledWith(0, 1);

      // should delete the complete extension managed location
      await expect(pathExists(dbLocation)).resolves.toBe(false);
    });

    it("should remove a database item outside of the extension controlled area", async () => {
      const mockDbItem = createMockDB(dir);
      await ensureDir(mockDbItem.databaseUri.fsPath);

      // pretend that this item is the first workspace folder in the list
      jest
        .spyOn(mockDbItem, "belongsToSourceArchiveExplorerUri")
        .mockReturnValue(true);
      await (databaseManager as any).addDatabaseItem(mockDbItem);
      updateSpy.mockClear();

      // pretend that the database location is not controlled by the extension
      (databaseManager as any).ctx.storageUri = Uri.file("hucairz");
      extensionContextStoragePath = "hucairz";

      await databaseManager.removeDatabaseItem(mockDbItem);

      expect(databaseManager.databaseItems).toEqual([]);
      expect(updateSpy).toHaveBeenCalledWith("databaseList", []);
      // should remove the folder
      expect(workspace.updateWorkspaceFolders).toHaveBeenCalledWith(0, 1);

      // should NOT delete the db contents
      await expect(pathExists(mockDbItem.databaseUri.fsPath)).resolves.toBe(
        true,
      );
    });

    it("should register and deregister a database when adding and removing it", async () => {
      // similar test as above, but also check the call to sendRequestSpy to make sure they send the
      // registration messages.
      const mockDbItem = createMockDB(dir);

      await (databaseManager as any).addDatabaseItem(mockDbItem);
      // Should have registered this database
      expect(registerSpy).toHaveBeenCalledWith(mockDbItem);

      await databaseManager.removeDatabaseItem(mockDbItem);

      // Should have deregistered this database
      expect(deregisterSpy).toHaveBeenCalledWith(mockDbItem);
    });

    it("Should move database to secondary query server and back", async () => {
      // similar test as above, but also check the call to sendRequestSpy to make sure they send the
      // registration messages.
      const mockDbItem = createMockDB(dir);

      await (databaseManager as any).addDatabaseItem(mockDbItem);

      await databaseManager.setCurrentDatabaseItem(mockDbItem, true);
      // Should have registered this database
      expect(registerSpy).toHaveBeenCalledWith(mockDbItem);

      await databaseManager.runWithDatabaseInSeparateQueryRunner(
        secondQueryRunner,
        async () => {
          // Should have moved database registration
          expect(deregisterSpy).toHaveBeenCalledWith(mockDbItem);
          expect(registerSpy2).toHaveBeenCalledWith(mockDbItem);
        },
      );

      // Should have moved database registration back
      expect(deregisterSpy2).toHaveBeenCalledWith(mockDbItem);
      expect(registerSpy).toHaveBeenCalledWith(mockDbItem);

      await databaseManager.removeDatabaseItem(mockDbItem);

      // Should have deregistered this database
      expect(deregisterSpy).toHaveBeenCalledWith(mockDbItem);
    });
  });

  describe("resolveSourceFile", () => {
    it("should fail to resolve when not a uri", () => {
      const db = createMockDB(
        dir,
        mockDbOptions(),
        Uri.parse("file:/sourceArchive-uri/"),
      );
      (db as any).contents.sourceArchiveUri = undefined;
      expect(() => db.resolveSourceFile("abc")).toThrow("Scheme is missing");
    });

    it("should fail to resolve when not a file uri", () => {
      const db = createMockDB(
        dir,
        mockDbOptions(),
        Uri.parse("file:/sourceArchive-uri/"),
      );
      (db as any).contents.sourceArchiveUri = undefined;
      expect(() => db.resolveSourceFile("http://abc")).toThrow(
        "Invalid uri scheme",
      );
    });

    describe("no source archive", () => {
      it("should resolve undefined", () => {
        const db = createMockDB(
          dir,
          mockDbOptions(),
          Uri.parse("file:/sourceArchive-uri/"),
        );
        (db as any).contents.sourceArchiveUri = undefined;
        const resolved = db.resolveSourceFile(undefined);
        expect(resolved.toString(true)).toBe(dbLocationUri(dir).toString(true));
      });

      it("should resolve an empty file", () => {
        const db = createMockDB(
          dir,
          mockDbOptions(),
          Uri.parse("file:/sourceArchive-uri/"),
        );
        (db as any).contents.sourceArchiveUri = undefined;
        const resolved = db.resolveSourceFile("file:");
        expect(resolved.toString()).toBe("file:///");
      });
    });

    describe("zipped source archive", () => {
      it("should encode a source archive url", () => {
        const db = createMockDB(
          dir,
          mockDbOptions(),
          encodeSourceArchiveUri({
            sourceArchiveZipPath: "sourceArchive-uri",
            pathWithinSourceArchive: "def",
          }),
        );
        const resolved = db.resolveSourceFile(Uri.file("abc").toString());

        // must recreate an encoded archive uri instead of typing out the
        // text since the uris will be different on windows and ubuntu.
        expect(resolved.toString()).toBe(
          encodeSourceArchiveUri({
            sourceArchiveZipPath: "sourceArchive-uri",
            pathWithinSourceArchive: "def/abc",
          }).toString(),
        );
      });

      it("should encode a source archive url with trailing slash", () => {
        const db = createMockDB(
          dir,
          mockDbOptions(),
          encodeSourceArchiveUri({
            sourceArchiveZipPath: "sourceArchive-uri",
            pathWithinSourceArchive: "def/",
          }),
        );
        const resolved = db.resolveSourceFile(Uri.file("abc").toString());

        // must recreate an encoded archive uri instead of typing out the
        // text since the uris will be different on windows and ubuntu.
        expect(resolved.toString()).toBe(
          encodeSourceArchiveUri({
            sourceArchiveZipPath: "sourceArchive-uri",
            pathWithinSourceArchive: "def/abc",
          }).toString(),
        );
      });

      it("should encode an empty source archive url", () => {
        const db = createMockDB(
          dir,
          mockDbOptions(),
          encodeSourceArchiveUri({
            sourceArchiveZipPath: "sourceArchive-uri",
            pathWithinSourceArchive: "def",
          }),
        );
        const resolved = db.resolveSourceFile("file:");
        expect(resolved.toString()).toBe(
          "codeql-zip-archive://1-18/sourceArchive-uri/def/",
        );
      });
    });

    it("should handle an empty file", () => {
      const db = createMockDB(
        dir,
        mockDbOptions(),
        Uri.parse("file:/sourceArchive-uri/"),
      );
      const resolved = db.resolveSourceFile("");
      expect(resolved.toString()).toBe("file:///sourceArchive-uri/");
    });
  });

  describe("getPrimaryLanguage", () => {
    it("should get the primary language", async () => {
      resolveDatabaseSpy.mockResolvedValue({
        languages: ["python"],
      } as unknown as DbInfo);
      const result = await (databaseManager as any).getPrimaryLanguage(
        "hucairz",
      );
      expect(result).toBe("python");
    });

    it("should handle missing the primary language", async () => {
      resolveDatabaseSpy.mockResolvedValue({
        languages: [],
      } as unknown as DbInfo);
      const result = await (databaseManager as any).getPrimaryLanguage(
        "hucairz",
      );
      expect(result).toBe("");
    });
  });

  describe("isAffectedByTest", () => {
    let directoryPath: string;
    let projectPath: string;
    let qlFilePath: string;

    beforeEach(async () => {
      directoryPath = join(dir.name, "dir");
      await ensureDir(directoryPath);
      projectPath = join(directoryPath, "dir.testproj");
      await writeFile(projectPath, "");
      qlFilePath = join(directoryPath, "test.ql");
      await writeFile(qlFilePath, "");
    });

    it("should return true for testproj database in test directory", async () => {
      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(projectPath),
      );
      expect(await db.isAffectedByTest(directoryPath)).toBe(true);
    });

    it("should return false for non-existent test directory", async () => {
      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(join(dir.name, "non-existent/non-existent.testproj")),
      );
      expect(await db.isAffectedByTest(join(dir.name, "non-existent"))).toBe(
        false,
      );
    });

    it("should return false for non-testproj database in test directory", async () => {
      const anotherProjectPath = join(directoryPath, "dir.proj");
      await writeFile(anotherProjectPath, "");

      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(anotherProjectPath),
      );
      expect(await db.isAffectedByTest(directoryPath)).toBe(false);
    });

    it("should return false for testproj database outside test directory", async () => {
      const anotherProjectDir = join(dir.name, "other");
      await ensureDir(anotherProjectDir);
      const anotherProjectPath = join(anotherProjectDir, "other.testproj");
      await writeFile(anotherProjectPath, "");

      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(anotherProjectPath),
      );
      expect(await db.isAffectedByTest(directoryPath)).toBe(false);
    });

    it("should return false for testproj database for prefix directory", async () => {
      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(projectPath),
      );
      // /d is a prefix of /dir/dir.testproj, but
      // /dir/dir.testproj is not under /d
      expect(await db.isAffectedByTest(join(directoryPath, "d"))).toBe(false);
    });

    it("should return true for testproj database for test file", async () => {
      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(projectPath),
      );
      expect(await db.isAffectedByTest(qlFilePath)).toBe(true);
    });

    it("should return false for non-existent test file", async () => {
      const otherTestFile = join(directoryPath, "other-test.ql");
      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(projectPath),
      );
      expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
    });

    it("should return false for non-testproj database for test file", async () => {
      const anotherProjectPath = join(directoryPath, "dir.proj");
      await writeFile(anotherProjectPath, "");

      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(anotherProjectPath),
      );
      expect(await db.isAffectedByTest(qlFilePath)).toBe(false);
    });

    it("should return false for testproj database not matching test file", async () => {
      const otherTestFile = join(dir.name, "test.ql");
      await writeFile(otherTestFile, "");

      const db = createMockDB(
        dir,
        mockDbOptions(),
        sourceLocationUri(dir),
        Uri.file(projectPath),
      );
      expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
    });
  });

  describe("findSourceArchive", () => {
    ["src", "output/src_archive"].forEach((name) => {
      it(`should find source folder in ${name}`, async () => {
        const uri = Uri.file(join(dir.name, name));
        await ensureFile(join(uri.fsPath, "hucairz.txt"));
        const srcUri = await findSourceArchive(dir.name);
        expect(srcUri!.fsPath).toBe(uri.fsPath);
      });

      it(`should find source archive in ${name}.zip`, async () => {
        const uri = Uri.file(join(dir.name, `${name}.zip`));
        await ensureFile(uri.fsPath);
        const srcUri = await findSourceArchive(dir.name);
        expect(srcUri!.fsPath).toBe(uri.fsPath);
      });

      it(`should prioritize ${name}.zip over ${name}`, async () => {
        const uri = Uri.file(join(dir.name, `${name}.zip`));
        await ensureFile(uri.fsPath);

        const uriFolder = Uri.file(join(dir.name, name));
        await ensureFile(join(uriFolder.fsPath, "hucairz.txt"));

        const srcUri = await findSourceArchive(dir.name);
        expect(srcUri!.fsPath).toBe(uri.fsPath);
      });
    });

    it("should prioritize src over output/src_archive", async () => {
      const uriSrc = Uri.file(join(dir.name, "src.zip"));
      await ensureFile(uriSrc.fsPath);
      const uriSrcArchive = Uri.file(join(dir.name, "src.zip"));
      await ensureFile(uriSrcArchive.fsPath);

      const resultUri = await findSourceArchive(dir.name);
      expect(resultUri!.fsPath).toBe(uriSrc.fsPath);
    });
  });

  describe("createSkeletonPacks", () => {
    let mockDbItem: DatabaseItemImpl;
    let language: string;
    let generateSpy: jest.SpyInstance;

    beforeEach(() => {
      language = "ruby";

      const options: FullDatabaseOptions = {
        dateAdded: 123,
        language,
        origin: {
          type: "folder",
        },
        extensionManagedLocation: undefined,
      };
      mockDbItem = createMockDB(dir, options);

      generateSpy = jest
        .spyOn(QlPackGenerator.prototype, "generate")
        .mockImplementation(() => Promise.resolve());
    });

    describe("when the language is set", () => {
      it("should offer the user to set up a skeleton QL pack", async () => {
        await (databaseManager as any).createSkeletonPacks(mockDbItem);

        expect(showNeverAskAgainDialogSpy).toHaveBeenCalledTimes(1);
      });

      it("should return early if the user refuses help", async () => {
        showNeverAskAgainDialogSpy = jest
          .spyOn(dialog, "showNeverAskAgainDialog")
          .mockResolvedValue("No");

        await (databaseManager as any).createSkeletonPacks(mockDbItem);

        expect(generateSpy).not.toHaveBeenCalled();
      });

      it("should return early if the user escapes out of the dialog", async () => {
        showNeverAskAgainDialogSpy = jest
          .spyOn(dialog, "showNeverAskAgainDialog")
          .mockResolvedValue(undefined);

        await (databaseManager as any).createSkeletonPacks(mockDbItem);

        expect(generateSpy).not.toHaveBeenCalled();
      });

      it("should return early and write choice to settings if user wants to never be asked again", async () => {
        showNeverAskAgainDialogSpy = jest
          .spyOn(dialog, "showNeverAskAgainDialog")
          .mockResolvedValue("No, and never ask me again");
        const setAutogenerateQlPacksSpy = jest.spyOn(
          config,
          "setAutogenerateQlPacks",
        );

        await (databaseManager as any).createSkeletonPacks(mockDbItem);

        expect(generateSpy).not.toHaveBeenCalled();
        expect(setAutogenerateQlPacksSpy).toHaveBeenCalledWith("never");
      });

      it("should create the skeleton QL pack for the user", async () => {
        await (databaseManager as any).createSkeletonPacks(mockDbItem);

        expect(generateSpy).toHaveBeenCalled();
      });
    });

    describe("when the language is not set", () => {
      it("should fail gracefully", async () => {
        mockDbItem = createMockDB(dir);
        await (databaseManager as any).createSkeletonPacks(mockDbItem);
        expect(logSpy).toHaveBeenCalledWith(
          "Could not create skeleton QL pack because the selected database's language is not set.",
        );
      });
    });

    describe("when the databaseItem is not set", () => {
      it("should fail gracefully", async () => {
        await (databaseManager as any).createSkeletonPacks(undefined);
        expect(logSpy).toHaveBeenCalledWith(
          "Could not create QL pack because no database is selected. Please add a database.",
        );
      });
    });

    describe("when the QL pack already exists", () => {
      beforeEach(async () => {
        await ensureDir(join(dir.name, `codeql-custom-queries-${language}`));
      });

      it("should exit early", async () => {
        showNeverAskAgainDialogSpy = jest
          .spyOn(dialog, "showNeverAskAgainDialog")
          .mockResolvedValue("No");

        await (databaseManager as any).createSkeletonPacks(mockDbItem);

        expect(generateSpy).not.toHaveBeenCalled();
      });
    });
  });

  describe("openDatabase", () => {
    let createSkeletonPacksSpy: jest.SpyInstance;
    let resolveDatabaseContentsSpy: jest.SpyInstance;
    let setCurrentDatabaseItemSpy: jest.SpyInstance;
    let addDatabaseSourceArchiveFolderSpy: jest.SpyInstance;
    let mockDbItem: DatabaseItemImpl;

    beforeEach(() => {
      createSkeletonPacksSpy = jest
        .spyOn(databaseManager, "createSkeletonPacks")
        .mockImplementation(async () => {
          /* no-op */
        });

      resolveDatabaseContentsSpy = jest
        .spyOn(DatabaseResolver, "resolveDatabaseContents")
        .mockResolvedValue({} as DatabaseContentsWithDbScheme);

      setCurrentDatabaseItemSpy = jest.spyOn(
        databaseManager,
        "setCurrentDatabaseItem",
      );

      addDatabaseSourceArchiveFolderSpy = jest.spyOn(
        databaseManager,
        "addDatabaseSourceArchiveFolder",
      );

      jest.mock("fs", () => ({
        promises: {
          pathExists: jest.fn().mockResolvedValue(true),
        },
      }));

      mockDbItem = createMockDB(dir);
    });

    it("should resolve the database contents", async () => {
      await databaseManager.openDatabase(
        mockDbItem.databaseUri,
        mockDbItem.origin,
      );

      expect(resolveDatabaseContentsSpy).toHaveBeenCalledTimes(2);
    });

    it("should set the database as the currently selected one", async () => {
      await databaseManager.openDatabase(
        mockDbItem.databaseUri,
        mockDbItem.origin,
      );

      expect(setCurrentDatabaseItemSpy).toHaveBeenCalledTimes(1);
    });

    it("should not add database source archive folder when `codeQL.addingDatabases.addDatabaseSourceToWorkspace` is `false`", async () => {
      jest.spyOn(config, "addDatabaseSourceToWorkspace").mockReturnValue(false);

      await databaseManager.openDatabase(
        mockDbItem.databaseUri,
        mockDbItem.origin,
      );

      expect(addDatabaseSourceArchiveFolderSpy).toHaveBeenCalledTimes(0);
    });

    it("should add database source archive folder when `codeQL.addingDatabases.addDatabaseSourceToWorkspace` is `true`", async () => {
      jest.spyOn(config, "addDatabaseSourceToWorkspace").mockReturnValue(true);

      await databaseManager.openDatabase(
        mockDbItem.databaseUri,
        mockDbItem.origin,
      );

      expect(addDatabaseSourceArchiveFolderSpy).toHaveBeenCalledTimes(1);
    });

    describe("when codeQL.codespacesTemplate is set to true", () => {
      describe("when we add the tutorial database to the codespace", () => {
        it("should not offer to create a skeleton QL pack", async () => {
          jest.spyOn(config, "isCodespacesTemplate").mockReturnValue(true);

          const isTutorialDatabase = true;
          const makeSelected = true;
          const nameOverride = "CodeQL Tutorial Database";

          await databaseManager.openDatabase(
            mockDbItem.databaseUri,
            mockDbItem.origin,
            makeSelected,
            nameOverride,
            { isTutorialDatabase },
          );

          expect(createSkeletonPacksSpy).toHaveBeenCalledTimes(0);
        });
      });

      describe("when we add a new database that isn't the tutorial one", () => {
        it("should create a skeleton QL pack", async () => {
          jest.spyOn(config, "isCodespacesTemplate").mockReturnValue(true);

          await databaseManager.openDatabase(
            mockDbItem.databaseUri,
            mockDbItem.origin,
          );

          expect(createSkeletonPacksSpy).toHaveBeenCalledTimes(1);
        });
      });
    });

    describe("when codeQL.codespacesTemplate is set to false or not defined", () => {
      it("should not create a skeleton QL pack", async () => {
        jest.spyOn(config, "isCodespacesTemplate").mockReturnValue(false);

        await databaseManager.openDatabase(
          mockDbItem.databaseUri,
          mockDbItem.origin,
        );
        expect(createSkeletonPacksSpy).toHaveBeenCalledTimes(0);
      });
    });
  });
});
