/**
 * HTTP server session fixtures shared by gateway session tests.
 */
import fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { AssistantMessage, UserMessage } from "openclaw/plugin-sdk/llm";
import { afterAll, beforeAll, beforeEach, expect, vi } from "vitest";
import type { SessionEntry } from "../../config/sessions.js";
import type { InternalHookEvent } from "../../hooks/internal-hooks.js";
import { resetSystemEventsForTest } from "../../infra/system-events.js";
import { startGatewayServerHarness, type GatewayServerHarness } from "../server.e2e-ws-harness.js";
import {
  connectOk,
  embeddedRunMock,
  installGatewayTestHooks,
  agentDiscoveryMock,
  rpcReq,
  testState,
  writeSessionStore,
} from "../test-helpers.js";

let sessionManagerModulePromise:
  | Promise<typeof import("../../agents/sessions/index.js")>
  | undefined;
let gatewayConfigModulePromise: Promise<typeof import("../../config/config.js")> | undefined;

export async function getSessionManagerModule() {
  sessionManagerModulePromise ??= import("../../agents/sessions/index.js");
  return await sessionManagerModulePromise;
}

export async function getGatewayConfigModule() {
  gatewayConfigModulePromise ??= import("../../config/config.js");
  return await gatewayConfigModulePromise;
}

export async function getSessionsHandlers() {
  return (await import("../server-methods/sessions.js")).sessionsHandlers;
}

