/* eslint-disable no-console -- diagnostic skip messages in a Docker-guarded suite */
import { spawn } from "node:child_process";
import { setTimeout as sleepTimer } from "node:timers/promises";

import { Effect } from "effect";
import { afterEach, describe, expect, it as vitestIt } from "vitest";

import { DockerSandboxProvider } from "./docker-sandbox";
import { OrbSandboxError } from "./domain";

// Real containers need more than the 5s default; bind a generous timeout here.
const it = (name: string, fn: () => Promise<void>): void => {
  vitestIt(name, fn, 30_000);
};

// Node-compatible CLI helpers so the suite runs under vitest/node workers as
// well as the Bun runtime. `Bun.*` globals are absent under vitest.

const runCliText = (args: readonly string[]): Promise<string> => {
  const [command = "docker", ...rest] = args;
  const proc = spawn(command, rest, {
    stdio: ["ignore", "pipe", "pipe"],
  });
  const chunks: Buffer[] = [];
  // eslint-disable-next-line promise/avoid-new -- wrapping one-shot stream events in a single promise
  return new Promise((resolve) => {
    proc.stdout?.on("data", (c: Buffer) => chunks.push(c));
    proc.on("close", () => {
      resolve(Buffer.concat(chunks).toString("utf-8"));
    });
  });
};

const runCliExit = (args: readonly string[]): Promise<number | null> =>
  // eslint-disable-next-line promise/avoid-new -- wrapping one-shot child close in a single promise
  new Promise((resolve) => {
    const [command = "docker", ...rest] = args;
    const proc = spawn(command, rest, {
      stdio: ["ignore", "pipe", "pipe"],
    });
    proc.on("error", () => resolve(null));
    proc.on("close", (code) => resolve(code));
  });

const dockerAvailable = async (): Promise<boolean> => {
  try {
    const code = await runCliExit([
      "docker",
      "version",
      "--format",
      "{{.Server.Version}}",
    ]);
    return code === 0;
  } catch {
    return false;
  }
};

const containerExists = async (name: string): Promise<boolean> => {
  const listing = await runCliText([
    "docker",
    "ps",
    "-a",
    "--filter",
    `name=^${name}$`,
    "--format",
    "{{.ID}}",
  ]);
  return listing.trim().length > 0;
};

const created: string[] = [];

afterEach(async () => {
  for (const name of created.splice(0)) {
    // eslint-disable-next-line no-await-in-loop -- sequential cleanup of test containers
    await runCliText(["docker", "rm", "-f", name]);
  }
});

// Top-level await so describe.skipIf can evaluate Docker availability at registration.
const HAVE_DOCKER = await dockerAvailable();

const uniqueName = (label: string): string => {
  const name = `orb-test-${label}-${Date.now()}-${Math.trunc(Math.random() * 1e6)}`;
  created.push(name);
  return name;
};

const statusOf = async (
  client: {
    listProcesses: () => Promise<{
      processes: { id: string; status?: string }[];
    }>;
  },
  id: string
): Promise<string | undefined> => {
  const list = await client.listProcesses();
  return list.processes.find((p) => p.id === id)?.status;
};

const waitForStatus = async (
  check: () => Promise<string | undefined>,
  target: string,
  timeoutMs = 8000
): Promise<string | undefined> => {
  const deadline = Date.now() + timeoutMs;
  let status: string | undefined;
  while (Date.now() < deadline) {
    // eslint-disable-next-line no-await-in-loop -- polling probes sequentially
    status = await check();
    if (status === target) {
      return status;
    }
    // eslint-disable-next-line no-await-in-loop -- polling probes sequentially
    await sleepTimer(250);
  }
  return status;
};

describe.skipIf(!HAVE_DOCKER)("DockerSandboxProvider lifecycle", () => {
  it("starts a container and removes it on dispose", async () => {
    const name = uniqueName("start");
    const provider = await Effect.runPromise(
      DockerSandboxProvider.create({
        containerName: name,
        hostWorkspacePath: `/tmp/${name}`,
      })
    );
    const client = await provider.start();
    expect(provider.isStarted).toBe(true);

    const version = await client.runProcess({ command: "bun --version" });
    expect(version.exitCode).toBe(0);
    expect((version.stdout ?? "").trim()).toMatch(/\d/u);

    expect(await containerExists(name)).toBe(true);
    await provider.dispose();
    expect(await containerExists(name)).toBe(false);
    expect(provider.isStarted).toBe(false);
  });

  it("dispose is idempotent and a no-op before start", async () => {
    const name = uniqueName("noop");
    const provider = await Effect.runPromise(
      DockerSandboxProvider.create({
        containerName: name,
        hostWorkspacePath: `/tmp/${name}`,
      })
    );
    await expect(provider.dispose()).resolves.toBeUndefined();
    await expect(provider.dispose()).resolves.toBeUndefined();
    expect(await containerExists(name)).toBe(false);
  });
});

