import { describe, expect, it } from "vitest";
import {
  applyShard,
  buildGraphDirName,
  buildRuntimeGraphs,
  buildTaskQueue,
  claimNextTask,
  collectSuites,
  resolveRuntimeConfigs,
} from "./planning.mjs";

function makeConfig(name, extras = {}) {
  const providedTestkit = extras.testkit || {};
  return {
    name,
    suites: extras.suites || {},
    testkit: {
      dependsOn: extras.dependsOn || providedTestkit.dependsOn || [],
      execution: providedTestkit.execution || {
        workers: 1,
        fileTimeoutSeconds: 60,
      },
      runtime: providedTestkit.runtime || {
        instances: 1,
        maxConcurrentTasks: Number.POSITIVE_INFINITY,
      },
      requirements: providedTestkit.requirements || {
        suites: [],
        files: [],
        fileLocksByPath: new Map(),
      },
      ...providedTestkit,
    },
    ...extras,
  };
}

describe("runner-planning", () => {
  it("orders runtime configs by dependency and detects cycles", () => {
    const api = makeConfig("api");
    const frontend = makeConfig("frontend", { dependsOn: ["api"] });
    const configMap = new Map([
      ["api", api],
      ["frontend", frontend],
    ]);

    expect(resolveRuntimeConfigs(frontend, configMap).map((config) => config.name)).toEqual([
      "api",
      "frontend",
    ]);

    api.testkit.dependsOn = ["frontend"];
    expect(() => resolveRuntimeConfigs(frontend, configMap)).toThrow("Dependency cycle");
  });

  it("collects suites with user-facing type selection", () => {
    const config = makeConfig("api", {
      suites: {
        integration: [
          { name: "health", files: ["tests/api/integration/health.int.testkit.ts"] },
        ],
        e2e: [
          {
            name: "auth",
            framework: "playwright",
            files: [
              "tests/frontend/e2e/auth.pw.testkit.ts",
              "tests/frontend/e2e/signup.pw.testkit.ts",
            ],
          },
        ],
      },
    });

    expect(collectSuites(config, ["int"], [], [])[0]).toMatchObject({
      name: "health",
      type: "integration",
      displayType: "int",
      framework: "k6",
      weight: 1,
    });

    expect(collectSuites(config, ["pw"], [], [])[0]).toMatchObject({
      name: "auth",
      displayType: "pw",
      framework: "playwright",
      weight: 2,
    });

    expect(
      collectSuites(
        config,
        ["all"],
        [],
        ["tests/frontend/e2e/signup.pw.testkit.ts"]
      )[0]
    ).toMatchObject({
      name: "auth",
      files: ["tests/frontend/e2e/signup.pw.testkit.ts"],
    });
  });

  it("keeps skipped files visible while removing them from runnable work", () => {
    const config = makeConfig("api", {
      suites: {
        integration: [
          {
            name: "billing",
            files: [
              "__testkit__/billing/a.int.testkit.ts",
              "__testkit__/billing/b.int.testkit.ts",
            ],
          },
        ],
      },
      testkit: {
        skip: {
          fileReasonByPath: new Map([
            ["__testkit__/billing/a.int.testkit.ts", "Billing is stubbed"],
          ]),
          suites: [],
        },
      },
    });

    expect(collectSuites(config, ["int"], [], [])).toEqual([
      expect.objectContaining({
        name: "billing",
        files: ["__testkit__/billing/b.int.testkit.ts"],
        skippedFiles: [
          {
            path: "__testkit__/billing/a.int.testkit.ts",
            reason: "Billing is stubbed",
          },
        ],
        totalFileCount: 2,
      }),
    ]);
  });

  it("applies lock requirements to matching suites and files", () => {
    const api = makeConfig("api", {
      suites: {
        integration: [
          {
            name: "routes",
            framework: "k6",
            files: [
              "src/api/routes/__testkit__/health.int.testkit.ts",
              "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
            ],
            orderIndex: 0,
            weight: 2,
          },
        ],
      },
      testkit: {
        runtime: {
          instances: 3,
          maxConcurrentTasks: 2,
        },
        requirements: {
          suites: [
            {
              selector: { kind: "typed", type: "int", name: "routes", raw: "int:routes" },
              locks: ["suite-lock"],
            },
          ],
          files: [
            {
              path: "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
              locks: ["worker-loop"],
            },
          ],
          fileLocksByPath: new Map([
            [
              "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
              ["worker-loop"],
            ],
          ]),
        },
      },
    });

    const graphs = buildRuntimeGraphs([
      {
        config: api,
        skipped: false,
        runtimeConfigs: [api],
        runtimeNames: ["api"],
        runtimeKey: "api",
        suites: collectSuites(api, ["int"], [], []),
      },
    ]);

    const queue = buildTaskQueue(
      [
        {
          config: api,
          skipped: false,
          runtimeConfigs: [api],
          runtimeNames: ["api"],
          runtimeKey: "api",
          assignedGraphKey: "api",
          suites: collectSuites(api, ["int"], [], []),
        },
      ],
      graphs,
      { files: {} }
    );

    expect(queue).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          file: "src/api/routes/__testkit__/health.int.testkit.ts",
          locks: ["suite-lock"],
        }),
        expect.objectContaining({
          file: "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
          locks: ["suite-lock", "worker-loop"],
        }),
      ])
    );
  });

  it("builds exact runtime graphs and claims single file tasks", () => {
    const api = makeConfig("api", {
      testkit: {
        runtime: {
          instances: 2,
          maxConcurrentTasks: 4,
        },
      },
    });
    const frontend = makeConfig("frontend", {
      dependsOn: ["api"],
      testkit: {
        runtime: {
          instances: 1,
          maxConcurrentTasks: 2,
        },
      },
    });

    const plans = [
      {
        config: api,
        skipped: false,
        runtimeConfigs: [api],
        runtimeNames: ["api"],
        runtimeKey: "api",
        suites: [
          {
            name: "health",
            type: "integration",
            framework: "k6",
            files: ["a.js", "b.js"],
            orderIndex: 0,
            weight: 2,
          },
        ],
      },
      {
        config: frontend,
        skipped: false,
        runtimeConfigs: [api, frontend],
        runtimeNames: ["api", "frontend"],
        runtimeKey: "api|frontend",
        suites: [
          {
            name: "auth",
            type: "e2e",
            framework: "playwright",
            files: ["auth.spec.js"],
            orderIndex: 0,
            weight: 1,
          },
        ],
      },
    ];

    expect(applyShard(["a", "b", "c", "d"], { index: 2, total: 2 })).toEqual(["b", "d"]);

    const graphs = buildRuntimeGraphs(plans);
    expect(graphs).toEqual([
      expect.objectContaining({
        key: "api",
        instanceCount: 2,
        maxConcurrentTasks: 4,
        targetNames: ["api"],
      }),
      expect.objectContaining({
        key: "api|frontend",
        instanceCount: 1,
        maxConcurrentTasks: 2,
        targetNames: ["frontend"],
      }),
    ]);
    expect(buildGraphDirName(["api", "frontend"])).toBe("api__frontend");

    const queue = buildTaskQueue(plans, graphs, { files: {} });
    expect(queue).toHaveLength(3);
    expect(claimNextTask(queue, "api|frontend")).toMatchObject({
      framework: "playwright",
      graphKey: "api|frontend",
      file: "auth.spec.js",
    });
    expect(claimNextTask(queue, "api")).toMatchObject({
      framework: "k6",
      graphKey: "api",
    });
  });

  it("skips blocked preferred-graph work and claims the next runnable task", () => {
    const queue = [
      {
        id: 1,
        graphKey: "api",
        file: "blocked.js",
      },
      {
        id: 2,
        graphKey: "api|frontend",
        file: "runnable.js",
      },
    ];

    const claimed = claimNextTask(
      queue,
      "api",
      (task) => task.file === "runnable.js"
    );

    expect(claimed).toMatchObject({
      id: 2,
      file: "runnable.js",
    });
    expect(queue).toEqual([
      expect.objectContaining({
        id: 1,
        file: "blocked.js",
      }),
    ]);
  });
});