export function createDeferred<T>() {
  let resolve!: (value: T | PromiseLike<T>) => void;
  let reject!: (reason?: unknown) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

const sessionCleanupMocks = vi.hoisted(() => ({
  clearSessionQueues: vi.fn((keys: Array<string | undefined>) => {
    const clearedKeys = Array.from(
      new Set(
        keys
          .map((key) => (typeof key === "string" ? key.trim() : ""))
          .filter((key) => key.length > 0),
      ),
    );
    return { followupCleared: 0, laneCleared: 0, keys: clearedKeys };
  }),
  stopSubagentsForRequester: vi.fn(() => ({ stopped: 0 })),
}));

const bootstrapCacheMocks = vi.hoisted(() => ({
  clearBootstrapSnapshot: vi.fn(),
}));

const sessionHookMocks = vi.hoisted(() => ({
  hasInternalHookListeners: vi.fn(() => true),
  triggerInternalHook: vi.fn(async (_eventValue: unknown) => {}),
}));

const beforeResetHookMocks = vi.hoisted(() => ({
  runBeforeReset: vi.fn(async () => {}),
}));

const sessionLifecycleHookMocks = vi.hoisted(() => ({
  runSessionEnd: vi.fn(async () => {}),
  runSessionStart: vi.fn(async () => {}),
}));

const subagentLifecycleHookMocks = vi.hoisted(() => ({
  runSubagentEnded: vi.fn(async () => {}),
}));

const beforeResetHookState = vi.hoisted(() => ({
  hasBeforeResetHook: false,
}));

const sessionLifecycleHookState = vi.hoisted(() => ({
  hasSessionEndHook: true,
  hasSessionStartHook: true,
}));

const subagentLifecycleHookState = vi.hoisted(() => ({
  hasSubagentEndedHook: true,
}));

const threadBindingMocks = vi.hoisted(() => ({
  unbindThreadBindingsBySessionKey: vi.fn((_params?: unknown) => []),
}));
const acpRuntimeMocks = vi.hoisted(() => ({
  cancel: vi.fn(async () => {}),
  close: vi.fn(async () => {}),
  getAcpRuntimeBackend: vi.fn(),
  requireAcpRuntimeBackend: vi.fn(),
}));
const acpManagerMocks = vi.hoisted(() => ({
  cancelSession: vi.fn(async () => {}),
  closeSession: vi.fn(async () => {}),
}));
const browserSessionTabMocks = vi.hoisted(() => ({
  closeTrackedBrowserTabsForSessions: vi.fn(async () => 0),
}));
const bundleMcpRuntimeMocks = vi.hoisted(() => ({
  disposeSessionMcpRuntime: vi.fn(async (_sessionId: string) => {}),
  disposeAllSessionMcpRuntimes: vi.fn(async () => {}),
}));

vi.mock("../../auto-reply/reply/queue.js", async () => {
  const actual = await vi.importActual<typeof import("../../auto-reply/reply/queue.js")>(
    "../../auto-reply/reply/queue.js",
  );
  return {
    ...actual,
    clearSessionQueues: sessionCleanupMocks.clearSessionQueues,
  };
});

vi.mock("../../auto-reply/reply/queue/cleanup.js", async () => {
  const actual = await vi.importActual<typeof import("../../auto-reply/reply/queue/cleanup.js")>(
    "../../auto-reply/reply/queue/cleanup.js",
  );
  return {
    ...actual,
    clearSessionQueues: sessionCleanupMocks.clearSessionQueues,
  };
});

vi.mock("../../auto-reply/reply/abort.js", async () => {
  const actual = await vi.importActual<typeof import("../../auto-reply/reply/abort.js")>(
    "../../auto-reply/reply/abort.js",
  );
  return {
    ...actual,
    stopSubagentsForRequester: sessionCleanupMocks.stopSubagentsForRequester,
  };
});

vi.mock("../../agents/bootstrap-cache.js", async () => {
  const actual = await vi.importActual<typeof import("../../agents/bootstrap-cache.js")>(
    "../../agents/bootstrap-cache.js",
  );
  return {
    ...actual,
    clearBootstrapSnapshot: bootstrapCacheMocks.clearBootstrapSnapshot,
  };
});

vi.mock("../../hooks/internal-hooks.js", async () => {
  const actual = await vi.importActual<typeof import("../../hooks/internal-hooks.js")>(
    "../../hooks/internal-hooks.js",
  );
  return {
    ...actual,
    hasInternalHookListeners: sessionHookMocks.hasInternalHookListeners,
    triggerInternalHook: sessionHookMocks.triggerInternalHook,
  };
});

vi.mock("../../plugins/hook-runner-global.js", async () => {
  const actual = await vi.importActual<typeof import("../../plugins/hook-runner-global.js")>(
    "../../plugins/hook-runner-global.js",
  );
  return {
    ...actual,
    getGlobalHookRunner: vi.fn(() => ({
      hasHooks: (hookName: string) =>
        (hookName === "subagent_ended" && subagentLifecycleHookState.hasSubagentEndedHook) ||
        (hookName === "before_reset" && beforeResetHookState.hasBeforeResetHook) ||
        (hookName === "session_end" && sessionLifecycleHookState.hasSessionEndHook) ||
        (hookName === "session_start" && sessionLifecycleHookState.hasSessionStartHook),
      runBeforeReset: beforeResetHookMocks.runBeforeReset,
      runSessionEnd: sessionLifecycleHookMocks.runSessionEnd,
      runSessionStart: sessionLifecycleHookMocks.runSessionStart,
      runSubagentEnded: subagentLifecycleHookMocks.runSubagentEnded,
    })),
  };
});

vi.mock("../../infra/outbound/session-binding-service.js", async () => {
  const actual = await vi.importActual<
    typeof import("../../infra/outbound/session-binding-service.js")
  >("../../infra/outbound/session-binding-service.js");
  return {
    ...actual,
    getSessionBindingService: () => ({
      ...actual.getSessionBindingService(),
      unbind: async (params: unknown) =>
        threadBindingMocks.unbindThreadBindingsBySessionKey(params),
    }),
  };
});

vi.mock("../../acp/runtime/registry.js", async () => {
  const actual = await vi.importActual<typeof import("../../acp/runtime/registry.js")>(
    "../../acp/runtime/registry.js",
  );
  return {
    ...actual,
    getAcpRuntimeBackend: acpRuntimeMocks.getAcpRuntimeBackend,
    requireAcpRuntimeBackend: (backendId?: string) => {
      const backend = acpRuntimeMocks.requireAcpRuntimeBackend(backendId);
      if (!backend) {
        throw new Error("missing mocked ACP backend");
      }
      return backend;
    },
  };
});

vi.mock("../../acp/control-plane/manager.js", () => ({
  getAcpSessionManager: () => ({
    cancelSession: acpManagerMocks.cancelSession,
    closeSession: acpManagerMocks.closeSession,
  }),
}));

vi.mock("../../plugin-sdk/browser-maintenance.js", () => ({
  closeTrackedBrowserTabsForSessions: browserSessionTabMocks.closeTrackedBrowserTabsForSessions,
  movePathToTrash: vi.fn(async () => {}),
}));

vi.mock("../../agents/agent-bundle-mcp-tools.js", () => ({
  disposeSessionMcpRuntime: bundleMcpRuntimeMocks.disposeSessionMcpRuntime,
  disposeAllSessionMcpRuntimes: bundleMcpRuntimeMocks.disposeAllSessionMcpRuntimes,
  retireSessionMcpRuntime: ({ sessionId }: { sessionId?: string | null }) =>
    sessionId
      ? bundleMcpRuntimeMocks.disposeSessionMcpRuntime(sessionId).then(() => true)
      : Promise.resolve(false),
}));

export function setupGatewaySessionsTestHarness() {
  installGatewayTestHooks({ scope: "suite" });

  let harness: GatewayServerHarness | undefined;
  let sharedSessionStoreDir: string | undefined;
  let sessionStoreCaseSeq = 0;

  beforeAll(async () => {
    harness = await startGatewayServerHarness();
    sharedSessionStoreDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
  });

  afterAll(async () => {
    await harness?.close();
    if (sharedSessionStoreDir) {
      await fs.rm(sharedSessionStoreDir, { recursive: true, force: true });
    }
  });

  beforeEach(async () => {
    const { clearConfigCache, clearRuntimeConfigSnapshot } = await getGatewayConfigModule();
    clearRuntimeConfigSnapshot();
    clearConfigCache();
    sessionCleanupMocks.clearSessionQueues.mockClear();
    sessionCleanupMocks.stopSubagentsForRequester.mockClear();
    bootstrapCacheMocks.clearBootstrapSnapshot.mockReset();
    sessionHookMocks.hasInternalHookListeners.mockReset();
    sessionHookMocks.hasInternalHookListeners.mockReturnValue(true);
    sessionHookMocks.triggerInternalHook.mockClear();
    beforeResetHookMocks.runBeforeReset.mockClear();
    beforeResetHookState.hasBeforeResetHook = false;
    sessionLifecycleHookMocks.runSessionEnd.mockClear();
    sessionLifecycleHookMocks.runSessionStart.mockClear();
    sessionLifecycleHookState.hasSessionEndHook = true;
    sessionLifecycleHookState.hasSessionStartHook = true;
    subagentLifecycleHookMocks.runSubagentEnded.mockClear();
    subagentLifecycleHookState.hasSubagentEndedHook = true;
    threadBindingMocks.unbindThreadBindingsBySessionKey.mockClear();
    resetSystemEventsForTest();
    acpRuntimeMocks.cancel.mockClear();
    acpRuntimeMocks.close.mockClear();
    acpRuntimeMocks.getAcpRuntimeBackend.mockReset();
    acpRuntimeMocks.getAcpRuntimeBackend.mockReturnValue(null);
    acpRuntimeMocks.requireAcpRuntimeBackend.mockReset();
    acpRuntimeMocks.requireAcpRuntimeBackend.mockImplementation((backendId?: string) =>
      acpRuntimeMocks.getAcpRuntimeBackend(backendId),
    );
    acpManagerMocks.cancelSession.mockClear();
    acpManagerMocks.closeSession.mockClear();
    browserSessionTabMocks.closeTrackedBrowserTabsForSessions.mockClear();
    browserSessionTabMocks.closeTrackedBrowserTabsForSessions.mockResolvedValue(0);
    bundleMcpRuntimeMocks.disposeSessionMcpRuntime.mockClear();
    bundleMcpRuntimeMocks.disposeSessionMcpRuntime.mockResolvedValue(undefined);
  });

  const requireHarness = () => {
    if (!harness) {
      throw new Error("Gateway sessions test harness was not started");
    }
    return harness;
  };

  const requireSharedSessionStoreDir = () => {
    if (!sharedSessionStoreDir) {
      throw new Error("Gateway sessions shared session store dir was not created");
    }
    return sharedSessionStoreDir;
  };

  const openClient = async (opts?: Parameters<typeof connectOk>[1]) =>
    await requireHarness().openClient(opts);

  async function createSessionStoreDir() {
    const dir = path.join(requireSharedSessionStoreDir(), `case-${sessionStoreCaseSeq++}`);
    await fs.mkdir(dir, { recursive: true });
    const storePath = path.join(dir, "sessions.json");
    testState.sessionStorePath = storePath;
    return { dir, storePath };
  }

  async function createSelectedGlobalSessionStore() {
    const { dir } = await createSessionStoreDir();
    const storeTemplate = path.join(dir, "{agentId}", "sessions.json");
    testState.sessionStorePath = storeTemplate;
    testState.sessionConfig = { scope: "global" };
    testState.agentsConfig = { list: [{ id: "main", default: true }, { id: "work" }] };
    return {
      dir,
      storeTemplate,
      mainStorePath: storeTemplate.replace("{agentId}", "main"),
      workStorePath: storeTemplate.replace("{agentId}", "work"),
    };
  }

  async function createConfiguredGlobalAgentSessionStore({
    writePrimeStore = false,
    withTranscripts = false,
  }: {
    writePrimeStore?: boolean;
    withTranscripts?: boolean;
  } = {}) {
    const { dir } = await createSessionStoreDir();
    const storeTemplate = path.join(dir, "{agentId}", "sessions.json");
    testState.sessionStorePath = storeTemplate;
    testState.sessionConfig = { scope: "global" };
    if (writePrimeStore) {
      await writeSessionStore({
        entries: {},
        storePath: path.join(dir, "prime-sessions.json"),
      });
    }

    const mainStorePath = storeTemplate.replace("{agentId}", "main");
    const workStorePath = storeTemplate.replace("{agentId}", "work");
    const mainTranscript = path.join(path.dirname(mainStorePath), "sess-main-global.jsonl");
    const workTranscript = path.join(path.dirname(workStorePath), "sess-work-global.jsonl");
    await fs.mkdir(path.dirname(mainStorePath), { recursive: true });
    await fs.mkdir(path.dirname(workStorePath), { recursive: true });
    if (withTranscripts) {
      await fs.writeFile(mainTranscript, "main one\nmain two\n", "utf-8");
      await fs.writeFile(workTranscript, "work one\nwork two\n", "utf-8");
    }
    await fs.writeFile(
      mainStorePath,
      JSON.stringify(
        {
          global: sessionStoreEntry(
            "sess-main-global",
            withTranscripts ? { sessionFile: mainTranscript } : undefined,
          ),
        },
        null,
        2,
      ),
      "utf-8",
    );
    await fs.writeFile(
      workStorePath,
      JSON.stringify(
        {
          global: sessionStoreEntry(
            "sess-work-global",
            withTranscripts
              ? { authProfileOverride: "github-copilot:work", sessionFile: workTranscript }
              : undefined,
          ),
        },
        null,
        2,
      ),
      "utf-8",
    );

    const configPath = process.env.OPENCLAW_CONFIG_PATH;
    if (!configPath) {
      throw new Error("OPENCLAW_CONFIG_PATH is required");
    }
    await fs.writeFile(
      configPath,
      `${JSON.stringify(
        {
          agents: { list: [{ id: "main", default: true }, { id: "work" }] },
          session: { scope: "global", store: storeTemplate },
        },
        null,
        2,
      )}\n`,
      "utf-8",
    );
    const { clearConfigCache, clearRuntimeConfigSnapshot, getRuntimeConfig } =
      await getGatewayConfigModule();
    clearRuntimeConfigSnapshot();
    clearConfigCache();

    return {
      clearConfigCache,
      clearRuntimeConfigSnapshot,
      configPath,
      getRuntimeConfig,
      mainStorePath,
      mainTranscript,
      workStorePath,
      workTranscript,
    };
  }

  async function resetConfiguredGlobalAgentSessionStore({
    clearConfigCache,
    clearRuntimeConfigSnapshot,
    configPath,
  }: {
    clearConfigCache: () => void;
    clearRuntimeConfigSnapshot: () => void;
    configPath: string;
  }) {
    testState.sessionStorePath = undefined;
    testState.sessionConfig = undefined;
    await fs.writeFile(configPath, "{}\n", "utf-8");
    clearRuntimeConfigSnapshot();
    clearConfigCache();
  }

  async function seedActiveMainSession() {
    const { dir, storePath } = await createSessionStoreDir();
    await writeSingleLineSession(dir, "sess-main", "hello");
    await writeSessionStore({
      entries: {
        main: sessionStoreEntry("sess-main"),
      },
    });
    return { dir, storePath };
  }

  return {
    createConfiguredGlobalAgentSessionStore,
    createSessionStoreDir,
    createSelectedGlobalSessionStore,
    getHarness: requireHarness,
    openClient,
    resetConfiguredGlobalAgentSessionStore,
    seedActiveMainSession,
  };
}

export async function writeSingleLineSession(dir: string, sessionId: string, content: string) {
  await fs.writeFile(
    path.join(dir, `${sessionId}.jsonl`),
    `${JSON.stringify({ role: "user", content })}\n`,
    "utf-8",
  );
}

export function sessionStoreEntry(sessionId: string, overrides: Partial<SessionEntry> = {}) {
  return {
    sessionId,
    updatedAt: Date.now(),
    ...overrides,
  };
}

export async function createCheckpointFixture(
  dir: string,
  options: { legacyPreCompactionSnapshot?: boolean } = { legacyPreCompactionSnapshot: true },
) {
  const { SessionManager } = await getSessionManagerModule();
  const session = SessionManager.create(dir, dir);
  const userMessage: UserMessage = {
    role: "user",
    content: "before compaction",
    timestamp: Date.now(),
  };
  const assistantMessage: AssistantMessage = {
    role: "assistant",
    content: [{ type: "text", text: "working on it" }],
    api: "responses",
    provider: "openai",
    model: "gpt-test",
    usage: {
      input: 1,
      output: 1,
      cacheRead: 0,
      cacheWrite: 0,
      totalTokens: 2,
      cost: {
        input: 0,
        output: 0,
        cacheRead: 0,
        cacheWrite: 0,
        total: 0,
      },
    },
    stopReason: "stop",
    timestamp: Date.now(),
  };
  session.appendMessage(userMessage);
  session.appendMessage(assistantMessage);
  const preCompactionLeafId = session.getLeafId();
  if (!preCompactionLeafId) {
    throw new Error("expected persisted session leaf before compaction");
  }
  const sessionFile = session.getSessionFile();
  if (!sessionFile) {
    throw new Error("expected persisted session file");
  }
  const legacyPreCompactionSnapshot = options.legacyPreCompactionSnapshot ?? true;
  const preCompactionSessionFile = legacyPreCompactionSnapshot
    ? path.join(dir, `${path.parse(sessionFile).name}.checkpoint-test.jsonl`)
    : undefined;
  if (preCompactionSessionFile) {
    fsSync.copyFileSync(sessionFile, preCompactionSessionFile);
  }
  const preCompactionSession = preCompactionSessionFile
    ? SessionManager.open(preCompactionSessionFile, dir)
    : undefined;
  session.appendCompaction("checkpoint summary", preCompactionLeafId, 123, { ok: true });
  const postCompactionLeafId = session.getLeafId();
  if (!postCompactionLeafId) {
    throw new Error("expected post-compaction leaf");
  }
  return {
    session,
    sessionId: session.getSessionId(),
    sessionFile,
    preCompactionSession,
    preCompactionSessionFile,
    preCompactionLeafId,
    postCompactionLeafId,
  };
}

export function expectActiveRunCleanup(
  requesterSessionKey: string,
  expectedQueueKeys: string[],
  sessionId: string,
) {
  expect(sessionCleanupMocks.stopSubagentsForRequester).toHaveBeenCalledWith({
    cfg: expect.any(Object),
    requesterSessionKey,
  });
  expect(sessionCleanupMocks.clearSessionQueues).toHaveBeenCalledTimes(1);
  const clearedKeys = (
    sessionCleanupMocks.clearSessionQueues.mock.calls as unknown as Array<[string[]]>
  )[0]?.[0];
  for (const key of expectedQueueKeys) {
    expect(clearedKeys).toContain(key);
  }
  expect(embeddedRunMock.abortCalls).toEqual([sessionId]);
  expect(embeddedRunMock.waitCalls).toEqual([sessionId]);
}

export async function getMainPreviewEntry(ws: import("ws").WebSocket) {
  const preview = await rpcReq<{
    previews: Array<{
      key: string;
      status: string;
      items: Array<{ role: string; text: string }>;
    }>;
  }>(ws, "sessions.preview", { keys: ["main"], limit: 3, maxChars: 120 });
  expect(preview.ok).toBe(true);
  const entry = preview.payload?.previews[0];
  expect(entry?.key).toBe("main");
  expect(entry?.status).toBe("ok");
  return entry;
}

type SessionsHandlers = Awaited<ReturnType<typeof getSessionsHandlers>>;

export async function directSessionReq<TPayload = unknown>(
  method: keyof SessionsHandlers,
  params: Record<string, unknown>,
  opts?: {
    context?: Record<string, unknown>;
    client?: Parameters<SessionsHandlers[keyof SessionsHandlers]>[0]["client"];
    isWebchatConnect?: Parameters<SessionsHandlers[keyof SessionsHandlers]>[0]["isWebchatConnect"];
    coercePayload?: (payload: unknown) => TPayload;
  },
): Promise<{ ok: boolean; payload?: TPayload; error?: { code?: string; message?: string } }> {
  const sessionsHandlers = await getSessionsHandlers();
  const { getRuntimeConfig } = await getGatewayConfigModule();
  let result:
    | { ok: boolean; payload?: TPayload; error?: { code?: string; message?: string } }
    | undefined;
  await sessionsHandlers[method]({
    req: {} as never,
    params,
    respond: (ok, payload, error) => {
      result = {
        ok,
        payload:
          payload === undefined
            ? undefined
            : opts?.coercePayload
              ? opts.coercePayload(payload)
              : (payload as TPayload),
        error,
      };
    },
    context: {
      broadcastToConnIds: vi.fn(),
      getSessionEventSubscriberConnIds: () => new Set<string>(),
      loadGatewayModelCatalog: async () => agentDiscoveryMock.models,
      getRuntimeConfig,
      ...opts?.context,
    } as never,
    client: opts?.client ?? null,
    isWebchatConnect: opts?.isWebchatConnect ?? (() => false),
  });
  if (!result) {
    throw new Error(`${method} did not respond`);
  }
  return result;
}

export function isInternalHookEvent(value: unknown): value is InternalHookEvent {
  if (!value || typeof value !== "object") {
    return false;
  }
  const candidate = value as Record<string, unknown>;
  return (
    typeof candidate.type === "string" &&
    typeof candidate.action === "string" &&
    typeof candidate.sessionKey === "string" &&
    Array.isArray(candidate.messages) &&
    typeof candidate.context === "object" &&
    candidate.context !== null
  );
}

export {
  bootstrapCacheMocks,
  sessionHookMocks,
  beforeResetHookMocks,
  sessionLifecycleHookMocks,
  subagentLifecycleHookMocks,
  beforeResetHookState,
  subagentLifecycleHookState,
  threadBindingMocks,
  acpRuntimeMocks,
  acpManagerMocks,
  browserSessionTabMocks,
  bundleMcpRuntimeMocks,
};
