/**
 * pi-extension/tasklens.ts 的测试。
 *
 * 两类用例:
 * 1) 用假的 ExtensionAPI 断言注册契约(17 个工具 + 4 个命令 + schema 形状 + 参数校验不抛异常)。
 * 2) 用真实 `tasklens-cli` 驱动临时知识库,跑通 vault_task_brief / vault_toggle_checkbox 等
 *    真实读写;CLI 不存在时整组 skip,不会失败。
 */

import { existsSync } from "node:fs";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import process from "node:process";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

import tasklensExtension, { VAULT_TOOLS } from "./tasklens";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

interface ToolResultLike {
  content: Array<{ type: string; text: string }>;
  details: unknown;
}

interface ToolDefinitionLike {
  name: string;
  label: string;
  description: string;
  parameters: { type?: string };
  execute(
    toolCallId: string,
    params: unknown,
    signal?: AbortSignal,
    onUpdate?: unknown,
    ctx?: unknown,
  ): Promise<ToolResultLike>;
}

interface CommandDefinitionLike {
  description: string;
  handler: unknown;
}

const EXPECTED_TOOLS = [
  "vault_list",
  "vault_read",
  "vault_search",
  "vault_task_brief",
  "vault_events",
  "vault_create_note",
  "vault_create_folder",
  "vault_write",
  "vault_update_frontmatter",
  "vault_replace_section",
  "vault_append_item",
  "vault_set_line",
  "vault_insert_line",
  "vault_delete_line",
  "vault_toggle_checkbox",
  "vault_git_status",
  "vault_git_sync",
] as const;

const EXPECTED_COMMANDS = [
  "tasklens-file",
  "tasklens-today",
  "tasklens-distill",
  "tasklens-sync",
] as const;

function createFakeApi() {
  const tools: ToolDefinitionLike[] = [];
  const commands: Array<CommandDefinitionLike & { name: string }> = [];
  const handlers = new Map<string, Array<(...args: unknown[]) => unknown>>();

  return {
    tools,
    commands,
    handlers,
    registerTool(definition: ToolDefinitionLike): void {
      tools.push(definition);
    },
    registerCommand(name: string, definition: CommandDefinitionLike): void {
      commands.push({ name, ...definition });
    },
    on(event: string, handler: (...args: unknown[]) => unknown): void {
      const existing = handlers.get(event) ?? [];
      existing.push(handler);
      handlers.set(event, existing);
    },
    sendMessage(): void {},
    appendEntry(): void {},
  };
}

function schemaType(value: unknown): string | undefined {
  if (typeof value === "object" && value !== null && "type" in value && typeof value.type === "string") {
    return value.type;
  }
  return undefined;
}

function resultText(result: ToolResultLike): string {
  return result.content.map((part) => part.text).join("\n");
}

const fake = createFakeApi();
// 假 API 只实现本扩展用到的成员;这里是测试缝,故故意做一次窄化断言。
const api = fake as unknown as ExtensionAPI;
tasklensExtension(api);

describe("VAULT_TOOLS 导出", () => {
  it("导出 17 个工具,名字与既有契约一致且无重复", () => {
    expect(VAULT_TOOLS.length).toBe(17);
    expect(VAULT_TOOLS.map((tool) => tool.name)).toEqual([...EXPECTED_TOOLS]);
    expect(new Set(VAULT_TOOLS.map((tool) => tool.name)).size).toBe(17);
  });

  it("每个工具都有 name/label/description/parameters(schema)/execute", () => {
    for (const tool of VAULT_TOOLS) {
      expect(typeof tool.name).toBe("string");
      expect(typeof tool.label).toBe("string");
      expect(tool.description.length).toBeGreaterThan(0);
      expect(schemaType(tool.parameters)).toBe("object");
      expect(typeof tool.execute).toBe("function");
    }
  });

  it("registerTool 收到的就是 VAULT_TOOLS 里的同一批对象", () => {
    expect(fake.tools.length).toBe(VAULT_TOOLS.length);
    for (const [index, tool] of VAULT_TOOLS.entries()) {
      expect(fake.tools[index]).toBe(tool);
    }
  });
});

