// Covers managed task-flow creation, lookup, ownership, and state transitions.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
import {
  createTaskFlowForTask as createTaskFlowForTaskOrNull,
  createManagedTaskFlow as createManagedTaskFlowOrNull,
  deleteTaskFlowRecordById,
  getTaskFlowRegistryRestoreFailure,
  failFlow,
  getTaskFlowById,
  listTaskFlowRecords,
  requestFlowCancel,
  reloadTaskFlowRegistryFromStore,
  resumeFlow,
  setFlowWaiting,
  syncFlowFromTaskResult,
  updateFlowRecordByIdExpectedRevision,
} from "./task-flow-registry.js";
import type { TaskFlowRecord } from "./task-flow-registry.types.js";
import {
  configureTaskFlowRegistryRuntime,
  createFlowRecord as createFlowRecordOrNull,
  resetTaskFlowRegistryForTests,
} from "./task-runtime.test-helpers.js";

function createFlowRecord(params: Parameters<typeof createFlowRecordOrNull>[0]): TaskFlowRecord {
  const flow = createFlowRecordOrNull(params);
  if (!flow) {
    throw new Error("expected TaskFlow creation to succeed");
  }
  return flow;
}

function createManagedTaskFlow(
  params: Parameters<typeof createManagedTaskFlowOrNull>[0],
): TaskFlowRecord {
  const flow = createManagedTaskFlowOrNull(params);
  if (!flow) {
    throw new Error("expected managed TaskFlow creation to succeed");
  }
  return flow;
}

function createTaskFlowForTask(
  params: Parameters<typeof createTaskFlowForTaskOrNull>[0],
): TaskFlowRecord {
  const flow = createTaskFlowForTaskOrNull(params);
  if (!flow) {
    throw new Error("expected task-mirrored TaskFlow creation to succeed");
  }
  return flow;
}

function syncFlowFromTaskForTest(
  task: Parameters<typeof syncFlowFromTaskResult>[0],
): TaskFlowRecord | null {
  const result = syncFlowFromTaskResult(task);
  return result.ok ? result.flow : null;
}

async function withFlowRegistryTempDir<T>(run: () => Promise<T>): Promise<T> {
  return await withOpenClawTestState(
    { layout: "state-only", prefix: "openclaw-task-flow-registry-" },
    async () => {
      resetTaskFlowRegistryForTests();
      try {
        return await run();
      } finally {
        resetTaskFlowRegistryForTests();
      }
    },
  );
}

