import { afterEach, describe, expect, it, vi } from "vitest";
import type { ConvexHttpClient } from "convex/browser";

vi.mock("@/lib/auth/authContext", () => ({
  HttpError: class HttpError extends Error {
    status: number;

    constructor(status: number, message: string) {
      super(message);
      this.status = status;
    }
  },
  requireAuthContext: vi.fn(),
}));

vi.mock("@/lib/api-utils", () => ({
  apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({
    message,
    status,
    details,
  })),
}));

import {
  DocumentBridgeError,
  assertDocumentId,
  assertBlockId,
  assertNextBlock,
  assertOptionsPatch,
  assertStats,
  assertTitle,
  buildDocumentBridgeMutationRequest,
  buildDocumentBridgeContextWithActor,
  buildDocumentCommandEnvelope,
  buildDocumentQueryEnvelope,
  type BridgeContext,
} from "@/lib/documents/bridge";
import { executeMediaAssetWritebackBridgeCommand } from "@/lib/documents/media-asset-command-adapter";
import { executeMetadataBridgeCommand } from "@/lib/documents/metadata-command-adapter";
import { executePageLifecycleBridgeCommand } from "@/lib/documents/page-command-adapter";
import { executeSaveBridgeCommand } from "@/lib/documents/save-command-adapter";
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";

vi.mock("@/lib/convex/route", () => ({
  getAuthedConvexClient: vi.fn(),
}));

vi.mock("@/lib/documents/bridge-log", () => ({
  recordBridgeCommandArtifacts: vi.fn(),
  recordBridgeCommandFailureArtifacts: vi.fn(),
}));

vi.mock("@/lib/documents/rust-runtime", () => ({
  resolveRustBridgeCommandPlan: vi.fn(),
  executeRustBridgeMutationTransport: vi.fn(),
  resolveRustBridgeQueryPlan: vi.fn(),
  executeRustBridgeQueryTransport: vi.fn(),
}));

const mockContext: BridgeContext = {
  deploymentId: null,
  projectId: null,
  workspaceId: "ws_1",
  requestId: "req_1",
  traceId: "trace_1",
  actor: {
    actorType: "user",
    actorId: "user_1",
    sessionId: "sess_1",
  },
  source: {
    channel: "next-route",
    client: "vitest",
  },
  tenantId: null,
  authToken: null,
  idempotencyKey: "idem_1",
  validateOnly: false,
  dryRun: false,
};

afterEach(() => {
  vi.clearAllMocks();
});

