// Plan F.1: pure-function correctness tests for `mergeByIdReplaceWins`,
// the helper that combines `snapshot.messages` with `session.out` replay
// at run boot (plan section B.3). Replay wins on id collision because
// `session.out` carries the freshest representation of an assistant
// message.

import "../src/v3/test/index.js";

import type { UIMessage } from "ai";
import { describe, expect, it } from "vitest";
import { __mergeByIdReplaceWinsForTests as mergeByIdReplaceWins } from "../src/v3/ai.js";

// ── Helpers ────────────────────────────────────────────────────────────

function userMessage(id: string, text: string): UIMessage {
  return {
    id,
    role: "user",
    parts: [{ type: "text", text }],
  };
}

function assistantMessage(id: string, text: string): UIMessage {
  return {
    id,
    role: "assistant",
    parts: [{ type: "text", text }],
  };
}

// ── Tests ──────────────────────────────────────────────────────────────

describe("mergeByIdReplaceWins", () => {
  it("returns a copy of `a` when `b` is empty", () => {
    const a = [userMessage("u-1", "hello")];
    const result = mergeByIdReplaceWins(a, []);
    expect(result).toEqual(a);
    // Verify it's a copy (mutating result shouldn't touch a).
    result.push(assistantMessage("a-1", "extra"));
    expect(a).toHaveLength(1);
  });

  it("returns a copy of `b` when `a` is empty", () => {
    const b = [assistantMessage("a-1", "world")];
    const result = mergeByIdReplaceWins([], b);
    expect(result).toEqual(b);
    result.push(userMessage("u-extra", "extra"));
    expect(b).toHaveLength(1);
  });

  it("returns [] when both inputs are empty", () => {
    expect(mergeByIdReplaceWins([], [])).toEqual([]);
  });

  it("appends fresh ids from `b` after `a`'s entries", () => {
    const a = [userMessage("u-1", "hi")];
    const b = [assistantMessage("a-1", "ok")];
    const result = mergeByIdReplaceWins(a, b);
    expect(result.map((m) => m.id)).toEqual(["u-1", "a-1"]);
    expect(result[0]!.role).toBe("user");
    expect(result[1]!.role).toBe("assistant");
  });

  it("replaces by id when `b` has a colliding entry — replay wins", () => {
    const a = [userMessage("u-1", "hi"), assistantMessage("a-1", "stale-version")];
    const b = [assistantMessage("a-1", "fresh-version")];
    const result = mergeByIdReplaceWins(a, b);
    expect(result).toHaveLength(2);
    expect(result[1]!.id).toBe("a-1");
    expect((result[1]!.parts[0] as { text: string }).text).toBe("fresh-version");
  });

  it("preserves order from `a` even when entries are replaced", () => {
    const a = [
      userMessage("u-1", "first"),
      assistantMessage("a-1", "stale"),
      userMessage("u-2", "second"),
      assistantMessage("a-2", "also-stale"),
    ];
    const b = [assistantMessage("a-1", "fresh-1"), assistantMessage("a-2", "fresh-2")];
    const result = mergeByIdReplaceWins(a, b);
    expect(result.map((m) => m.id)).toEqual(["u-1", "a-1", "u-2", "a-2"]);
    expect((result[1]!.parts[0] as { text: string }).text).toBe("fresh-1");
    expect((result[3]!.parts[0] as { text: string }).text).toBe("fresh-2");
  });

  it("appends `b` entries with no id collision after the merged set", () => {
    const a = [userMessage("u-1", "first")];
    const b = [
      assistantMessage("a-1", "reply-1"),
      userMessage("u-2", "second"),
      assistantMessage("a-2", "reply-2"),
    ];
    const result = mergeByIdReplaceWins(a, b);
    expect(result.map((m) => m.id)).toEqual(["u-1", "a-1", "u-2", "a-2"]);
  });

  it("treats messages without an id as always-append (no collision possible)", () => {
    const a = [
      userMessage("u-1", "first"),
      // Synthetic message missing the id field — should append, never replace.
      {
        id: "" as string,
        role: "assistant",
        parts: [{ type: "text", text: "no-id-a" }],
      } as UIMessage,
    ];
    const b = [
      {
        id: "" as string,
        role: "assistant",
        parts: [{ type: "text", text: "no-id-b" }],
      } as UIMessage,
    ];
    const result = mergeByIdReplaceWins(a, b);
    expect(result).toHaveLength(3);
    // Both empty-id messages survive — no merge happens.
    const noIdParts = result
      .filter((m) => m.id === "")
      .map((m) => (m.parts[0] as { text: string }).text);
    expect(noIdParts).toEqual(["no-id-a", "no-id-b"]);
  });

  it("handles consecutive replays of the same id in `b` — last one wins", () => {
    // Edge case: `b` has two entries with the same id (shouldn't happen
    // for assistants in practice, but the helper must be deterministic).
    const a = [assistantMessage("a-1", "v0")];
    const b = [assistantMessage("a-1", "v1"), assistantMessage("a-1", "v2")];
    const result = mergeByIdReplaceWins(a, b);
    expect(result).toHaveLength(1);
    expect((result[0]!.parts[0] as { text: string }).text).toBe("v2");
  });

  it("preserves user messages (only assistants come from replay) — semantic check", () => {
    // The runtime contract: `session.out` contains assistant chunks only,
    // so `b` should never contain user messages. If it does (defensively),
    // the merge still works — but we lock down the typical pattern here.
    const a = [
      userMessage("u-1", "first"),
      assistantMessage("a-1", "stale"),
      userMessage("u-2", "second"),
    ];
    const b = [assistantMessage("a-1", "fresh")];
    const result = mergeByIdReplaceWins(a, b);
    // User messages from snapshot survive untouched.
    expect(result.filter((m) => m.role === "user").map((m) => m.id)).toEqual(["u-1", "u-2"]);
  });

  it("does not mutate either input array", () => {
    const a = [userMessage("u-1", "hi"), assistantMessage("a-1", "stale")];
    const b = [assistantMessage("a-1", "fresh"), userMessage("u-2", "next")];
    const aSnapshot = JSON.stringify(a);
    const bSnapshot = JSON.stringify(b);

    mergeByIdReplaceWins(a, b);

    expect(JSON.stringify(a)).toBe(aSnapshot);
    expect(JSON.stringify(b)).toBe(bSnapshot);
  });
});
