import { describe, expect, it, vi } from "vitest";
import { HttpError } from "../../../shared/api/errors";
import { createBindingApi } from "../bindingApi";
import {
  isBindAttachmentsRequest,
  isBindAttachmentsResult,
  isUnbindAttachmentResult,
} from "../bindingTypes";

const headers = new Headers();
const ok = <T,>(data: T) => ({
  data,
  message: "success",
  requestId: "binding-request-id",
  httpStatus: 200,
  headers,
});

describe("F6 binding API and DTO contract", () => {
  it("uses the canonical bind path and exact request body", async () => {
    const client = {
      post: vi.fn().mockResolvedValue(ok({
        createdCount: 2,
        existingCount: 1,
        attachmentCount: 4,
        mainPlanRowVersion: 7,
      })),
      delete: vi.fn(),
    };
    const api = createBindingApi(client);
    const request = {
      attachmentIds: ["9", "8"],
      mainPlanRowVersion: 6,
    };
    const result = await api.bind("3", request);
    expect(client.post).toHaveBeenCalledWith(
      "main-plans/3/attachments/bind",
      request,
      { signal: undefined },
    );
    expect(result.requestId).toBe("binding-request-id");
    expect(result.data.createdCount).toBe(2);
  });

  it("uses attachmentId, not bindingId, in the unbind path", async () => {
    const client = {
      post: vi.fn(),
      delete: vi.fn().mockResolvedValue(ok({
        attachmentCount: 2,
        mainPlanRowVersion: 8,
      })),
    };
    const api = createBindingApi(client);
    await api.unbind("3", "9", 7);
    expect(client.delete).toHaveBeenCalledWith(
      "main-plans/3/attachments/9",
      { query: { mainPlanRowVersion: 7 }, signal: undefined },
    );
  });

  it("rejects number IDs before making requests", async () => {
    const client = { post: vi.fn(), delete: vi.fn() };
    const api = createBindingApi(client);
    await expect(api.bind(3 as unknown as string, {
      attachmentIds: ["9"],
      mainPlanRowVersion: 0,
    })).rejects.toThrow("字符串ID");
    await expect(api.unbind("3", 9 as unknown as string, 0))
      .rejects.toThrow("字符串ID");
    expect(client.post).not.toHaveBeenCalled();
    expect(client.delete).not.toHaveBeenCalled();
  });

  it("rejects unknown bind response fields and keeps requestId", async () => {
    const client = {
      post: vi.fn().mockResolvedValue(ok({
        createdCount: 1,
        existingCount: 0,
        attachmentCount: 1,
        mainPlanRowVersion: 1,
        bindingId: "10",
      })),
      delete: vi.fn(),
    };
    await expect(createBindingApi(client).bind("3", {
      attachmentIds: ["9"],
      mainPlanRowVersion: 0,
    })).rejects.toMatchObject({
      code: "INTERNAL_ERROR",
      requestId: "binding-request-id",
    });
  });

  it("rejects unknown unbind response fields", async () => {
    const client = {
      post: vi.fn(),
      delete: vi.fn().mockResolvedValue(ok({
        attachmentCount: 0,
        mainPlanRowVersion: 2,
        deleted: true,
      })),
    };
    await expect(createBindingApi(client).unbind("3", "9", 1))
      .rejects.toBeInstanceOf(HttpError);
  });

  it("strictly validates request IDs, fields and version", () => {
    expect(isBindAttachmentsRequest({
      attachmentIds: ["9", "8"],
      mainPlanRowVersion: 0,
    })).toBe(true);
    expect(isBindAttachmentsRequest({
      attachment_ids: ["9"],
      main_plan_row_version: 0,
    })).toBe(false);
    expect(isBindAttachmentsRequest({
      attachmentIds: [9],
      mainPlanRowVersion: 0,
    })).toBe(false);
    expect(isBindAttachmentsRequest({
      attachmentIds: [],
      mainPlanRowVersion: 0,
    })).toBe(false);
  });

  it("strictly validates bind and unbind integer results", () => {
    expect(isBindAttachmentsResult({
      createdCount: 0,
      existingCount: 2,
      attachmentCount: 2,
      mainPlanRowVersion: 4,
    })).toBe(true);
    expect(isBindAttachmentsResult({
      createdCount: -1,
      existingCount: 0,
      attachmentCount: 0,
      mainPlanRowVersion: 0,
    })).toBe(false);
    expect(isUnbindAttachmentResult({
      attachmentCount: 1,
      mainPlanRowVersion: 5,
    })).toBe(true);
    expect(isUnbindAttachmentResult({
      attachmentCount: 1,
      main_plan_row_version: 5,
    })).toBe(false);
  });
});
