import { Readable } from "node:stream";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BASE_URL, ChatClient, DifyClient, WorkflowClient, routes } from "./index";

const stubFetch = (): ReturnType<typeof vi.fn> => {
  const fetchMock = vi.fn();
  vi.stubGlobal("fetch", fetchMock);
  return fetchMock;
};

const jsonResponse = (body: unknown, init: ResponseInit = {}): Response =>
  new Response(JSON.stringify(body), {
    status: 200,
    ...init,
    headers: {
      "content-type": "application/json",
      ...init.headers,
    },
  });

describe("Client", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("creates a client with default settings", () => {
    const difyClient = new DifyClient("test");

    expect(difyClient.getHttpClient().getSettings()).toMatchObject({
      apiKey: "test",
      baseUrl: BASE_URL,
      timeout: 60,
    });
  });

  it("updates the api key", () => {
    const difyClient = new DifyClient("test");
    difyClient.updateApiKey("test2");

    expect(difyClient.getHttpClient().getSettings().apiKey).toBe("test2");
  });
});

describe("Send Requests", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("makes a successful request to the application parameter route", async () => {
    const fetchMock = stubFetch();
    const difyClient = new DifyClient("test");
    const method = "GET";
    const endpoint = routes.application.url();

    fetchMock.mockResolvedValueOnce(jsonResponse("response"));

    const response = await difyClient.sendRequest(method, endpoint);

    expect(response).toMatchObject({
      status: 200,
      data: "response",
      headers: {
        "content-type": "application/json",
      },
    });
    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(`${BASE_URL}${endpoint}`);
    expect(init.method).toBe(method);
    expect(init.headers).toMatchObject({
      Authorization: "Bearer test",
      "User-Agent": "dify-client-node",
    });
  });

  it("uses the getMeta route configuration", async () => {
    const fetchMock = stubFetch();
    const difyClient = new DifyClient("test");
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));

    await difyClient.getMeta("end-user");

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(`${BASE_URL}${routes.getMeta.url()}?user=end-user`);
    expect(init.method).toBe(routes.getMeta.method);
    expect(init.headers).toMatchObject({
      Authorization: "Bearer test",
    });
  });
});

describe("File uploads", () => {
  const OriginalFormData = globalThis.FormData;

  beforeAll(() => {
    globalThis.FormData = class FormDataMock extends Readable {
      constructor() {
        super();
      }

      override _read() {}

      append() {}

      getHeaders() {
        return {
          "content-type": "multipart/form-data; boundary=test",
        };
      }
    } as unknown as typeof FormData;
  });

  afterAll(() => {
    globalThis.FormData = OriginalFormData;
  });

  beforeEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("does not override multipart boundary headers for legacy FormData", async () => {
    const fetchMock = stubFetch();
    const difyClient = new DifyClient("test");
    const form = new globalThis.FormData();
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));

    await difyClient.fileUpload(form, "end-user");

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(`${BASE_URL}${routes.fileUpload.url()}`);
    expect(init.method).toBe(routes.fileUpload.method);
    expect(init.headers).toMatchObject({
      Authorization: "Bearer test",
      "content-type": "multipart/form-data; boundary=test",
    });
    expect(init.body).not.toBe(form);
    expect((init as RequestInit & { duplex?: string }).duplex).toBe("half");
  });
});

describe("Workflow client", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("uses tasks stop path for workflow stop", async () => {
    const fetchMock = stubFetch();
    const workflowClient = new WorkflowClient("test");
    fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success" }));

    await workflowClient.stop("task-1", "end-user");

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(`${BASE_URL}${routes.stopWorkflow.url("task-1")}`);
    expect(init.method).toBe(routes.stopWorkflow.method);
    expect(init.headers).toMatchObject({
      Authorization: "Bearer test",
      "Content-Type": "application/json",
    });
    expect(init.body).toBe(JSON.stringify({ user: "end-user" }));
  });

  it("maps workflow log filters to service api params", async () => {
    const fetchMock = stubFetch();
    const workflowClient = new WorkflowClient("test");
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));

    await workflowClient.getLogs({
      createdAtAfter: "2024-01-01T00:00:00Z",
      createdAtBefore: "2024-01-02T00:00:00Z",
      createdByEndUserSessionId: "sess-1",
      createdByAccount: "acc-1",
      page: 2,
      limit: 10,
    });

    const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
    const parsedUrl = new URL(url);
    expect(parsedUrl.origin + parsedUrl.pathname).toBe(`${BASE_URL}/workflows/logs`);
    expect(parsedUrl.searchParams.get("created_at__before")).toBe(
      "2024-01-02T00:00:00Z"
    );
    expect(parsedUrl.searchParams.get("created_at__after")).toBe(
      "2024-01-01T00:00:00Z"
    );
    expect(parsedUrl.searchParams.get("created_by_end_user_session_id")).toBe(
      "sess-1"
    );
    expect(parsedUrl.searchParams.get("created_by_account")).toBe("acc-1");
    expect(parsedUrl.searchParams.get("page")).toBe("2");
    expect(parsedUrl.searchParams.get("limit")).toBe("10");
  });
});

describe("Chat client", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("places user in query for suggested messages", async () => {
    const fetchMock = stubFetch();
    const chatClient = new ChatClient("test");
    fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success", data: [] }));

    await chatClient.getSuggested("msg-1", "end-user");

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(`${BASE_URL}${routes.getSuggested.url("msg-1")}?user=end-user`);
    expect(init.method).toBe(routes.getSuggested.method);
    expect(init.headers).toMatchObject({
      Authorization: "Bearer test",
    });
  });

  it("uses last_id when listing conversations", async () => {
    const fetchMock = stubFetch();
    const chatClient = new ChatClient("test");
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));

    await chatClient.getConversations("end-user", "last-1", 10);

    const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(`${BASE_URL}${routes.getConversations.url()}?user=end-user&last_id=last-1&limit=10`);
  });

  it("lists app feedbacks without user params", async () => {
    const fetchMock = stubFetch();
    const chatClient = new ChatClient("test");
    fetchMock.mockResolvedValueOnce(jsonResponse({ data: [] }));

    await chatClient.getAppFeedbacks(1, 20);

    const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(`${BASE_URL}/app/feedbacks?page=1&limit=20`);
  });
});
