import { createMockLogger } from "../../../__mocks__/loggerMock";
import type { DatabaseItem } from "../../../../src/databases/local-databases";
import { DatabaseKind } from "../../../../src/databases/local-databases";
import { file } from "tmp-promise";
import { QueryResultType } from "../../../../src/query-server/messages";
import {
  generateCandidateFilterPack,
  runAutoModelQueries,
} from "../../../../src/model-editor/auto-model-codeml-queries";
import { Mode } from "../../../../src/model-editor/shared/mode";
import { mockedObject, mockedUri } from "../../utils/mocking.helpers";
import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import type { QueryRunner } from "../../../../src/query-server";
import * as queryResolver from "../../../../src/local-queries/query-resolver";
import type { MethodSignature } from "../../../../src/model-editor/method";
import { EndpointType } from "../../../../src/model-editor/method";
import { join } from "path";
import { pathExists, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { CancellationTokenSource } from "vscode-jsonrpc";
import { QueryOutputDir } from "../../../../src/local-queries/query-output-dir";
import type { ModelExtensionFile } from "../../../../src/model-editor/model-extension-file";

describe("runAutoModelQueries", () => {
  let resolveQueriesSpy: jest.SpiedFunction<
    typeof queryResolver.resolveQueries
  >;
  beforeEach(() => {
    resolveQueriesSpy = jest
      .spyOn(queryResolver, "resolveQueries")
      .mockImplementation(
        async (_cliServer, _packsToSearch, _name, constraints) => {
          if (constraints["tags contain all"]?.includes("candidates")) {
            return ["/a/b/c/ql/candidates.ql"];
          }
          if (constraints["tags contain all"]?.includes("positive")) {
            return ["/a/b/c/ql/positive-examples.ql"];
          }
          if (constraints["tags contain all"]?.includes("negative")) {
            return ["/a/b/c/ql/negative-examples.ql"];
          }

          return [];
        },
      );
  });

  it("should run the query and return the results", async () => {
    const queryStorageDir = (await file()).path;
    const outputDir = new QueryOutputDir(join(queryStorageDir, "1"));

    const options = {
      mode: Mode.Application,
      candidateMethods: [],
      cliServer: mockedObject<CodeQLCliServer>({
        resolveQlpacks: jest.fn().mockResolvedValue({
          "/a/b/c/my-extension-pack": {},
        }),
        resolveMetadata: jest.fn().mockResolvedValue({
          kind: "problem",
        }),
        interpretBqrsSarif: jest.fn().mockResolvedValue({
          version: "2.1.0",
          $schema: "http://json.schemastore.org/sarif-2.1.0-rtm.4",
          runs: [
            {
              tool: {
                driver: {
                  name: "CodeQL",
                },
              },
              results: [
                {
                  message: {
                    text: "msg",
                  },
                  locations: [
                    {
                      physicalLocation: {
                        contextRegion: {
                          startLine: 10,
                          endLine: 12,
                          snippet: {
                            text: "Foo",
                          },
                        },
                        region: {
                          startLine: 10,
                          startColumn: 1,
                          endColumn: 3,
                        },
                        artifactLocation: {
                          uri: "foo.js",
                        },
                      },
                    },
                  ],
                },
              ],
            },
          ],
        }),
      }),
      queryRunner: mockedObject<QueryRunner>({
        createQueryRun: jest.fn().mockReturnValue({
          evaluate: jest.fn().mockResolvedValue({
            resultType: QueryResultType.SUCCESS,
            outputDir,
          }),
          outputDir,
        }),
        logger: createMockLogger(),
      }),
      databaseItem: mockedObject<DatabaseItem>({
        databaseUri: mockedUri("/a/b/c/src.zip"),
        contents: {
          kind: DatabaseKind.Database,
          name: "foo",
          datasetUri: mockedUri(),
        },
        language: "java",
        getSourceLocationPrefix: jest
          .fn()
          .mockResolvedValue("/home/runner/work/my-repo/my-repo"),
        sourceArchive: mockedUri("/a/b/c/src.zip"),
      }),
      queryStorageDir: "/tmp/queries",
      progress: jest.fn(),
      cancellationTokenSource: new CancellationTokenSource(),
    };

    const result = await runAutoModelQueries(options);
    expect(result).not.toBeUndefined();

    expect(options.cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
    expect(options.cliServer.resolveQlpacks).toHaveBeenCalledWith(
      expect.arrayContaining([expect.stringContaining("tmp")]),
      true,
    );
    expect(resolveQueriesSpy).toHaveBeenCalledTimes(1);
    expect(resolveQueriesSpy).toHaveBeenCalledWith(
      options.cliServer,
      ["codeql/java-automodel-queries"],
      "Extract automodel candidates",
      {
        kind: "problem",
        "tags contain all": ["automodel", "application-mode", "candidates"],
      },
    );
    expect(options.queryRunner.createQueryRun).toHaveBeenCalledTimes(1);
    expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith(
      "/a/b/c/src.zip",
      {
        queryPath: "/a/b/c/ql/candidates.ql",
        quickEvalPosition: undefined,
        quickEvalCountOnly: false,
      },
      false,
      expect.arrayContaining([expect.stringContaining("tmp")]),
      ["/a/b/c/my-extension-pack"],
      {},
      "/tmp/queries",
      undefined,
      undefined,
    );
  });
});

describe("generateCandidateFilterPack", () => {
  it("should create a temp pack containing the candidate filters", async () => {
    const candidateMethods: MethodSignature[] = [
      {
        signature: "org.my.A#x()",
        endpointType: EndpointType.Method,
        packageName: "org.my",
        typeName: "A",
        methodName: "x",
        methodParameters: "()",
      },
    ];
    const { packDir, cleanup } = await generateCandidateFilterPack(
      "java",
      candidateMethods,
    );
    expect(packDir).not.toBeUndefined();
    const qlpackFile = join(packDir, "codeql-pack.yml");
    expect(await pathExists(qlpackFile)).toBe(true);
    const filterFile = join(packDir, "filter.yml");
    expect(await pathExists(filterFile)).toBe(true);
    // Read the contents of filterFile and parse as yaml
    const yaml = (await loadYaml(
      await readFile(filterFile, "utf8"),
    )) as ModelExtensionFile;
    const extensions = yaml.extensions;
    expect(extensions).toBeInstanceOf(Array);
    expect(extensions).toHaveLength(1);
    const extension = extensions[0];
    expect(extension.addsTo.pack).toEqual("codeql/java-automodel-queries");
    expect(extension.addsTo.extensible).toEqual("automodelCandidateFilter");
    expect(extension.data).toBeInstanceOf(Array);
    expect(extension.data).toHaveLength(1);
    expect(extension.data[0]).toEqual(["org.my", "A", "x", "()"]);

    await cleanup();
    expect(await pathExists(packDir)).toBe(false);
  });
});
