import { expect, test } from "@playwright/test";
import { randomInt } from "node:crypto";
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
import { apiRequest } from "./api.js";
import { expectId, readJsonSafe } from "./generalFixtures.js";
const HEADER_PREFIX = "omop:";
const UNDO_PATH = "/api/audit_logs/audit-logs/actions/undo";
const REDO_PATH = "/api/audit_logs/audit-logs/actions/redo";
const ACTIONS_PATH = "/api/audit_logs/audit-logs/actions";
const UNDO_TESTS_DISABLED_ENV = "OM_INTEGRATION_UNDO_TESTS_DISABLED";
function undoTestsDisabled(env = process.env) {
  return parseBooleanWithDefault(env[UNDO_TESTS_DISABLED_ENV], false);
}
function skipIfUndoTestsDisabled() {
  test.skip(undoTestsDisabled(), `${UNDO_TESTS_DISABLED_ENV} is set \u2014 undo/redo integration tests skipped`);
}
function extractOperation(response) {
  const header = response.headers()["x-om-operation"];
  if (!header || typeof header !== "string") return null;
  const trimmed = header.startsWith(HEADER_PREFIX) ? header.slice(HEADER_PREFIX.length) : header;
  try {
    const parsed = JSON.parse(decodeURIComponent(trimmed));
    if (typeof parsed.id !== "string" || typeof parsed.commandId !== "string") return null;
    if (typeof parsed.undoToken !== "string" || !parsed.undoToken) return null;
    return {
      logId: parsed.id,
      undoToken: parsed.undoToken,
      commandId: parsed.commandId,
      resourceKind: parsed.resourceKind ?? null,
      resourceId: parsed.resourceId ?? null
    };
  } catch {
    return null;
  }
}
function expectOperation(response, context) {
  const op = extractOperation(response);
  expect(op, `Expected an undo token (x-om-operation header) for ${context}, got none`).toBeTruthy();
  return op;
}
async function undoByToken(request, token, undoToken) {
  return apiRequest(request, "POST", UNDO_PATH, { token, data: { undoToken } });
}
async function redoByLogId(request, token, logId) {
  return apiRequest(request, "POST", REDO_PATH, { token, data: { logId } });
}
async function undoOk(request, token, undoToken, context) {
  const res = await undoByToken(request, token, undoToken);
  const body = await res.json().catch(() => null);
  expect(res.ok(), `Undo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy();
  expect(body?.ok, `Undo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy();
  return body?.logId;
}
async function redoOk(request, token, logId, context) {
  const res = await redoByLogId(request, token, logId);
  const body = await res.json().catch(() => null);
  expect(res.ok(), `Redo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy();
  expect(body?.ok, `Redo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy();
  return { logId: body?.logId, undoToken: body?.undoToken ?? null };
}
async function expectTokenConsumed(request, token, undoToken, context) {
  const res = await undoByToken(request, token, undoToken);
  expect(res.ok(), `Expected double-undo to be rejected for ${context}, but it succeeded`).toBeFalsy();
}
async function listUndoable(request, token, params = {}) {
  const qs = new URLSearchParams({ undoableOnly: "true", ...params }).toString();
  const res = await apiRequest(request, "GET", `${ACTIONS_PATH}?${qs}`, { token });
  return res.json().catch(() => null);
}
function assertFieldsEqual(actual, expected, fields, context) {
  expect(actual, `${context}: actual entity missing`).toBeTruthy();
  expect(expected, `${context}: expected entity missing`).toBeTruthy();
  for (const field of fields) {
    expect(
      JSON.stringify(actual[field]),
      `${context}: field "${field}" not restored (expected ${JSON.stringify(expected[field])}, got ${JSON.stringify(actual[field])})`
    ).toBe(JSON.stringify(expected[field]));
  }
}
function findRecord(body, id) {
  if (!body || typeof body !== "object") return null;
  if (!Array.isArray(body) && body.id === id) {
    return body;
  }
  for (const value of Array.isArray(body) ? body : Object.values(body)) {
    const found = findRecord(value, id);
    if (found) return found;
  }
  return null;
}
async function readRecord(request, token, entity, id) {
  const path = entity.readPath?.(id) ?? `${entity.collectionPath}?id=${encodeURIComponent(id)}`;
  const response = await apiRequest(request, "GET", path, { token });
  const body = await readJsonSafe(response);
  if (!response.ok()) return null;
  return findRecord(body, id);
}
function fieldValue(record, field) {
  return record?.[field];
}
async function deleteEntity(request, token, entity, id) {
  const path = entity.deletePath?.(id) ?? `${entity.collectionPath}?id=${encodeURIComponent(id)}`;
  return apiRequest(request, "DELETE", path, { token });
}
async function runCrudUndoRoundTrip(request, token, entity) {
  const stamp = `${Date.now()}${randomInt(1e3)}`;
  let createUndoId = null;
  let cycleId = null;
  try {
    const createUndoRes = await apiRequest(request, "POST", entity.collectionPath, {
      token,
      data: entity.createPayload(`${stamp}a`)
    });
    expect(createUndoRes.status(), `${entity.label} create-for-undo status`).toBe(entity.createStatus ?? 201);
    const createUndoOp = expectOperation(createUndoRes, `${entity.label}.create`);
    createUndoId = createUndoOp.resourceId || expectId((await readJsonSafe(createUndoRes))?.id, `${entity.label} create id`);
    expect(fieldValue(await readRecord(request, token, entity, createUndoId), entity.field), `${entity.label} field readable after create`).toBeDefined();
    await undoOk(request, token, createUndoOp.undoToken, `${entity.label} undo create`);
    expect(await readRecord(request, token, entity, createUndoId), `${entity.label} create\u2192undo soft-deletes/removes the record (I3)`).toBeNull();
    await expectTokenConsumed(request, token, createUndoOp.undoToken, `${entity.label} create token consumed (I5)`);
    const createRes = await apiRequest(request, "POST", entity.collectionPath, {
      token,
      data: entity.createPayload(`${stamp}b`)
    });
    expect(createRes.status(), `${entity.label} create status`).toBe(entity.createStatus ?? 201);
    const createOp = expectOperation(createRes, `${entity.label}.create`);
    cycleId = createOp.resourceId || expectId((await readJsonSafe(createRes))?.id, `${entity.label} cycle id`);
    const beforeUpdate = await readRecord(request, token, entity, cycleId);
    const beforeValue = fieldValue(beforeUpdate, entity.field);
    expect(beforeValue, `${entity.label} field readable before update`).toBeDefined();
    const updateRes = await apiRequest(request, "PUT", entity.collectionPath, {
      token,
      data: entity.updatePayload(cycleId, stamp)
    });
    expect(updateRes.status(), `${entity.label} update status`).toBe(entity.updateStatus ?? 200);
    const updateOp = expectOperation(updateRes, `${entity.label}.update`);
    const afterUpdate = await readRecord(request, token, entity, cycleId);
    const afterUpdateValue = fieldValue(afterUpdate, entity.field);
    expect(JSON.stringify(afterUpdateValue), `${entity.label} field changed by update`).not.toBe(JSON.stringify(beforeValue));
    await new Promise((resolve) => setTimeout(resolve, 10));
    await undoOk(request, token, updateOp.undoToken, `${entity.label} undo update`);
    const afterUndo = await readRecord(request, token, entity, cycleId);
    expect(JSON.stringify(fieldValue(afterUndo, entity.field)), `${entity.label} update\u2192undo restores ${entity.field} (I1)`).toBe(JSON.stringify(beforeValue));
    if (typeof beforeUpdate?.updatedAt === "string" && typeof afterUndo?.updatedAt === "string") {
      expect(afterUndo.updatedAt, `${entity.label} undo bumps updatedAt`).not.toBe(beforeUpdate.updatedAt);
    }
    await redoOk(request, token, updateOp.logId, `${entity.label} redo update`);
    expect(JSON.stringify(fieldValue(await readRecord(request, token, entity, cycleId), entity.field)), `${entity.label} redo re-applies update (I6)`).toBe(JSON.stringify(afterUpdateValue));
    const deleteRes = await deleteEntity(request, token, entity, cycleId);
    expect(deleteRes.ok(), `${entity.label} delete status ${deleteRes.status()}`).toBeTruthy();
    const deleteOp = expectOperation(deleteRes, `${entity.label}.delete`);
    expect(await readRecord(request, token, entity, cycleId), `${entity.label} deleted record should not read`).toBeNull();
    await undoOk(request, token, deleteOp.undoToken, `${entity.label} undo delete`);
    expect(fieldValue(await readRecord(request, token, entity, cycleId), entity.field), `${entity.label} delete\u2192undo re-materializes (I2)`).toBeDefined();
  } finally {
    if (createUndoId) await deleteEntity(request, token, entity, createUndoId).catch(() => {
    });
    if (cycleId) await deleteEntity(request, token, entity, cycleId).catch(() => {
    });
  }
}
export {
  UNDO_TESTS_DISABLED_ENV,
  assertFieldsEqual,
  expectOperation,
  expectTokenConsumed,
  extractOperation,
  listUndoable,
  redoByLogId,
  redoOk,
  runCrudUndoRoundTrip,
  skipIfUndoTestsDisabled,
  undoByToken,
  undoOk,
  undoTestsDisabled
};
//# sourceMappingURL=undoHarness.js.map
