import {MarkupContent, TextEdit} from "vscode-languageserver-types";
import {complete} from "./complete";
import {WorkflowContext} from "./context/workflow-context";
import {registerLogger} from "./log";
import {getPositionFromCursor} from "./test-utils/cursor-position";
import {TestLogger} from "./test-utils/logger";
import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";

registerLogger(new TestLogger());

describe("completion", () => {
  it("runs-on", async () => {
    const input = "on: push\njobs:\n  build:\n    runs-on: |";
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(12);
    const labels = result.map(x => x.label);
    expect(labels).toContain("macos-latest");
  });

  it("needs", async () => {
    const input = `on: push
jobs:
  build:
      runs-on: ubuntu-latest
  build2:
      runs-on: ubuntu-latest
      needs: bu|`;
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(1);
    expect(result[0].label).toEqual("build");
  });

  it("empty workflow", async () => {
    const input = "|";
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(8);
    expect(result[0].label).toEqual("concurrency");
  });

  it("completion within a sequence", async () => {
    const input = `on: push
jobs:
  build:
    runs-on: [ubuntu-latest, u|]`;
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(11);

    const labels = result.map(x => x.label);
    expect(labels).toContain("macos-latest");
    expect(labels).not.toContain("ubuntu-latest");
  });

  it("one-of definition completion", async () => {
    const input = `on: push
jobs:
  build:
    |`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(20);
  });

  it("string definition completion in sequence", async () => {
    const input = `on:
  release:
    types:
      - |`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result.map(x => x.label)).toEqual([
      "created",
      "deleted",
      "edited",
      "prereleased",
      "published",
      "released",
      "unpublished"
    ]);
  });

  it("string definition completion", async () => {
    const input = `on:
  release:
    types: |`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result.map(x => x.label)).toEqual([
      "created",
      "deleted",
      "edited",
      "prereleased",
      "published",
      "released",
      "unpublished"
    ]);
  });

  it("map keys filter out existing values", async () => {
    const input = `on: push
jobs:
  build:
    runs-on: ubuntu-latest
    |`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result.map(x => x.label)).not.toContain("runs-on");
  });

  it("one-of narrows down to a specific type", async () => {
    // A job could be a job-factory or a workflow-job (callable workflow with uses)
    // If we have `runs-on`, we should be able to identify that we're in a job-factory
    const jobFactory = `on: push
jobs:
  build:
    runs-on: ubuntu-latest
    |`;
    const jobFactoryResult = await complete(...getPositionFromCursor(jobFactory));
    expect(jobFactoryResult).not.toBeUndefined();
    expect(jobFactoryResult.map(x => x.label)).not.toContain("uses");

    const workflowJob = `on: push
    jobs:
      build:
        uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
        |`;
    const workflowJobResult = await complete(...getPositionFromCursor(workflowJob));
    expect(workflowJobResult).not.toBeUndefined();
    expect(workflowJobResult.map(x => x.label)).not.toContain("runs-on");
  });

  it("completes boolean values", async () => {
    const input = `on: push
jobs:
  build:
    continue-on-error: t|`;
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(2);

    expect(result.map(x => x.label).sort()).toEqual(["false", "true"]);
  });

  it("completes for empty map values", async () => {
    const input = `on: push
jobs:
  build:
    continue-on-error: |`;
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(2);

    expect(result.map(x => x.label).sort()).toEqual(["false", "true"]);
  });

  it("does not complete empty map values when cursor is immediately after the position", async () => {
    const input = `on: push
jobs:
  build:
    continue-on-error:|`;
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(0);
  });

  it("custom value providers override defaults", async () => {
    const input = "on: push\njobs:\n  build:\n    runs-on: |";

    const config: ValueProviderConfig = {
      "runs-on": {
        kind: ValueProviderKind.SuggestedValues,
        get: async (_: WorkflowContext) => {
          return [{label: "my-custom-label"}];
        }
      }
    };
    const result = await complete(...getPositionFromCursor(input), {valueProviderConfig: config});

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(1);
    expect(result[0].label).toEqual("my-custom-label");
  });

  it("custom value providers for sequences", async () => {
    const input = "on: push\njobs:\n  build:\n    runs-on: [m|]";

    const config: ValueProviderConfig = {
      "runs-on": {
        kind: ValueProviderKind.SuggestedValues,
        get: async (_: WorkflowContext) => {
          return [{label: "my-custom-label"}];
        }
      }
    };
    const result = await complete(...getPositionFromCursor(input), {valueProviderConfig: config});

    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(1);
    expect(result[0].label).toEqual("my-custom-label");
  });

  it("does not show parent mapping sibling keys", async () => {
    const input = `on: push
jobs:
  build:
    container: |
    runs-on: ubuntu-latest`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(6);
    // Should not contain other top-level job keys like `if` and `runs-on`
    expect(result.map(x => x.label)).not.toContain("if");
    expect(result.map(x => x.label)).not.toContain("runs-on");
  });

  it("shows mapping keys within a new map ", async () => {
    const input = `on: push
jobs:
  build:
    concurrency: |`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result.map(x => x.label).sort()).toEqual(["cancel-in-progress", "group"]);
  });

  it("job key", async () => {
    const input = `on: push
jobs:
  build:
    runs-|`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result).toHaveLength(20);
  });

  it("job key with comment afterwards", async () => {
    const input = `on: push
jobs:
  build:
    runs-|
  #`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result).toHaveLength(20);
  });

  it("job key with other values afterwards", async () => {
    const input = `on: push
jobs:
  build:
    runs-|

    concurrency: 'group-name'`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result).toHaveLength(19);
  });

  it("step key without space after colon", async () => {
    const input = `on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - env:|
      run: echo`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).toHaveLength(0);
  });

  it("empty step", async () => {
    const input = `on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - run: echo
    - |`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).toHaveLength(11);
    expect(result.map(x => x.label)).toEqual([
      "continue-on-error",
      "env",
      "id",
      "if",
      "name",
      "run",
      "shell",
      "timeout-minutes",
      "uses",
      "with",
      "working-directory"
    ]);

    // Includes detail when available. Using continue-on-error as a sample here.
    expect(result.map(x => (x.documentation as MarkupContent)?.value)).toContain(
      "Prevents a job from failing when a step fails. Set to `true` to allow a job to pass when this step fails."
    );
  });

  it("loose mapping keys have no completion suggestions", async () => {
    const input = `
on:
  workflow_dispatch:
    inputs:
      name:
        type: string
        description: "hello"
      |
`;
    const result = await complete(...getPositionFromCursor(input));
    expect(result).toHaveLength(0);
  });

  it("well known mapping keys have descriptions", async () => {
    const input = `
o|
`;
    const result = await complete(...getPositionFromCursor(input));
    const onResult = result.find(x => x.label === "on");
    expect(onResult).not.toBeUndefined();
    expect((onResult!.documentation as MarkupContent).value).toContain("The GitHub event that triggers the workflow.");
  });

  it("event list includes descriptions when available ", async () => {
    const input = `
    on: [check_run, |]`;
    const result = await complete(...getPositionFromCursor(input));
    const dispatchResult = result.find(x => x.label === "workflow_dispatch");
    expect(dispatchResult).not.toBeUndefined();
    expect((dispatchResult!.documentation as MarkupContent).value).toContain(
      "The `workflow_dispatch` event allows you to manually trigger a workflow run."
    );
  });

  it("sets range when completing token", async () => {
    const input = `on: push
jobs:
  pre-build:
    runs-on: ubuntu-latest
  build:
    runs-on: ubuntu-latest
    needs: pre-bu|`;

    const result = await complete(...getPositionFromCursor(input));
    expect(result).not.toBeUndefined();
    expect(result.length).toEqual(1);

    let textEdit = result[0].textEdit as TextEdit;
    expect(textEdit.newText).toEqual("pre-build");
    expect(textEdit.range).toEqual({
      start: {line: 6, character: 11},
      end: {line: 6, character: 17}
    });
  });

  it("sets a range for token key", async () => {
    const input = "on: push\njobs:\n  build:\n    runs-o|";
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.map(e => e.label)).toContain("runs-on");

    let textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit;
    expect(textEdit.newText).toEqual("runs-on");
    expect(textEdit.range).toEqual({
      start: {line: 3, character: 4},
      end: {line: 3, character: 10}
    });
  });

  it("sets a 0-length range while no initial token key", async () => {
    const input = "on: push\njobs:\n  build:\n    |";
    const result = await complete(...getPositionFromCursor(input));

    expect(result).not.toBeUndefined();
    expect(result.map(e => e.label)).toContain("runs-on");

    let textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit;
    expect(textEdit.newText).toEqual("runs-on");
    expect(textEdit.range).toEqual({
      start: {line: 3, character: 4},
      end: {line: 3, character: 4}
    });
  });

  describe("completes with indentation", () => {
    it("default indentation", async () => {
      const input = `on: push
jobs:
  build:
    step|`;
      const result = await complete(...getPositionFromCursor(input));

      // Sequence
      expect(result.filter(x => x.label === "steps").map(x => x.textEdit?.newText)).toEqual(["steps:\n  - "]);

      // Mapping
      expect(result.filter(x => x.label === "env").map(x => x.textEdit?.newText)).toEqual(["env:\n  "]);

      // Value
      expect(result.filter(x => x.label === "timeout-minutes").map(x => x.textEdit?.newText)).toEqual([
        "timeout-minutes: "
      ]);

      // One-of
      expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
    });

    it("custom indentation", async () => {
      // Use 3 spaces to indent
      const input = `on: push
jobs:
   build:
      step|`;
      const result = await complete(...getPositionFromCursor(input));

      // Sequence
      expect(result.filter(x => x.label === "steps").map(x => x.textEdit?.newText)).toEqual(["steps:\n   - "]);

      // Mapping
      expect(result.filter(x => x.label === "env").map(x => x.textEdit?.newText)).toEqual(["env:\n   "]);

      // Value
      expect(result.filter(x => x.label === "timeout-minutes").map(x => x.textEdit?.newText)).toEqual([
        "timeout-minutes: "
      ]);

      // One-of
      expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
    });
  });
});