describe("注册契约", () => {
  it("注册全部 17 个工具,参数 schema 都是 object", () => {
    const names = fake.tools.map((tool) => tool.name);
    expect(names).toEqual([...EXPECTED_TOOLS]);
    expect(new Set(names).size).toBe(17);
    for (const tool of fake.tools) {
      expect(tool.label.length, `${tool.name} 缺少 label`).toBeGreaterThan(0);
      expect(tool.description.length, `${tool.name} 缺少 description`).toBeGreaterThan(0);
      // 工具描述必须是中文,且说清「什么时候用」。
      expect(tool.description, `${tool.name} 描述应为中文`).toMatch(/[\u4e00-\u9fff]/);
      expect(tool.description, `${tool.name} 描述没有说明使用时机`).toMatch(/什么时候用/);
      expect(schemaType(tool.parameters), `${tool.name} 的 parameters 应为 object`).toBe("object");
    }
  });

  it("注册 4 个斜杠命令", () => {
    expect(fake.commands.map((command) => command.name)).toEqual([...EXPECTED_COMMANDS]);
    for (const command of fake.commands) {
      expect(command.description).toMatch(/[\u4e00-\u9fff]/);
      expect(typeof command.handler).toBe("function");
    }
  });

  it("订阅 session_start", () => {
    expect(fake.handlers.get("session_start")?.length).toBe(1);
  });

  it("参数非法时返回可读错误而不抛异常", async () => {
    const read = fake.tools.find((tool) => tool.name === "vault_read");
    const toggle = fake.tools.find((tool) => tool.name === "vault_toggle_checkbox");
    const frontmatter = fake.tools.find((tool) => tool.name === "vault_update_frontmatter");
    const createFolder = fake.tools.find((tool) => tool.name === "vault_create_folder");
    expect(read && toggle && frontmatter && createFolder).toBeTruthy();

    const cases: Array<[ToolDefinitionLike | undefined, unknown]> = [
      [read, { path: "../逃逸.md" }],
      [read, { path: "/abs/绝对路径.md" }],
      [read, { path: "" }],
      [toggle, { path: "a.md", lines: [0] }],
      [toggle, { path: "a.md", lines: "nope" }],
      [frontmatter, { path: "a.md", updates: "not-an-object" }],
      [createFolder, { path: "../逃逸" }],
      [createFolder, { path: "" }],
    ];
    for (const [tool, params] of cases) {
      const result = await tool?.execute("call", params, undefined, undefined, undefined);
      expect(resultText(result as ToolResultLike)).toMatch(/^参数错误/);
    }
  });
});

const CLI_BIN = process.platform === "win32" ? "tasklens-cli.exe" : "tasklens-cli";
const cliPath =
  process.env.TASK_LENS_CLI?.trim() || path.resolve(process.cwd(), "target", "debug", CLI_BIN);
const cliAvailable = existsSync(cliPath);
const TASK = "产研工作/工作任务/示例任务.md";
const LOG = "工作内容/2026-09-12.md";

let vault = "";
let previousCli: string | undefined;
let previousVault: string | undefined;
let previousConfig: string | undefined;

async function executeTool(name: string, params: unknown): Promise<string> {
  const tool = fake.tools.find((candidate) => candidate.name === name);
  if (!tool) throw new Error(`工具未注册:${name}`);
  const result = await tool.execute("call", params, undefined, undefined, undefined);
  return resultText(result);
}

