import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { FeishuRuntime } from "../../src/feishu/client.js";
import {
  findMarkdownImagesOutsideCode,
  maskMarkdownImagesForStreaming,
  materializeAnswerSegments,
  uploadMessageImage,
} from "../../src/feishu/outboundImages.js";
import { buildAgentCard } from "../../src/feishu/card/builder.js";

const temps: string[] = [];

afterEach(async () => {
  await Promise.all(temps.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});

describe("outbound markdown image parsing", () => {
  it("finds image refs outside fenced code blocks", () => {
    const text = [
      "See diagram:",
      "![plot](https://cdn.example.com/a.png)",
      "",
      "```md",
      "![not-this](https://cdn.example.com/b.png)",
      "```",
      "![local](assets/x.png \"title\")",
    ].join("\n");

    const refs = findMarkdownImagesOutsideCode(text);
    expect(refs.map((ref) => ref.src)).toEqual([
      "https://cdn.example.com/a.png",
      "assets/x.png",
    ]);
  });

  it("masks image urls for streaming cards", () => {
    expect(maskMarkdownImagesForStreaming("before ![alt text](https://x/y.png) after")).toBe(
      "before alt text after",
    );
    expect(maskMarkdownImagesForStreaming("![](https://x/y.png)")).toBe("【图片】");
  });
});

describe("materializeAnswerSegments", () => {
  it("uploads remote and workspace images and builds card segments", async () => {
    const workspaceRoot = await mkdtemp(join(tmpdir(), "cph-img-root-"));
    temps.push(workspaceRoot);
    const workspaceDir = join(workspaceRoot, "project");
    await mkdir(join(workspaceDir, "assets"), { recursive: true });
    await writeFile(
      join(workspaceDir, "assets", "local.png"),
      Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
    );

    const imageCreate = vi
      .fn()
      .mockResolvedValueOnce({ image_key: "img_remote_1" })
      .mockResolvedValueOnce({ image_key: "img_local_1" });
    const rt = mockRuntime({ imageCreate });

    const fetchImpl = vi.fn(async () =>
      new Response(Buffer.from("remote-bytes"), {
        status: 200,
        headers: { "content-type": "image/png" },
      }),
    );

    const text = "Intro\n\n![remote](https://cdn.example.com/r.png)\n\nAnd local ![local](assets/local.png)\nDone.";
    const { segments, unresolved } = await materializeAnswerSegments(text, {
      rt,
      workspaceRoot,
      workspaceDir,
      fetchImpl: fetchImpl as unknown as typeof fetch,
    });

    expect(unresolved).toEqual([]);
    expect(imageCreate).toHaveBeenCalledTimes(2);
    expect(segments).toEqual([
      { type: "markdown", content: "Intro\n\n" },
      { type: "image", imgKey: "img_remote_1", alt: "remote" },
      { type: "markdown", content: "\n\nAnd local " },
      { type: "image", imgKey: "img_local_1", alt: "local" },
      { type: "markdown", content: "\nDone." },
    ]);

    const card = buildAgentCard({
      phase: "complete",
      text: "",
      contentSegments: segments,
      reasoningText: undefined,
      toolUseSteps: [],
      toolUseElapsedMs: undefined,
      isError: false,
      interrupted: false,
      runId: undefined,
    });
    expect(card.elements).toEqual(expect.arrayContaining([
      expect.objectContaining({ tag: "img", img_key: "img_remote_1" }),
      expect.objectContaining({ tag: "img", img_key: "img_local_1" }),
      expect.objectContaining({ tag: "markdown", content: "Intro\n\n" }),
    ]));
  });

  it("rejects private remote hosts", async () => {
    const imageCreate = vi.fn();
    const rt = mockRuntime({ imageCreate });
    const fetchImpl = vi.fn();
    const { segments, unresolved } = await materializeAnswerSegments(
      "![x](http://127.0.0.1/secret.png)",
      { rt, fetchImpl: fetchImpl as unknown as typeof fetch },
    );
    expect(fetchImpl).not.toHaveBeenCalled();
    expect(imageCreate).not.toHaveBeenCalled();
    expect(unresolved).toEqual(["http://127.0.0.1/secret.png"]);
    expect(segments).toEqual([{ type: "markdown", content: "x" }]);
  });
});

describe("uploadMessageImage", () => {
  it("returns image_key from Feishu upload", async () => {
    const imageCreate = vi.fn(async () => ({ data: { image_key: "img_nested" } }));
    const rt = mockRuntime({ imageCreate });
    await expect(uploadMessageImage(rt, Buffer.from("png"))).resolves.toBe("img_nested");
    expect(imageCreate).toHaveBeenCalledWith({
      data: { image_type: "message", image: Buffer.from("png") },
    });
  });
});

function mockRuntime(options: {
  readonly imageCreate?: (payload: unknown) => Promise<unknown>;
}): FeishuRuntime {
  return {
    client: {
      im: {
        v1: {
          image: {
            create: options.imageCreate ?? vi.fn(),
          },
          message: {
            create: vi.fn(),
            reply: vi.fn(),
            patch: vi.fn(),
          },
        },
      },
    } as unknown as FeishuRuntime["client"],
    logger: {
      warn: vi.fn(),
      error: vi.fn(),
      info: vi.fn(),
      debug: vi.fn(),
      child: vi.fn(),
      fatal: vi.fn(),
      trace: vi.fn(),
      silent: vi.fn(),
      level: "info",
    } as unknown as FeishuRuntime["logger"],
  };
}