describe("task-flow-registry", () => {
  beforeEach(() => {
    vi.useRealTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
    resetTaskFlowRegistryForTests();
  });

  it("creates managed flows and updates them through revision-checked helpers", async () => {
    await withFlowRegistryTempDir(async () => {
      const created = createManagedTaskFlow({
        ownerKey: "agent:main:main",
        controllerId: "tests/managed-controller",
        goal: "Investigate flaky test",
        currentStep: "spawn_task",
        stateJson: { phase: "spawn" },
      });

      expect(created.flowId).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
      );
      expect(created.syncMode).toBe("managed");
      expect(created.controllerId).toBe("tests/managed-controller");
      expect(created.revision).toBe(0);
      expect(created.status).toBe("queued");
      expect(created.currentStep).toBe("spawn_task");
      expect(created.stateJson).toEqual({ phase: "spawn" });

      const waiting = setFlowWaiting({
        flowId: created.flowId,
        expectedRevision: created.revision,
        currentStep: "await_review",
        stateJson: { phase: "await_review" },
        waitJson: { kind: "task", taskId: "task-123" },
      });
      expect(waiting.applied).toBe(true);
      if (!waiting.applied) {
        throw new Error("Expected wait state update to apply");
      }
      expect(waiting.flow.flowId).toBe(created.flowId);
      expect(waiting.flow.revision).toBe(1);
      expect(waiting.flow.status).toBe("waiting");
      expect(waiting.flow.currentStep).toBe("await_review");
      expect(waiting.flow.waitJson).toEqual({ kind: "task", taskId: "task-123" });

      const conflict = updateFlowRecordByIdExpectedRevision({
        flowId: created.flowId,
        expectedRevision: 0,
        patch: {
          currentStep: "stale",
        },
      });
      expect(conflict.applied).toBe(false);
      if (conflict.applied) {
        throw new Error("Expected stale revision update to conflict");
      }
      expect(conflict.reason).toBe("revision_conflict");
      expect(conflict.current?.flowId).toBe(created.flowId);
      expect(conflict.current?.revision).toBe(1);

      const resumed = resumeFlow({
        flowId: created.flowId,
        expectedRevision: 1,
        status: "running",
        currentStep: "resume_work",
      });
      expect(resumed.applied).toBe(true);
      if (!resumed.applied) {
        throw new Error("Expected resume update to apply");
      }
      expect(resumed.flow.flowId).toBe(created.flowId);
      expect(resumed.flow.revision).toBe(2);
      expect(resumed.flow.status).toBe("running");
      expect(resumed.flow.currentStep).toBe("resume_work");
      expect(resumed.flow.waitJson).toBeNull();

      const cancelRequested = requestFlowCancel({
        flowId: created.flowId,
        expectedRevision: 2,
        cancelRequestedAt: 400,
      });
      expect(cancelRequested.applied).toBe(true);
      if (!cancelRequested.applied) {
        throw new Error("Expected cancel request update to apply");
      }
      expect(cancelRequested.flow.flowId).toBe(created.flowId);
      expect(cancelRequested.flow.revision).toBe(3);
      expect(cancelRequested.flow.cancelRequestedAt).toBe(400);

      const failed = failFlow({
        flowId: created.flowId,
        expectedRevision: 3,
        blockedSummary: "Task runner failed.",
        endedAt: 500,
      });
      expect(failed.applied).toBe(true);
      if (!failed.applied) {
        throw new Error("Expected fail update to apply");
      }
      expect(failed.flow.flowId).toBe(created.flowId);
      expect(failed.flow.revision).toBe(4);
      expect(failed.flow.status).toBe("failed");
      expect(failed.flow.blockedSummary).toBe("Task runner failed.");
      expect(failed.flow.endedAt).toBe(500);

      const flows = listTaskFlowRecords();
      expect(flows).toHaveLength(1);
      expect(flows[0]?.flowId).toBe(created.flowId);
      expect(flows[0]?.revision).toBe(4);
      expect(flows[0]?.cancelRequestedAt).toBe(400);

      expect(deleteTaskFlowRecordById(created.flowId)).toBe(true);
      expect(getTaskFlowById(created.flowId)).toBeUndefined();
    });
  });

  it("requires a controller for managed flows and rejects clearing it later", async () => {
    await withFlowRegistryTempDir(async () => {
      expect(() =>
        createFlowRecord({
          ownerKey: "agent:main:main",
          goal: "Missing controller",
        }),
      ).toThrow("Managed flow controllerId is required.");

      const created = createManagedTaskFlow({
        ownerKey: "agent:main:main",
        controllerId: "tests/managed-controller",
        goal: "Protected controller",
      });

      expect(() =>
        updateFlowRecordByIdExpectedRevision({
          flowId: created.flowId,
          expectedRevision: created.revision,
          patch: {
            controllerId: null,
          },
        }),
      ).toThrow("Managed flow controllerId is required.");
    });
  });

  it("emits restored, upserted, and deleted flow observer events", () => {
    const onEvent = vi.fn();
    configureTaskFlowRegistryRuntime({
      store: {
        loadSnapshot: () => ({
          flows: new Map(),
        }),
        saveSnapshot: () => {},
      },
      observers: {
        onEvent,
      },
    });

    const created = createManagedTaskFlow({
      ownerKey: "agent:main:main",
      controllerId: "tests/observers",
      goal: "Observe observers",
    });

    deleteTaskFlowRecordById(created.flowId);

    expect(onEvent).toHaveBeenCalledWith({
      kind: "restored",
      flows: [],
    });
    const events = onEvent.mock.calls.map((call) => call[0]);
    expect(events[1]?.kind).toBe("upserted");
    expect(events[1]?.flow?.flowId).toBe(created.flowId);
    expect(events[2]?.kind).toBe("deleted");
    expect(events[2]?.flowId).toBe(created.flowId);
  });

  it("keeps restore failures sticky until an explicit reload succeeds", () => {
    const hiddenFlow: TaskFlowRecord = {
      flowId: "hidden-flow",
      syncMode: "managed",
      ownerKey: "agent:main:main",
      controllerId: "tests/hidden-flow",
      revision: 4,
      status: "running",
      notifyPolicy: "done_only",
      goal: "Existing durable flow",
      createdAt: 10,
      updatedAt: 20,
    };
    const loadSnapshot = vi
      .fn()
      .mockImplementationOnce(() => {
        throw new Error("SQLITE_CORRUPT: task-flow restore failed");
      })
      .mockReturnValue({
        flows: new Map([[hiddenFlow.flowId, hiddenFlow]]),
      });
    const upsertFlow = vi.fn();
    const deleteFlow = vi.fn();
    configureTaskFlowRegistryRuntime({
      store: {
        loadSnapshot,
        saveSnapshot: () => {},
        upsertFlow,
        deleteFlow,
      },
    });

    expect(() => listTaskFlowRecords()).toThrow(
      "Task-flow registry restore failed: SQLITE_CORRUPT: task-flow restore failed",
    );
    expect(getTaskFlowRegistryRestoreFailure()).toBe("SQLITE_CORRUPT: task-flow restore failed");
    expect(() =>
      createManagedTaskFlowOrNull({
        ownerKey: "agent:main:main",
        controllerId: "tests/restore-failure",
        goal: "Must not persist over hidden flows",
      }),
    ).toThrow("Task-flow registry restore failed: SQLITE_CORRUPT: task-flow restore failed");
    expect(() =>
      updateFlowRecordByIdExpectedRevision({
        flowId: hiddenFlow.flowId,
        expectedRevision: hiddenFlow.revision,
        patch: { currentStep: "must not overwrite hidden state" },
      }),
    ).toThrow("Task-flow registry restore failed: SQLITE_CORRUPT: task-flow restore failed");
    expect(() => deleteTaskFlowRecordById(hiddenFlow.flowId)).toThrow(
      "Task-flow registry restore failed: SQLITE_CORRUPT: task-flow restore failed",
    );
    expect(loadSnapshot).toHaveBeenCalledTimes(1);
    expect(upsertFlow).not.toHaveBeenCalled();
    expect(deleteFlow).not.toHaveBeenCalled();

    reloadTaskFlowRegistryFromStore();

    expect(loadSnapshot).toHaveBeenCalledTimes(2);
    expect(getTaskFlowRegistryRestoreFailure()).toBeNull();
    expect(listTaskFlowRecords()).toEqual([expect.objectContaining(hiddenFlow)]);
    expect(
      createManagedTaskFlowOrNull({
        ownerKey: "agent:main:main",
        controllerId: "tests/restore-recovery",
        goal: "Create after explicit recovery",
      }),
    ).not.toBeNull();
    expect(upsertFlow).toHaveBeenCalledTimes(1);
  });

  it("does not throw or register memory when flow create persistence fails", () => {
    const upsertFlow = vi.fn((_flow: TaskFlowRecord) => {
      throw new Error("SQLITE_FULL: database or disk is full");
    });
    configureTaskFlowRegistryRuntime({
      store: {
        loadSnapshot: () => ({
          flows: new Map(),
        }),
        saveSnapshot: () => {},
        upsertFlow,
      },
    });

    const created = createManagedTaskFlowOrNull({
      ownerKey: "agent:main:main",
      controllerId: "tests/create-persist-fail",
      goal: "Create while persistence fails",
    });

    expect(created).toBeNull();
    const attempted = upsertFlow.mock.calls[0]?.[0];
    expect(attempted?.flowId).toEqual(expect.any(String));
    expect(getTaskFlowById(attempted?.flowId ?? "")).toBeUndefined();
  });

  it("does not throw or mutate memory when flow update persistence fails", () => {
    let failUpsert = false;
    const upsertFlow = vi.fn(() => {
      if (failUpsert) {
        throw new Error("SQLITE_IOERR: disk I/O error");
      }
    });
    configureTaskFlowRegistryRuntime({
      store: {
        loadSnapshot: () => ({
          flows: new Map(),
        }),
        saveSnapshot: () => {},
        upsertFlow,
      },
    });
    const created = createManagedTaskFlow({
      ownerKey: "agent:main:main",
      controllerId: "tests/update-persist-fail",
      goal: "Update while persistence fails",
    });

    failUpsert = true;
    const result = setFlowWaiting({
      flowId: created.flowId,
      expectedRevision: created.revision,
      currentStep: "persist failed",
    });

    expect(result).toMatchObject({
      applied: false,
      reason: "persist_failed",
      current: {
        flowId: created.flowId,
        revision: 0,
        status: "queued",
      },
    });
    expect(getTaskFlowById(created.flowId)).toMatchObject({
      revision: 0,
      status: "queued",
    });
  });

  it("does not throw or delete memory when flow delete persistence fails", () => {
    const deleteFlow = vi.fn(() => {
      throw new Error("SQLITE_BUSY: database is locked");
    });
    configureTaskFlowRegistryRuntime({
      store: {
        loadSnapshot: () => ({
          flows: new Map(),
        }),
        saveSnapshot: () => {},
        upsertFlow: () => {},
        deleteFlow,
      },
    });
    const created = createManagedTaskFlow({
      ownerKey: "agent:main:main",
      controllerId: "tests/delete-persist-fail",
      goal: "Delete while persistence fails",
    });

    expect(deleteTaskFlowRecordById(created.flowId)).toBe(false);

    expect(deleteFlow).toHaveBeenCalledWith(created.flowId);
    expect(getTaskFlowById(created.flowId)?.flowId).toBe(created.flowId);
  });

  it("normalizes restored managed flows without a controller id", () => {
    configureTaskFlowRegistryRuntime({
      store: {
        loadSnapshot: () => ({
          flows: new Map([
            [
              "legacy-managed",
              {
                flowId: "legacy-managed",
                syncMode: "managed",
                ownerKey: "agent:main:main",
                revision: 0,
                status: "queued",
                notifyPolicy: "done_only",
                goal: "Legacy managed flow",
                createdAt: 10,
                updatedAt: 10,
              },
            ],
          ]),
        }),
        saveSnapshot: () => {},
      },
    });

    const restored = getTaskFlowById("legacy-managed");
    expect(restored?.flowId).toBe("legacy-managed");
    expect(restored?.syncMode).toBe("managed");
    expect(restored?.controllerId).toBe("core/legacy-restored");
  });

  it("mirrors one-task flow state from tasks and leaves managed flows alone", async () => {
    await withFlowRegistryTempDir(async () => {
      const mirrored = createTaskFlowForTask({
        task: {
          ownerKey: "agent:main:main",
          taskId: "task-running",
          notifyPolicy: "done_only",
          status: "running",
          label: "Fix permissions",
          task: "Fix permissions",
          createdAt: 100,
          lastEventAt: 100,
        },
      });

      const blocked = syncFlowFromTaskForTest({
        taskId: "task-blocked",
        parentFlowId: mirrored.flowId,
        status: "succeeded",
        terminalOutcome: "blocked",
        notifyPolicy: "done_only",
        label: "Fix permissions",
        task: "Fix permissions",
        lastEventAt: 200,
        endedAt: 200,
        terminalSummary: "Writable session required.",
      });
      if (!blocked) {
        throw new Error("Expected blocked mirrored flow update");
      }
      expect(blocked.flowId).toBe(mirrored.flowId);
      expect(blocked.syncMode).toBe("task_mirrored");
      expect(blocked.status).toBe("blocked");
      expect(blocked.blockedTaskId).toBe("task-blocked");
      expect(blocked.blockedSummary).toBe("Writable session required.");
      expect(blocked.endedAt).toBe(200);
      expect(blocked.updatedAt).toBe(200);

      const delivered = syncFlowFromTaskForTest({
        taskId: "task-blocked",
        parentFlowId: mirrored.flowId,
        status: "succeeded",
        terminalOutcome: "blocked",
        notifyPolicy: "done_only",
        label: "Fix permissions",
        task: "Fix permissions",
        lastEventAt: 250,
        endedAt: 200,
        terminalSummary: "Writable session required.",
      });
      if (!delivered) {
        throw new Error("Expected repeated mirrored flow update");
      }
      expect(delivered.flowId).toBe(mirrored.flowId);
      expect(delivered.status).toBe("blocked");
      expect(delivered.endedAt).toBe(200);
      expect(delivered.updatedAt).toBe(200);

      const terminalCreated = createTaskFlowForTask({
        task: {
          ownerKey: "agent:main:main",
          taskId: "task-failed",
          notifyPolicy: "done_only",
          status: "failed",
          label: "Fail permissions",
          task: "Fail permissions",
          createdAt: 100,
          lastEventAt: 300,
          endedAt: 200,
        },
      });
      expect(terminalCreated.status).toBe("failed");
      expect(terminalCreated.endedAt).toBe(200);
      expect(terminalCreated.updatedAt).toBe(200);

      const managed = createManagedTaskFlow({
        ownerKey: "agent:main:main",
        controllerId: "tests/managed",
        goal: "Cluster PRs",
        currentStep: "wait_for",
        status: "waiting",
        waitJson: { kind: "external_event" },
      });
      const syncedManaged = syncFlowFromTaskForTest({
        taskId: "task-child",
        parentFlowId: managed.flowId,
        status: "running",
        notifyPolicy: "done_only",
        label: "Child task",
        task: "Child task",
        lastEventAt: 250,
        progressSummary: "Running child task",
      });
      if (!syncedManaged) {
        throw new Error("Expected managed flow sync result");
      }
      expect(syncedManaged.flowId).toBe(managed.flowId);
      expect(syncedManaged.syncMode).toBe("managed");
      expect(syncedManaged.status).toBe("waiting");
      expect(syncedManaged.currentStep).toBe("wait_for");
      expect(syncedManaged.waitJson).toEqual({ kind: "external_event" });
    });
  });

  it("preserves explicit json null in state and wait payloads", async () => {
    await withFlowRegistryTempDir(async () => {
      const created = createManagedTaskFlow({
        ownerKey: "agent:main:main",
        controllerId: "tests/null-state",
        goal: "Null payloads",
        stateJson: null,
        waitJson: null,
      });

      expect(created.flowId).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
      );
      expect(created.stateJson).toBeNull();
      expect(created.waitJson).toBeNull();

      const resumed = resumeFlow({
        flowId: created.flowId,
        expectedRevision: created.revision,
        stateJson: null,
      });

      expect(resumed.applied).toBe(true);
      if (!resumed.applied) {
        throw new Error("Expected resume update to apply");
      }
      expect(resumed.flow.flowId).toBe(created.flowId);
      expect(resumed.flow.stateJson).toBeNull();
    });
  });
});
