import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { GeminiChatMemory, type GeminiChatContent } from "../geminiChatMemory.ts";
import {
  buildFunctionResponseUserTurn,
  repairFunctionResponseUserTurn,
  sanitizeGeminiContents,
  stripFunctionCallTurns,
  validateFunctionResponseAlignment,
} from "../geminiChatHistory.ts";

function fc(name: string, id?: string): GeminiChatContent {
  return {
    role: "model",
    parts: [{ functionCall: { name, id, args: {} } }],
  };
}

function fr(name: string, id?: string): GeminiChatContent {
  return {
    role: "user",
    parts: [
      {
        functionResponse: {
          name,
          id,
          response: { output: "ok" },
        },
      },
    ],
  };
}

function userText(text: string): GeminiChatContent {
  return { role: "user", parts: [{ text }] };
}

function modelText(text: string): GeminiChatContent {
  return { role: "model", parts: [{ text }] };
}

describe("geminiChatHistory", () => {
  it("sanitizeGeminiContents drops trailing orphan model(functionCall)", () => {
    const out = sanitizeGeminiContents([
      userText("hi"),
      fc("read_file", "c1"),
    ]);
    assert.equal(out.length, 1);
    assert.equal(out[0]!.role, "user");
  });

  it("sanitizeGeminiContents keeps model(functionCall) when followed by functionResponse", () => {
    const out = sanitizeGeminiContents([
      userText("hi"),
      fc("read_file", "c1"),
      fr("read_file", "c1"),
      modelText("done"),
    ]);
    assert.equal(out.length, 4);
    assert.equal(out[2]!.role, "user");
    assert.ok(
      (out[2]!.parts[0] as { functionResponse?: unknown }).functionResponse,
    );
  });

  it("sanitizeGeminiContents keeps latest of consecutive plain user turns", () => {
    const out = sanitizeGeminiContents([
      userText("old"),
      userText("new"),
    ]);
    assert.equal(out.length, 1);
    assert.equal((out[0]!.parts[0] as { text: string }).text, "new");
  });

  it("buildFunctionResponseUserTurn emits one response per functionCall in model parts", () => {
    const modelParts = [
      { functionCall: { name: "read_file", id: "a", args: {} } },
      { functionCall: { name: "list_dir", id: "b", args: {} } },
    ];
    const toolResults = [
      { name: "read_file", id: "a", response: { output: "file" } },
      {
        name: "list_dir",
        id: "b",
        response: { output: "Duplicate tool call skipped." },
      },
    ];
    const turn = buildFunctionResponseUserTurn(modelParts, toolResults);
    assert.equal(turn.role, "user");
    assert.equal(turn.parts.length, 2);
    const names = turn.parts.map(
      (p) => (p as { functionResponse: { name: string } }).functionResponse.name,
    );
    assert.deepEqual(names, ["read_file", "list_dir"]);
  });

  it("validateFunctionResponseAlignment detects call/response count mismatch", () => {
    const modelParts = [
      { functionCall: { name: "read_file", id: "a", args: {} } },
      { functionCall: { name: "list_dir", id: "b", args: {} } },
    ];
    const userTurn: GeminiChatContent = {
      role: "user",
      parts: [
        {
          functionResponse: {
            name: "read_file",
            id: "a",
            response: { output: "ok" },
          },
        },
      ],
    };
    const v = validateFunctionResponseAlignment(modelParts, userTurn);
    assert.equal(v.ok, false);
    assert.equal(v.callCount, 2);
    assert.equal(v.responseCount, 1);
  });

  it("validateFunctionResponseAlignment detects id mismatch at same index", () => {
    const modelParts = [
      { functionCall: { name: "read_file", id: "a", args: {} } },
    ];
    const userTurn: GeminiChatContent = {
      role: "user",
      parts: [
        {
          functionResponse: {
            name: "read_file",
            id: "wrong",
            response: { output: "ok" },
          },
        },
      ],
    };
    const v = validateFunctionResponseAlignment(modelParts, userTurn);
    assert.equal(v.ok, false);
    assert.equal(v.callCount, 1);
    assert.equal(v.responseCount, 1);
  });

  it("repairFunctionResponseUserTurn rebuilds aligned responses", () => {
    const modelParts = [
      { functionCall: { name: "read_file", id: "a", args: {} } },
      { functionCall: { name: "list_dir", id: "b", args: {} } },
    ];
    const toolResults = [
      { name: "list_dir", id: "b", response: { output: "dir" } },
      { name: "read_file", id: "a", response: { output: "file" } },
    ];
    const repaired = repairFunctionResponseUserTurn(modelParts, toolResults);
    const v = validateFunctionResponseAlignment(modelParts, repaired);
    assert.equal(v.ok, true);
    const ids = repaired.parts.map(
      (p) =>
        (p as { functionResponse: { id?: string } }).functionResponse.id,
    );
    assert.deepEqual(ids, ["a", "b"]);
  });

  it("stripFunctionCallTurns removes tool turns for tools=0 requests", () => {
    const out = stripFunctionCallTurns([
      userText("hi"),
      fc("read_file"),
      fr("read_file"),
      modelText("answer"),
    ]);
    assert.equal(out.length, 2);
    assert.equal((out[0]!.parts[0] as { text: string }).text, "hi");
    assert.equal((out[1]!.parts[0] as { text: string }).text, "answer");
  });

  it("sanitizeGeminiContents inserts model stub between user(FR) and plain user", () => {
    const out = sanitizeGeminiContents([
      userText("hi"),
      fc("read_file", "c1"),
      fr("read_file", "c1"),
      userText("按计划去执行"),
    ]);
    assert.equal(out.length, 5);
    assert.equal(out[2]!.role, "user");
    assert.equal(out[3]!.role, "model");
    assert.equal((out[3]!.parts[0] as { text: string }).text, "…");
    assert.equal((out[4]!.parts[0] as { text: string }).text, "按计划去执行");
  });

  it("sanitizeGeminiContents drops leading orphan user(functionResponse)", () => {
    const out = sanitizeGeminiContents([
      fr("read_file", "c1"),
      userText("continue"),
    ]);
    assert.equal(out.length, 1);
    assert.equal((out[0]!.parts[0] as { text: string }).text, "continue");
  });

  it("sanitizeGeminiContents drops trailing orphan user(functionResponse)", () => {
    const out = sanitizeGeminiContents([
      userText("hi"),
      fc("read_file", "c1"),
      fr("read_file", "c1"),
    ]);
    assert.equal(out.length, 1);
    assert.equal((out[0]!.parts[0] as { text: string }).text, "hi");
  });

  it("GeminiChatMemory trim re-sanitizes broken tool sequence", () => {
    const mem = new GeminiChatMemory();
    const key = "sdk:agent";
    const long: GeminiChatContent[] = [];
    for (let i = 0; i < 20; i++) {
      long.push(userText(`u${i}`));
      long.push(modelText(`m${i}`));
    }
    long.push(userText("final"));
    long.push(fc("write_report", "orphan"));
    mem.save(key, long, 4);

    const loaded = mem.load(key);
    for (const msg of loaded) {
      if (msg.role === "model") {
        assert.ok(
          !msg.parts.some((p) => Boolean((p as { functionCall?: unknown }).functionCall)),
          "trimmed memory must not end with orphan functionCall",
        );
      }
    }
    const last = loaded[loaded.length - 1]!;
    assert.notEqual(last.role, "model");
  });
});