describe.skipIf(!cliAvailable)("真实 CLI(临时知识库)", () => {
  beforeAll(async () => {
    vault = await mkdtemp(path.join(tmpdir(), "tasklens-pi-"));
    await mkdir(path.join(vault, "产研工作", "工作任务"), { recursive: true });
    await mkdir(path.join(vault, "工作内容"), { recursive: true });
    await writeFile(
      path.join(vault, ...TASK.split("/")),
      [
        "---",
        "type: 任务",
        "status: 进行中",
        "priority: 高",
        "category: 产研",
        `created: 2026-09-12`,
        "owner: 张三",
        "---",
        "",
        "# 示例任务",
        "",
        "## 背景",
        "验证 pi 扩展与 vault core 的联通。",
        "",
        "## 子任务",
        "- [ ] 第一步",
        "- [ ] 第二步",
        "",
        "## 变更日志",
        "- 2026-09-12: 创建任务",
        "",
      ].join("\n"),
      "utf8",
    );
    await writeFile(
      path.join(vault, ...LOG.split("/")),
      ["---", "type: 日志", "date: 2026-09-12", "---", "", "# 2026-09-12", "", "- 推进 [[示例任务]] 的联调", ""].join(
        "\n",
      ),
      "utf8",
    );

    previousCli = process.env.TASK_LENS_CLI;
    previousVault = process.env.TASK_LENS_VAULT;
    previousConfig = process.env.TASK_LENS_CONFIG_DIR;
    process.env.TASK_LENS_CLI = cliPath;
    process.env.TASK_LENS_VAULT = vault;
    process.env.TASK_LENS_CONFIG_DIR = path.join(vault, ".tasklens-config");
  });

  afterAll(async () => {
    if (previousCli === undefined) delete process.env.TASK_LENS_CLI;
    else process.env.TASK_LENS_CLI = previousCli;
    if (previousVault === undefined) delete process.env.TASK_LENS_VAULT;
    else process.env.TASK_LENS_VAULT = previousVault;
    if (previousConfig === undefined) delete process.env.TASK_LENS_CONFIG_DIR;
    else process.env.TASK_LENS_CONFIG_DIR = previousConfig;
    if (vault) await rm(vault, { recursive: true, force: true });
  });

  it("vault_task_brief 返回任务标题、子任务行号与相关日志", async () => {
    const text = await executeTool("vault_task_brief", { path: TASK });
    expect(text).toContain("示例任务");
    expect(text).toContain("子任务");
    expect(text).toContain("第一步");
    expect(text).toMatch(/第 \d+ 行/);
    expect(text).toContain(LOG);
    expect(text).toContain("联调");
  });

  it("vault_toggle_checkbox 真的把 - [ ] 改成 - [x]", async () => {
    const file = path.join(vault, ...TASK.split("/"));
    const before = await readFile(file, "utf8");
    const lines = before.split("\n");
    const index = lines.findIndex((line) => line.trim() === "- [ ] 第一步");
    expect(index).toBeGreaterThanOrEqual(0);
    const lineNumber = index + 1;

    const text = await executeTool("vault_toggle_checkbox", { path: TASK, lines: [lineNumber] });
    expect(text).toContain("已切换复选框");
    expect(text).toContain(`第 ${lineNumber} 行`);

    const after = await readFile(file, "utf8");
    expect(after).toContain("- [x] 第一步");
    expect(after).toContain("- [ ] 第二步");
  });

  it("vault_read 返回原文,vault_list 支持 type 过滤", async () => {
    const read = await executeTool("vault_read", { path: TASK });
    expect(read).toContain("## 变更日志");

    const tasks = await executeTool("vault_list", { type: "任务" });
    expect(tasks).toContain(TASK);
    expect(tasks).toContain("| 路径 | 标题 |");

    const logs = await executeTool("vault_list", { type: "日志" });
    expect(logs).toContain(LOG);
    expect(logs).not.toContain(TASK);
  });

  it("vault_create_folder 建出多级目录,已存在也算成功", async () => {
    const first = await executeTool("vault_create_folder", { path: "产研工作/会议纪要/2026" });
    expect(first).toContain("已创建文件夹");
    expect(first).toContain("产研工作/会议纪要/2026");
    expect(existsSync(path.join(vault, "产研工作", "会议纪要", "2026"))).toBe(true);

    const again = await executeTool("vault_create_folder", { path: "产研工作/会议纪要/2026" });
    expect(again).toContain("已创建文件夹");
  });

  it("CLI 报错时返回错误文本而不是抛出", async () => {
    const text = await executeTool("vault_read", { path: "不存在的目录/不存在.md" });
    expect(text).toMatch(/^错误:/);
  });

  it("vault_git_status 报告非仓库状态", async () => {
    const text = await executeTool("vault_git_status", {});
    expect(text).toContain("不是 Git 仓库");
  });

  it("session_start 在存在知识库时提示工具就绪", async () => {
    const notifications: string[] = [];
    const ctx = {
      ui: {
        notify: (message: string): void => {
          notifications.push(message);
        },
      },
    };
    const handler = fake.handlers.get("session_start")?.[0];
    expect(handler).toBeTruthy();
    await handler?.({ reason: "startup" }, ctx);
    const joined = notifications.join("\n");
    expect(joined).toContain("TaskLens 知识库工具已就绪(17 个)");
    expect(joined).toContain(vault);
  });
});

describe("CLI 缺失", () => {
  it("返回可读的缺失提示而不是抛出", async () => {
    const previous = process.env.TASK_LENS_CLI;
    process.env.TASK_LENS_CLI = path.join(tmpdir(), "definitely-missing-tasklens-cli.exe");
    try {
      const text = await executeTool("vault_list", {});
      expect(text).toMatch(/^错误:/);
      expect(text).toContain("TASK_LENS_CLI");
    } finally {
      if (previous === undefined) delete process.env.TASK_LENS_CLI;
      else process.env.TASK_LENS_CLI = previous;
    }
  });
});