import { beforeEach, describe, expect, it, vi } from "vitest";

import { Result } from "@/utils/result";

const getHistoryMock = vi.hoisted(() => vi.fn());
const getPreviewsMock = vi.hoisted(() => vi.fn());

vi.mock("@/data/repositories/chat_repository_loader", () => ({
  loadChatRepository: async () => ({
    getHistory: getHistoryMock,
    getPreviews: getPreviewsMock,
  }),
}));

import {
  fetchSplashLatestMessagePreview,
  loadSplashLatestMessagePreview,
} from "../splash_latest_message";
import { createSplashLatestMessageCache } from "../splash_latest_message_cache";

describe("fetchSplashLatestMessagePreview", () => {
  beforeEach(() => {
    getHistoryMock.mockReset();
    getPreviewsMock.mockReset();
  });

  it("uses the last message returned by history", async () => {
    getHistoryMock.mockResolvedValue(
      Result.ok({
        messages: [
          { type: "text", content: "Older message" },
          { type: "text", content: "Latest message" },
        ],
      }),
    );

    const result = await fetchSplashLatestMessagePreview("elio");

    expect(getHistoryMock).toHaveBeenCalledWith(
      "elio",
      1,
      0,
    );
    expect(Result.isOk(result) && result.data).toBe("Latest message");
  });

  it("requests the selected character preview", async () => {
    getHistoryMock.mockResolvedValue(Result.ok({ messages: [] }));

    await fetchSplashLatestMessagePreview("character_aria");

    expect(getHistoryMock).toHaveBeenCalledWith("character_aria", 1, 0);
  });

  it("returns null when history is empty", async () => {
    getHistoryMock.mockResolvedValue(Result.ok({ messages: [] }));

    const result = await fetchSplashLatestMessagePreview("elio");

    expect(Result.isOk(result) && result.data).toBeNull();
  });
});

describe("loadSplashLatestMessagePreview", () => {
  it("loads all character previews once and caches each conversation", async () => {
    const cache = createSplashLatestMessageCache();
    getPreviewsMock.mockResolvedValue(
      Result.ok({
        items: [
          {
            characterId: "elio",
            message: { type: "text", content: "Hello Elio" },
          },
          {
            characterId: "maya-tan",
            message: { type: "text", content: "Hello Maya" },
          },
        ],
      }),
    );

    const result = await loadSplashLatestMessagePreview({
      cache,
      characterId: "maya-tan",
      resolveIdentity: vi.fn().mockResolvedValue(Result.ok("user:maya")),
      resolvePreviewIdentity: async (characterId) =>
        Result.ok(`user:${characterId}`),
    });

    expect(Result.isOk(result) && result.data).toBe("Hello Maya");
    expect(getPreviewsMock).toHaveBeenCalledOnce();
    expect(cache.get("user:elio")?.message).toBe("Hello Elio");
    expect(cache.get("user:maya-tan")?.message).toBe("Hello Maya");
  });

  it("reuses a successful result for the same identity", async () => {
    const cache = createSplashLatestMessageCache();
    const fetchPreview = vi.fn().mockResolvedValue(Result.ok("Hello again"));
    const resolveIdentity = vi.fn().mockResolvedValue(Result.ok("user:1"));

    const first = await loadSplashLatestMessagePreview({
      cache,
      characterId: "elio",
      fetchPreview,
      resolveIdentity,
    });
    const second = await loadSplashLatestMessagePreview({
      cache,
      characterId: "elio",
      fetchPreview,
      resolveIdentity,
    });

    expect(Result.isOk(first) && first.data).toBe("Hello again");
    expect(Result.isOk(second) && second.data).toBe("Hello again");
    expect(fetchPreview).toHaveBeenCalledTimes(1);
  });

  it("treats a cached empty history as a cache hit", async () => {
    const cache = createSplashLatestMessageCache();
    cache.set("user:empty", null);
    const fetchPreview = vi.fn();

    const result = await loadSplashLatestMessagePreview({
      cache,
      characterId: "elio",
      fetchPreview,
      resolveIdentity: vi
        .fn()
        .mockResolvedValue(Result.ok("user:empty")),
    });

    expect(Result.isOk(result) && result.data).toBeNull();
    expect(fetchPreview).not.toHaveBeenCalled();
  });

  it("does not cache failed requests", async () => {
    const cache = createSplashLatestMessageCache();
    const fetchPreview = vi
      .fn()
      .mockResolvedValueOnce(Result.err(new Error("offline")))
      .mockResolvedValueOnce(Result.ok("Recovered"));
    const resolveIdentity = vi.fn().mockResolvedValue(Result.ok("user:1"));

    await loadSplashLatestMessagePreview({
      cache,
      characterId: "elio",
      fetchPreview,
      resolveIdentity,
    });
    const retry = await loadSplashLatestMessagePreview({
      cache,
      characterId: "elio",
      fetchPreview,
      resolveIdentity,
    });

    expect(Result.isOk(retry) && retry.data).toBe("Recovered");
    expect(fetchPreview).toHaveBeenCalledTimes(2);
  });

  it("bypasses the cache when the identity is unavailable", async () => {
    const cache = createSplashLatestMessageCache();
    const fetchPreview = vi.fn().mockResolvedValue(Result.ok("Network"));
    const resolveIdentity = vi
      .fn()
      .mockResolvedValue(Result.err(new Error("missing identity")));

    await loadSplashLatestMessagePreview({
      cache,
      characterId: "elio",
      fetchPreview,
      resolveIdentity,
    });
    await loadSplashLatestMessagePreview({
      cache,
      characterId: "elio",
      fetchPreview,
      resolveIdentity,
    });

    expect(fetchPreview).toHaveBeenCalledTimes(2);
  });
});
