import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../helpers/import-fresh.js";

beforeEach(() => {
  vi.spyOn(console, "log").mockImplementation(() => {});
  vi.spyOn(console, "error").mockImplementation(() => {});
  vi.spyOn(process.stdout, "write").mockImplementation(() => true);
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
});

afterEach(() => {
  vi.useRealTimers();
  vi.restoreAllMocks();
  vi.resetModules();
});

describe("test planner executor", () => {
  it("falls back to child exit when close never arrives", async () => {
    vi.useRealTimers();
    const stdout = new PassThrough();
    const stderr = new PassThrough();
    const fakeChild = Object.assign(new EventEmitter(), {
      stdout,
      stderr,
      pid: 12345,
      kill: vi.fn(),
    });
    const spawnMock = vi.fn(() => {
      setTimeout(() => {
        fakeChild.emit("exit", 0, null);
      }, 0);
      return fakeChild;
    });
    vi.doMock("node:child_process", () => ({
      spawn: spawnMock,
    }));

    const { executePlan, createExecutionArtifacts } = await importFreshModule<
      typeof import("../../scripts/test-planner/executor.mjs")
    >(import.meta.url, "../../scripts/test-planner/executor.mjs?scope=exit-fallback");
    const artifacts = createExecutionArtifacts({ OPENCLAW_TEST_CLOSE_GRACE_MS: "10" });
    const executePromise = executePlan(
      {
        failurePolicy: "fail-fast",
        passthroughMetadataOnly: true,
        passthroughOptionArgs: [],
        runtimeCapabilities: { isWindowsCi: false, isCI: false, isWindows: false },
      },
      {
        env: { OPENCLAW_TEST_CLOSE_GRACE_MS: "10" },
        artifacts,
      },
    );

    await expect(executePromise).resolves.toMatchObject({
      exitCode: 0,
      summary: {
        failedRunCount: 0,
      },
    });
    expect(spawnMock).toHaveBeenCalledTimes(1);

    artifacts.cleanupTempArtifacts();
  });

  it("collects failures across planned units when failure policy is collect-all", async () => {
    vi.useRealTimers();
    const children = [1, 2].map((pid, index) => {
      const stdout = new PassThrough();
      const stderr = new PassThrough();
      return Object.assign(new EventEmitter(), {
        stdout,
        stderr,
        pid,
        kill: vi.fn(),
        index,
      });
    });
    let childIndex = 0;
    const spawnMock = vi.fn(() => {
      const child = children[childIndex];
      childIndex += 1;
      setTimeout(() => {
        child.stdout.write(
          child.index === 0
            ? " ❯ src/alpha.test.ts (1 test | 1 failed)\n"
            : " ❯ src/beta.test.ts (1 test | 1 failed)\n",
        );
        child.emit("exit", 1, null);
        child.emit("close", 1, null);
      }, 0);
      return child;
    });
    vi.doMock("node:child_process", () => ({
      spawn: spawnMock,
    }));

    const { executePlan, createExecutionArtifacts } = await importFreshModule<
      typeof import("../../scripts/test-planner/executor.mjs")
    >(import.meta.url, "../../scripts/test-planner/executor.mjs?scope=collect-all");
    const artifacts = createExecutionArtifacts({});
    const report = await executePlan(
      {
        failurePolicy: "collect-all",
        passthroughMetadataOnly: false,
        passthroughOptionArgs: [],
        targetedUnits: [],
        parallelUnits: [
          { id: "unit-a", args: ["vitest", "run", "src/alpha.test.ts"] },
          { id: "unit-b", args: ["vitest", "run", "src/beta.test.ts"] },
        ],
        serialUnits: [],
        serialPrefixUnits: [],
        shardCount: 1,
        shardIndexOverride: null,
        topLevelSingleShardAssignments: new Map(),
        runtimeCapabilities: { isWindowsCi: false, isCI: false, isWindows: false },
        topLevelParallelEnabled: false,
        topLevelParallelLimit: 1,
        deferredRunConcurrency: 1,
        passthroughRequiresSingleRun: false,
      },
      {
        env: {},
        artifacts,
      },
    );

    expect(spawnMock).toHaveBeenCalledTimes(2);
    expect(report.exitCode).toBe(1);
    expect(report.summary.failedRunCount).toBe(2);
    expect(report.summary.failedTestFileCount).toBe(2);
    expect(report.results.map((result) => result.classification)).toEqual([
      "test-failure",
      "test-failure",
    ]);

    artifacts.cleanupTempArtifacts();
  });

  it("injects a valid localstorage file path into child NODE_OPTIONS", async () => {
    vi.useRealTimers();
    const stdout = new PassThrough();
    const stderr = new PassThrough();
    const fakeChild = Object.assign(new EventEmitter(), {
      stdout,
      stderr,
      pid: 123,
      kill: vi.fn(),
    });
    let capturedEnv;
    const spawnMock = vi.fn((_command, _args, options) => {
      capturedEnv = options?.env;
      setTimeout(() => {
        fakeChild.emit("exit", 0, null);
        fakeChild.emit("close", 0, null);
      }, 0);
      return fakeChild;
    });
    vi.doMock("node:child_process", () => ({
      spawn: spawnMock,
    }));

    const { executePlan, createExecutionArtifacts } = await importFreshModule<
      typeof import("../../scripts/test-planner/executor.mjs")
    >(import.meta.url, "../../scripts/test-planner/executor.mjs?scope=localstorage-file");
    const artifacts = createExecutionArtifacts({
      NODE_OPTIONS: "--max_old_space_size=4096 --localstorage-file",
    });
    await expect(
      executePlan(
        {
          failurePolicy: "fail-fast",
          passthroughMetadataOnly: false,
          passthroughOptionArgs: [],
          targetedUnits: [],
          parallelUnits: [{ id: "unit-a", args: ["vitest", "run", "src/alpha.test.ts"] }],
          serialUnits: [],
          serialPrefixUnits: [],
          shardCount: 1,
          shardIndexOverride: null,
          topLevelSingleShardAssignments: new Map(),
          runtimeCapabilities: { isWindowsCi: false, isCI: false, isWindows: false },
          topLevelParallelEnabled: false,
          topLevelParallelLimit: 1,
          deferredRunConcurrency: 1,
          passthroughRequiresSingleRun: false,
        },
        {
          env: {
            NODE_OPTIONS: "--max_old_space_size=4096 --localstorage-file",
          },
          artifacts,
        },
      ),
    ).resolves.toMatchObject({
      exitCode: 0,
    });

    expect(spawnMock).toHaveBeenCalledTimes(1);
    expect(capturedEnv?.NODE_OPTIONS).toContain("--max_old_space_size=4096");
    expect(capturedEnv?.NODE_OPTIONS).toMatch(
      /--localstorage-file=[^\s]+\.localstorage\.json(?:\s|$)/u,
    );
    expect(capturedEnv?.NODE_OPTIONS).not.toMatch(/(^|\s)--localstorage-file(?=\s|$)/u);

    artifacts.cleanupTempArtifacts();
  });
});