describe.skipIf(!HAVE_DOCKER)("DockerSandboxClient detached processes", () => {
  const makeClient = async (label: string) => {
    const name = uniqueName(label);
    const provider = await Effect.runPromise(
      DockerSandboxProvider.create({
        containerName: name,
        hostWorkspacePath: `/tmp/${name}`,
      })
    );
    const client = await provider.start();
    return { client, name, provider };
  };

  it("captures a real group PID for a detached process", async () => {
    const { client, provider } = await makeClient("pid");
    try {
      const info = await client.createProcess({ command: "sleep 30" });
      expect(typeof info.pid).toBe("number");
      expect((info.pid ?? 0) > 0).toBe(true);
      expect(info.status).toBe("running");
      await client.killProcess(info.id);
    } finally {
      await provider.dispose();
    }
  });

  it("records durable stdout/stderr logs and a real exit code", async () => {
    const { client, provider } = await makeClient("logs");
    try {
      const info = await client.createProcess({
        command: "sh -c 'echo stdout-line; echo stderr-line 1>&2'",
      });
      const status = await waitForStatus(
        () => statusOf(client, info.id),
        "exited"
      );
      expect(status).toBe("exited");

      const list = await client.listProcesses();
      const refreshed = list.processes.find((p) => p.id === info.id);
      expect(refreshed?.exitCode).toBe(0);

      const stdout = await client.getProcessLogs(info.id, { stream: "stdout" });
      expect(stdout.entries.some((e) => e.data.includes("stdout-line"))).toBe(
        true
      );
      const stderr = await client.getProcessLogs(info.id, { stream: "stderr" });
      expect(stderr.entries.some((e) => e.data.includes("stderr-line"))).toBe(
        true
      );
    } finally {
      await provider.dispose();
    }
  });

  it("refreshes status after natural completion", async () => {
    const { client, provider } = await makeClient("exit");
    try {
      const info = await client.createProcess({ command: "sh -c 'exit 7'" });
      const status = await waitForStatus(
        () => statusOf(client, info.id),
        "exited"
      );
      expect(status).toBe("exited");
      const list = await client.listProcesses();
      const refreshed = list.processes.find((p) => p.id === info.id);
      expect(refreshed?.exitCode).toBe(7);
    } finally {
      await provider.dispose();
    }
  });

  it("stopProcess terminates a long-running detached process", async () => {
    const { client, provider } = await makeClient("stop");
    try {
      const info = await client.createProcess({ command: "sleep 30" });
      expect(info.status).toBe("running");

      const stopped = await client.stopProcess(info.id);
      expect(["stopped", "killed"]).toContain(stopped.status);

      const status = await statusOf(client, info.id);
      expect(status).not.toBe("running");
    } finally {
      await provider.dispose();
    }
  });

  it("killProcess forces termination", async () => {
    const { client, provider } = await makeClient("kill");
    try {
      const info = await client.createProcess({ command: "sleep 30" });
      const killed = await client.killProcess(info.id);
      expect(killed.status).toBe("killed");
    } finally {
      await provider.dispose();
    }
  });

  it("getProcessLogs throws for an unknown process", async () => {
    const { client, provider } = await makeClient("unknown");
    try {
      await expect(client.getProcessLogs("nope")).rejects.toBeInstanceOf(
        OrbSandboxError
      );
    } finally {
      await provider.dispose();
    }
  });
});

describe.skipIf(HAVE_DOCKER)("DockerSandboxProvider without Docker", () => {
  it("create fails with DockerUnavailable", async () => {
    const error = await Effect.runPromise(
      Effect.flip(
        DockerSandboxProvider.create({
          containerName: "orb-test-nodocker",
          hostWorkspacePath: "/tmp/orb-test-nodocker",
        })
      )
    );
    expect(error).toBeInstanceOf(OrbSandboxError);
    expect(error.reason).toBe("DockerUnavailable");
  });
});

if (!HAVE_DOCKER) {
  console.warn(
    "[docker-sandbox.test.ts] Docker daemon unavailable; Docker lifecycle tests skipped."
  );
}
