/**
 * Tests plugin SDK file lock retry, stale lock, and cleanup behavior.
 */
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  acquireFileLock,
  drainFileLockStateForTest,
  FILE_LOCK_TIMEOUT_ERROR_CODE,
  resetFileLockStateForTest,
} from "./file-lock.js";

describe("acquireFileLock", () => {
  let tempDir = "";

  beforeEach(async () => {
    resetFileLockStateForTest();
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-file-lock-"));
  });

  afterEach(async () => {
    await drainFileLockStateForTest();
    if (tempDir) {
      await fs.rm(tempDir, { recursive: true, force: true });
    }
  });

  it("respects the configured retry budget even when stale windows are much larger", async () => {
    const filePath = path.join(tempDir, "oauth-refresh");
    const lockPath = `${filePath}.lock`;
    const options = {
      retries: {
        retries: 1,
        factor: 1,
        minTimeout: 20,
        maxTimeout: 20,
      },
      stale: 100,
    } as const;

    await fs.writeFile(
      lockPath,
      JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
      "utf8",
    );

    let caught: { code?: string; lockPath?: string } | undefined;
    try {
      await acquireFileLock(filePath, options);
    } catch (error) {
      caught = error as { code?: string; lockPath?: string };
    }
    expect(caught?.code).toBe(FILE_LOCK_TIMEOUT_ERROR_CODE);
    expect(caught?.lockPath ? path.relative(await fs.realpath(tempDir), caught.lockPath) : "").toBe(
      "oauth-refresh.lock",
    );
  }, 5_000);

  it("removes a reported stale lock when its owner pid is dead", async () => {
    const filePath = path.join(tempDir, "auth-profiles.json");
    const lockPath = `${filePath}.lock`;
    const options = {
      retries: {
        retries: 0,
        factor: 1,
        minTimeout: 1,
        maxTimeout: 1,
      },
      stale: 10,
    } as const;

    const deadPid = -1;
    await fs.writeFile(
      lockPath,
      JSON.stringify({ pid: deadPid, createdAt: new Date(Date.now() - 60_000).toISOString() }),
      "utf8",
    );

    const lock = await acquireFileLock(filePath, options);
    try {
      await expect(fs.realpath(lock.lockPath)).resolves.toBe(await fs.realpath(lockPath));
      await expect(fs.readFile(lockPath, "utf8")).resolves.toContain(`"pid"`);
    } finally {
      await lock.release();
    }
  });

  it("keeps a reported stale lock when its payload is not readable", async () => {
    const filePath = path.join(tempDir, "payload-pending");
    const lockPath = `${filePath}.lock`;
    const options = {
      retries: {
        retries: 0,
        factor: 1,
        minTimeout: 1,
        maxTimeout: 1,
      },
      stale: 10,
    } as const;

    await fs.writeFile(lockPath, "{", "utf8");

    let caught: { lockPath?: string } | undefined;
    await expect(
      (async () => {
        try {
          await acquireFileLock(filePath, options);
        } catch (err) {
          caught = err as { lockPath?: string };
          throw err;
        }
      })(),
    ).rejects.toMatchObject({
      code: FILE_LOCK_TIMEOUT_ERROR_CODE,
    });
    await expect(fs.realpath(caught?.lockPath ?? "")).resolves.toBe(await fs.realpath(lockPath));
    await expect(fs.readFile(lockPath, "utf8")).resolves.toBe("{");
  });

  it("keeps an expired lock when its live owner has no starttime proof", async () => {
    const filePath = path.join(tempDir, "live-owner");
    const lockPath = `${filePath}.lock`;
    const options = {
      retries: {
        retries: 0,
        factor: 1,
        minTimeout: 1,
        maxTimeout: 1,
      },
      stale: 10,
    } as const;

    await fs.writeFile(
      lockPath,
      JSON.stringify({ pid: process.pid, createdAt: new Date(Date.now() - 60_000).toISOString() }),
      "utf8",
    );

    let caught: { lockPath?: string } | undefined;
    await expect(
      (async () => {
        try {
          await acquireFileLock(filePath, options);
        } catch (err) {
          caught = err as { lockPath?: string };
          throw err;
        }
      })(),
    ).rejects.toMatchObject({
      code: FILE_LOCK_TIMEOUT_ERROR_CODE,
    });
    await expect(fs.realpath(caught?.lockPath ?? "")).resolves.toBe(await fs.realpath(lockPath));
    await expect(fs.readFile(lockPath, "utf8")).resolves.toContain(`"pid":${process.pid}`);
  });

  it("closes an opened lock handle when writing the owner payload fails", async () => {
    const filePath = path.join(tempDir, "write-fails");
    const writeError = new Error("owner write failed");
    const close = vi.fn().mockResolvedValue(undefined);
    vi.spyOn(fs, "open").mockResolvedValue({
      close,
      writeFile: vi.fn().mockRejectedValue(writeError),
    } as unknown as Awaited<ReturnType<typeof fs.open>>);

    await expect(
      acquireFileLock(filePath, {
        retries: {
          retries: 0,
          factor: 1,
          minTimeout: 1,
          maxTimeout: 1,
        },
        stale: 100,
      }),
    ).rejects.toThrow(writeError);

    expect(close).toHaveBeenCalledTimes(1);
  });
});