describe("documents bridge helpers", () => {
  it("assertDocumentId returns trimmed id", () => {
    expect(assertDocumentId("  doc_1  ")).toBe("doc_1");
  });

  it("assertDocumentId throws on empty value", () => {
    expect(() => assertDocumentId("   ")).toThrow(DocumentBridgeError);
  });

  it("assertTitle normalizes empty title", () => {
    expect(assertTitle("   ")).toBe("无标题");
  });

  it("assertBlockId returns trimmed block id", () => {
    expect(assertBlockId("  blk_1  ")).toBe("blk_1");
  });

  it("assertNextBlock accepts plain object", () => {
    expect(() => assertNextBlock({ id: "blk_1" })).not.toThrow();
  });

  it("assertStats accepts finite numeric stats", () => {
    expect(() =>
      assertStats({
        wordCount: 1,
        characterCount: 2,
        blockCount: 3,
        todoTotal: 4,
        todoDone: 5,
      }),
    ).not.toThrow();
  });

  it("assertOptionsPatch accepts stable page options patch", () => {
    expect(() =>
      assertOptionsPatch({
        showToc: true,
        layoutDensity: "compact",
        embedDefaultBlockId: null,
      }),
    ).not.toThrow();
  });

  it("buildDocumentCommandEnvelope keeps context flags", () => {
    const envelope = buildDocumentCommandEnvelope({
      name: "documents.save",
      payload: { documentId: "doc_1" },
      context: mockContext,
      target: { workspaceId: "ws_1", pageId: "doc_1" },
    });

    expect(envelope.name).toBe("documents.save");
    expect(envelope.actor.actorId).toBe("user_1");
    expect(envelope.idempotencyKey).toBe("idem_1");
    expect(envelope.target?.pageId).toBe("doc_1");
  });

  it("buildDocumentQueryEnvelope keeps payload", () => {
    const envelope = buildDocumentQueryEnvelope({
      name: "documents.content.get",
      payload: { documentId: "doc_1" },
    });

    expect(envelope.payload).toEqual({ documentId: "doc_1" });
  });

  it("buildDocumentBridgeMutationRequest builds title update runtime request", () => {
    const envelope = buildDocumentCommandEnvelope({
      name: "documents.title.update",
      payload: {
        documentId: "doc_1",
        workspaceId: "ws_1",
        title: "新标题",
      },
      context: mockContext,
      target: { workspaceId: "ws_1", pageId: "doc_1" },
    });

    const request = buildDocumentBridgeMutationRequest({
      context: mockContext,
      envelope,
      mapConvexArgs: (payload) => ({
        id: payload.documentId,
        title: payload.title,
      }),
    });

    expect(request.functionName).toBe("documents:updateTitle");
    expect(request.workspaceId).toBe("ws_1");
    expect(request.args).toEqual({
      id: "doc_1",
      title: "新标题",
    });
    expect(JSON.parse(request.payloadJson)).toEqual({
      kind: "command",
      name: "documents.title.update",
      request_id: "req_1",
      trace_id: "trace_1",
      deployment_id: null,
      project_id: null,
      workspace_id: "ws_1",
      tenant_id: null,
      idempotency_key: "idem_1",
      actor: {
        type: "user",
        id: "user_1",
        session_id: "sess_1",
      },
      source: {
        channel: "next-route",
        client: "vitest",
      },
    });
  });

  it("buildDocumentBridgeMutationRequest builds documents.save runtime request", () => {
    const payload = buildDocumentSavePayload({
      documentId: "doc_1",
      workspaceId: "ws_1",
      revision: 7,
      content: [{ id: "block_1" }],
      conflictDetectionKey: "conflict_1",
      snapshotCapturedAt: "2026-04-14T14:30:00.000Z",
      blockCount: 1,
    });
    const envelope = buildDocumentCommandEnvelope({
      name: "documents.save",
      payload,
      context: mockContext,
      target: { workspaceId: "ws_1", pageId: "doc_1" },
    });

    const request = buildDocumentBridgeMutationRequest({
      context: mockContext,
      envelope,
      mapConvexArgs: (nextPayload) => ({
        id: nextPayload.documentId,
        content: nextPayload.content,
        expectedRevision: nextPayload.revision,
        conflictDetectionKey: nextPayload.conflictDetectionKey,
      }),
    });

    expect(request.functionName).toBe("documents:updateContent");
    expect(request.workspaceId).toBe("ws_1");
    expect(request.args).toEqual({
      id: "doc_1",
      content: [{ id: "block_1" }],
      expectedRevision: 7,
      conflictDetectionKey: "conflict_1",
    });
    expect(JSON.parse(request.payloadJson)).toMatchObject({
      kind: "command",
      name: "documents.save",
      workspace_id: "ws_1",
      request_id: "req_1",
      trace_id: "trace_1",
    });
  });

  it("buildDocumentBridgeMutationRequest builds documents.embed runtime request", () => {
    const payload = {
      ...buildDocumentSavePayload({
        documentId: "doc_2",
        workspaceId: "ws_1",
        revision: 3,
        content: [{ id: "block_2", type: "pageReference" }],
        conflictDetectionKey: "conflict_3",
        blockCount: 1,
      }),
      sourceDocumentId: "doc_1",
      targetDocumentId: "doc_2",
      anchorBlockId: "anchor_1",
    };
    const envelope = buildDocumentCommandEnvelope({
      name: "documents.embed",
      payload,
      context: mockContext,
      target: { workspaceId: "ws_1", pageId: "doc_2" },
    });

    const request = buildDocumentBridgeMutationRequest({
      context: mockContext,
      envelope,
      mapConvexArgs: (nextPayload) => ({
        id: nextPayload.documentId,
        content: nextPayload.content,
        expectedRevision: nextPayload.revision,
        conflictDetectionKey: nextPayload.conflictDetectionKey,
        sourceDocumentId: nextPayload.sourceDocumentId,
        targetDocumentId: nextPayload.targetDocumentId,
        anchorBlockId: nextPayload.anchorBlockId,
      }),
    });

    expect(request.functionName).toBe("documents:updateContent");
    expect(request.workspaceId).toBe("ws_1");
    expect(request.args).toEqual({
      id: "doc_2",
      content: [{ id: "block_2", type: "pageReference" }],
      expectedRevision: 3,
      conflictDetectionKey: "conflict_3",
      sourceDocumentId: "doc_1",
      targetDocumentId: "doc_2",
      anchorBlockId: "anchor_1",
    });
    expect(JSON.parse(request.payloadJson)).toMatchObject({
      kind: "command",
      name: "documents.embed",
      workspace_id: "ws_1",
      request_id: "req_1",
      trace_id: "trace_1",
    });
  });

  it("buildDocumentBridgeMutationRequest builds media asset writeback runtime request", () => {
    const envelope = buildDocumentCommandEnvelope({
      name: "media.assets.replace_storage",
      payload: {
        assetId: "asset_1",
        documentId: "doc_1",
        workspaceId: "ws_1",
        storageId: "storage_1",
        userId: "user_1",
      },
      context: mockContext,
      target: { workspaceId: "ws_1", pageId: "doc_1" },
    });

    const request = buildDocumentBridgeMutationRequest({
      context: mockContext,
      envelope,
      mapConvexArgs: (payload) => ({
        userId: payload.userId,
        id: payload.assetId,
        storageId: payload.storageId,
      }),
    });

    expect(request.functionName).toBe("mediaAssets:replaceStorageFromUpload");
    expect(request.workspaceId).toBe("ws_1");
    expect(request.args).toEqual({
      userId: "user_1",
      id: "asset_1",
      storageId: "storage_1",
    });
    expect(JSON.parse(request.payloadJson)).toMatchObject({
      kind: "command",
      name: "media.assets.replace_storage",
      workspace_id: "ws_1",
      request_id: "req_1",
      trace_id: "trace_1",
    });
  });

  it("buildDocumentBridgeMutationRequest builds page lifecycle runtime request", () => {
    const envelope = buildDocumentCommandEnvelope({
      name: "documents.create",
      payload: {
        documentId: "doc_1",
        workspaceId: "ws_1",
        parentId: "parent_1",
        title: "无标题",
        accessScope: "private",
        content: [],
      },
      context: mockContext,
      target: { workspaceId: "ws_1", pageId: "doc_1" },
    });

    const request = buildDocumentBridgeMutationRequest({
      context: mockContext,
      envelope,
      mapConvexArgs: (payload) => ({
        id: payload.documentId,
        workspaceId: payload.workspaceId,
        parentId: payload.parentId,
        title: payload.title,
        accessScope: payload.accessScope,
        content: payload.content,
      }),
    });

    expect(request.functionName).toBe("documents:createWithParentReference");
    expect(request.args).toEqual({
      id: "doc_1",
      workspaceId: "ws_1",
      parentId: "parent_1",
      title: "无标题",
      accessScope: "private",
      content: [],
    });
  });

  it("buildDocumentBridgeContextWithActor keeps explicit actor and source", () => {
    const request = new Request("http://127.0.0.1:3001/api/onlyoffice/callback", {
      headers: {
        "x-request-id": "req_oo_1",
        "x-trace-id": "trace_oo_1",
      },
    });

    const context = buildDocumentBridgeContextWithActor({
      request,
      workspaceId: "ws_1",
      actor: {
        actorType: "service",
        actorId: "user_1",
        sessionId: "onlyoffice-callback",
      },
      source: {
        channel: "onlyoffice-callback",
        client: "onlyoffice-document-server",
      },
      idempotencyKey: "asset_1",
    });

    expect(context.requestId).toBe("req_oo_1");
    expect(context.traceId).toBe("trace_oo_1");
    expect(context.actor).toEqual({
      actorType: "service",
      actorId: "user_1",
      sessionId: "onlyoffice-callback",
    });
    expect(context.source).toEqual({
      channel: "onlyoffice-callback",
      client: "onlyoffice-document-server",
    });
    expect(context.idempotencyKey).toBe("asset_1");
  });

  it("executeMetadataBridgeCommand routes title update through adapter", async () => {
    const { getAuthedConvexClient } = await import("@/lib/convex/route");
    const {
      resolveRustBridgeCommandPlan,
      executeRustBridgeMutationTransport,
    } = await import("@/lib/documents/rust-runtime");
    vi.mocked(getAuthedConvexClient).mockResolvedValue({
      auth: { userId: "user_1" },
      client: {
        mutation: vi.fn(),
      } as unknown as ConvexHttpClient,
    });
    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({
      kind: "command",
      commandName: "documents.title.update",
      commandId: "cmd_title_1",
      functionName: "documents:updateTitle",
      workspaceId: "ws_1",
      requestId: "req_1",
      traceId: "trace_1",
      actorId: "user_1",
      idempotencyKey: "idem_1",
      payloadJson: "{\"kind\":\"command\"}",
      argsJson: {
        id: "doc_1",
        title: "新标题",
      },
    });
    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({ ok: true });

    const result = await executeMetadataBridgeCommand({
      context: mockContext,
      envelope: buildDocumentCommandEnvelope({
        name: "documents.title.update",
        payload: {
          documentId: "doc_1",
          workspaceId: "ws_1",
          title: "新标题",
        },
        context: mockContext,
        target: { workspaceId: "ws_1", pageId: "doc_1" },
      }),
    });

    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({
      context: mockContext,
      envelope: expect.objectContaining({
        name: "documents.title.update",
      }),
    });
    expect(executeRustBridgeMutationTransport).toHaveBeenCalledWith({
      client: expect.any(Object),
      plan: expect.objectContaining({
        functionName: "documents:updateTitle",
        argsJson: {
          id: "doc_1",
          title: "新标题",
        },
      }),
    });
    expect(result.commandName).toBe("documents.title.update");
  });

  it("executeMetadataBridgeCommand routes options update through adapter", async () => {
    const mutation = vi.fn().mockResolvedValue({ ok: true });
    const { getAuthedConvexClient } = await import("@/lib/convex/route");
    vi.mocked(getAuthedConvexClient).mockResolvedValue({
      auth: { userId: "user_1" },
      client: {
        mutation,
      } as unknown as ConvexHttpClient,
    });

    const result = await executeMetadataBridgeCommand({
      context: mockContext,
      envelope: buildDocumentCommandEnvelope({
        name: "documents.options.update",
        payload: {
          documentId: "doc_1",
          workspaceId: "ws_1",
          options: {
            showToc: true,
            layoutDensity: "compact",
            embedDefaultBlockId: null,
          },
        },
        context: mockContext,
        target: { workspaceId: "ws_1", pageId: "doc_1" },
      }),
    });

    expect(mutation).toHaveBeenCalledTimes(1);
    expect(mutation.mock.calls[0]?.[1]).toEqual({
      id: "doc_1",
      options: {
        wideLayout: undefined,
        smallText: undefined,
        showHeadingNumbers: undefined,
        showToc: true,
        showStructure: undefined,
        protectEditing: undefined,
        showWordCount: undefined,
        collapseBacklinks: undefined,
        pageFont: undefined,
        layoutDensity: "compact",
        hideChildPages: undefined,
        showBlockRefCount: undefined,
        embedDefaultBlockId: null,
      },
    });
    expect(result.commandName).toBe("documents.options.update");
  });

  it("executeSaveBridgeCommand routes documents.save through adapter", async () => {
    const { getAuthedConvexClient } = await import("@/lib/convex/route");
    const { recordBridgeCommandArtifacts } = await import("@/lib/documents/bridge-log");
    const {
      resolveRustBridgeCommandPlan,
      executeRustBridgeMutationTransport,
    } = await import("@/lib/documents/rust-runtime");
    vi.mocked(getAuthedConvexClient).mockResolvedValue({
      auth: { userId: "user_1" },
      client: {
        mutation: vi.fn(),
      } as unknown as ConvexHttpClient,
    });
    vi.mocked(recordBridgeCommandArtifacts).mockResolvedValue(undefined);
    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({
      kind: "command",
      commandName: "documents.save",
      commandId: "cmd_save_1",
      functionName: "documents:updateContent",
      workspaceId: "ws_1",
      requestId: "req_1",
      traceId: "trace_1",
      actorId: "user_1",
      idempotencyKey: "idem_1",
      payloadJson: "{\"kind\":\"command\"}",
      argsJson: {
        id: "doc_1",
        content: [{ id: "block_1" }],
        expectedRevision: 7,
        conflictDetectionKey: "conflict_1",
      },
    });
    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({
      revision: 7,
      conflict_detection_key: "conflict_1",
    });
    const previousBridgeArtifactCalls = vi.mocked(recordBridgeCommandArtifacts).mock.calls.length;

    const payload = buildDocumentSavePayload({
      documentId: "doc_1",
      workspaceId: "ws_1",
      revision: 7,
      content: [{ id: "block_1" }],
      conflictDetectionKey: "conflict_1",
      snapshotCapturedAt: "2026-04-14T14:30:00.000Z",
      blockCount: 1,
    });

    const result = await executeSaveBridgeCommand({
      context: mockContext,
      envelope: buildDocumentCommandEnvelope({
        name: "documents.save",
        payload,
        context: mockContext,
        target: { workspaceId: "ws_1", pageId: "doc_1" },
      }),
    });

    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({
      context: mockContext,
      envelope: expect.objectContaining({
        name: "documents.save",
      }),
    });
    expect(executeRustBridgeMutationTransport).toHaveBeenCalledWith({
      client: expect.any(Object),
      plan: expect.objectContaining({
        functionName: "documents:updateContent",
        argsJson: {
          id: "doc_1",
          content: [{ id: "block_1" }],
          expectedRevision: 7,
          conflictDetectionKey: "conflict_1",
        },
      }),
    });
    expect(vi.mocked(recordBridgeCommandArtifacts).mock.calls.length).toBe(
      previousBridgeArtifactCalls + 1,
    );
    expect(recordBridgeCommandArtifacts).toHaveBeenCalledWith({
      context: mockContext,
      envelope: expect.objectContaining({
        name: "documents.save",
        target: { workspaceId: "ws_1", pageId: "doc_1" },
        payload,
      }),
    });
    expect(result.requestId).toBe("req_1");
    expect(result.traceId).toBe("trace_1");
    expect(result.commandName).toBe("documents.save");
  });

  it("executeSaveBridgeCommand 将冲突错误归一为 bridge rejected", async () => {
    const { getAuthedConvexClient } = await import("@/lib/convex/route");
    const {
      resolveRustBridgeCommandPlan,
      executeRustBridgeMutationTransport,
    } = await import("@/lib/documents/rust-runtime");
    vi.mocked(getAuthedConvexClient).mockResolvedValue({
      auth: { userId: "user_1" },
      client: {
        mutation: vi.fn(),
      } as unknown as ConvexHttpClient,
    });
    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({
      kind: "command",
      commandName: "documents.save",
      commandId: "cmd_save_1",
      functionName: "documents:updateContent",
      workspaceId: "ws_1",
      requestId: "req_1",
      traceId: "trace_1",
      actorId: "user_1",
      idempotencyKey: "idem_1",
      payloadJson: "{\"kind\":\"command\"}",
      argsJson: {
        id: "doc_1",
        content: [{ id: "block_1" }],
        expectedRevision: 7,
        conflictDetectionKey: "conflict_1",
      },
    });
    vi.mocked(executeRustBridgeMutationTransport).mockRejectedValue(
      new Error("正文内容已变更，请刷新后重试"),
    );

    const payload = buildDocumentSavePayload({
      documentId: "doc_1",
      workspaceId: "ws_1",
      revision: 7,
      content: [{ id: "block_1" }],
      conflictDetectionKey: "conflict_1",
    });

    await expect(
      executeSaveBridgeCommand({
        context: mockContext,
        envelope: buildDocumentCommandEnvelope({
          name: "documents.save",
          payload,
          context: mockContext,
          target: { workspaceId: "ws_1", pageId: "doc_1" },
        }),
      }),
    ).rejects.toMatchObject({
      name: "DocumentBridgeError",
      status: 409,
      code: "REJECTED",
    });
  });

  it("executePageLifecycleBridgeCommand routes page mutation through rust runtime", async () => {
    const { getAuthedConvexClient } = await import("@/lib/convex/route");
    const { recordBridgeCommandArtifacts } = await import("@/lib/documents/bridge-log");
    const {
      resolveRustBridgeCommandPlan,
      executeRustBridgeMutationTransport,
    } = await import("@/lib/documents/rust-runtime");
    vi.mocked(getAuthedConvexClient).mockResolvedValue({
      auth: { userId: "user_1" },
      client: {
        mutation: vi.fn(),
      } as unknown as ConvexHttpClient,
    });
    vi.mocked(recordBridgeCommandArtifacts).mockResolvedValue(undefined);
    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({
      kind: "command",
      commandName: "documents.create",
      commandId: "cmd_create_1",
      functionName: "documents:createWithParentReference",
      workspaceId: "ws_1",
      requestId: "req_1",
      traceId: "trace_1",
      actorId: "user_1",
      idempotencyKey: "idem_1",
      payloadJson: "{\"kind\":\"command\"}",
      argsJson: {
        id: "doc_1",
        workspaceId: "ws_1",
        parentId: "parent_1",
        title: "无标题",
        accessScope: "private",
        content: [],
      },
    });
    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({
      id: "doc_1",
      title: "无标题",
    });

    const result = await executePageLifecycleBridgeCommand({
      context: mockContext,
      envelope: buildDocumentCommandEnvelope({
        name: "documents.create",
        payload: {
          documentId: "doc_1",
          workspaceId: "ws_1",
          parentId: "parent_1",
          title: "无标题",
          accessScope: "private",
          content: [],
        },
        context: mockContext,
        target: { workspaceId: "ws_1", pageId: "doc_1" },
      }),
    });

    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({
      context: mockContext,
      envelope: expect.objectContaining({
        name: "documents.create",
      }),
    });
    expect(executeRustBridgeMutationTransport).toHaveBeenCalledWith({
      client: expect.any(Object),
      plan: expect.objectContaining({
        functionName: "documents:createWithParentReference",
      }),
    });
    expect(recordBridgeCommandArtifacts).toHaveBeenCalledWith({
      client: expect.any(Object),
      context: mockContext,
      envelope: expect.objectContaining({
        name: "documents.create",
      }),
    });
    expect(result.result).toEqual({
      id: "doc_1",
      title: "无标题",
    });
  });

  it("executeMediaAssetWritebackBridgeCommand routes callback writeback through adapter", async () => {
    const mutation = vi.fn().mockResolvedValue({ ok: true, fileUrl: "https://example.com/file.docx" });
    const { recordBridgeCommandArtifacts } = await import("@/lib/documents/bridge-log");
    vi.mocked(recordBridgeCommandArtifacts).mockResolvedValue(undefined);

    await executeMediaAssetWritebackBridgeCommand({
      client: {
        mutation,
      } as unknown as ConvexHttpClient,
      context: mockContext,
      envelope: buildDocumentCommandEnvelope({
        name: "media.assets.replace_storage",
        payload: {
          assetId: "asset_1",
          documentId: "doc_1",
          workspaceId: "ws_1",
          storageId: "storage_1",
          userId: "user_1",
        },
        context: mockContext,
        target: { workspaceId: "ws_1", pageId: "doc_1" },
      }),
    });

    expect(mutation).toHaveBeenCalledTimes(1);
    expect(mutation.mock.calls[0]?.[1]).toEqual({
      userId: "user_1",
      id: "asset_1",
      storageId: "storage_1",
    });
    expect(recordBridgeCommandArtifacts).toHaveBeenCalledWith({
      client: expect.any(Object),
      context: mockContext,
      envelope: expect.objectContaining({
        name: "media.assets.replace_storage",
      }),
    });
  });
});
