import syncFs from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  clearTopicNameCache,
  getTopicEntry,
  getTopicName,
  resetTopicNameCacheForTest,
  topicNameCacheSize,
  updateTopicName,
} from "./topic-name-cache.js";

describe("topic-name-cache", () => {
  beforeEach(() => {
    vi.useRealTimers();
    clearTopicNameCache();
    resetTopicNameCacheForTest();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("stores and retrieves a topic name", () => {
    updateTopicName(-100123, 42, { name: "Deployments" });
    expect(getTopicName(-100123, 42)).toBe("Deployments");
  });

  it("returns undefined for unknown topics", () => {
    expect(getTopicName(-100123, 99)).toBeUndefined();
  });

  it("handles renames via forum_topic_edited (overwrites previous name)", () => {
    updateTopicName(-100123, 42, { name: "Deployments" });
    updateTopicName(-100123, 42, { name: "CI/CD" });
    expect(getTopicName(-100123, 42)).toBe("CI/CD");
  });

  it("preserves name when patching only closed status", () => {
    updateTopicName(-100123, 42, { name: "Deployments" });
    updateTopicName(-100123, 42, { closed: true });
    expect(getTopicName(-100123, 42)).toBe("Deployments");
    expect(getTopicEntry(-100123, 42)?.closed).toBe(true);
  });

  it("marks topic as reopened", () => {
    updateTopicName(-100123, 42, { name: "Deployments", closed: true });
    updateTopicName(-100123, 42, { closed: false });
    expect(getTopicEntry(-100123, 42)?.closed).toBe(false);
  });

  it("stores icon metadata", () => {
    updateTopicName(-100123, 42, {
      name: "Design",
      iconColor: 0x6fb9f0,
      iconCustomEmojiId: "emoji123",
    });
    const entry = getTopicEntry(-100123, 42);
    expect(entry?.iconColor).toBe(0x6fb9f0);
    expect(entry?.iconCustomEmojiId).toBe("emoji123");
  });

  it("does not store entries with empty name and no prior entry", () => {
    updateTopicName(-100123, 42, { closed: true });
    expect(getTopicName(-100123, 42)).toBeUndefined();
    expect(topicNameCacheSize()).toBe(0);
  });

  it("updates timestamps on write", async () => {
    vi.useFakeTimers();
    updateTopicName(-100123, 42, { name: "A" });
    const t1 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
    await vi.advanceTimersByTimeAsync(10);
    updateTopicName(-100123, 42, { name: "B" });
    const t2 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
    expect(t2).toBeGreaterThan(t1);
  });

  it("works with string chatId and threadId", () => {
    updateTopicName("-100123", "42", { name: "StringKeys" });
    expect(getTopicName("-100123", "42")).toBe("StringKeys");
  });

  it("evicts the oldest entry when cache exceeds 2048", () => {
    for (let i = 0; i < 2049; i++) {
      updateTopicName(-100000, i, { name: `Topic ${i}` });
    }
    expect(topicNameCacheSize()).toBe(2048);
    expect(getTopicName(-100000, 0)).toBeUndefined();
    expect(getTopicName(-100000, 2048)).toBe("Topic 2048");
  });

  it("refreshes recency on read so active topics survive eviction", async () => {
    vi.useFakeTimers();
    updateTopicName(-100000, 1, { name: "Active" });
    await vi.advanceTimersByTimeAsync(10);
    for (let i = 2; i <= 2048; i++) {
      updateTopicName(-100000, i, { name: `Topic ${i}` });
    }
    getTopicName(-100000, 1);
    updateTopicName(-100000, 9999, { name: "Newcomer" });
    expect(getTopicName(-100000, 1)).toBe("Active");
    expect(topicNameCacheSize()).toBe(2048);
  });

  it("reloads persisted entries from disk", async () => {
    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-topic-cache-"));
    const persistedPath = path.join(tempDir, "topic-names.json");
    try {
      updateTopicName(-100123, 42, { name: "Deployments" }, persistedPath);
      resetTopicNameCacheForTest();
      expect(getTopicName(-100123, 42, persistedPath)).toBe("Deployments");
    } finally {
      await fs.rm(tempDir, { recursive: true, force: true });
      resetTopicNameCacheForTest();
    }
  });

  it("keeps separate in-memory stores for separate persisted paths", async () => {
    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-topic-cache-"));
    const firstPath = path.join(tempDir, "first-topic-names.json");
    const secondPath = path.join(tempDir, "second-topic-names.json");
    try {
      updateTopicName(-100123, 42, { name: "Deployments" }, firstPath);
      updateTopicName(-200456, 84, { name: "Incidents" }, secondPath);

      const readFileSpy = vi.spyOn(syncFs, "readFileSync");

      expect(getTopicName(-100123, 42, firstPath)).toBe("Deployments");
      expect(getTopicName(-200456, 84, secondPath)).toBe("Incidents");
      expect(readFileSpy).not.toHaveBeenCalled();
    } finally {
      vi.restoreAllMocks();
      await fs.rm(tempDir, { recursive: true, force: true });
      resetTopicNameCacheForTest();
    }
  });
});
