import "fake-indexeddb/auto";

import Dexie from "dexie";
import { afterEach, describe, expect, it } from "vitest";

import {
  buildChatConversationKey,
  buildChatMediaCacheKey,
} from "@/lib/chat/chat_cache_keys";

import {
  LocalChatDB,
  type LocalChatMediaRow,
  type LocalMessageRow,
} from "../local_chat_db";

const DATABASE_SCHEMA = {
  messages: "++dbId, sessionId",
  media:
    "cacheKey, ownerKey, messageId, kind, remoteUrl, updatedAt, lastAccessedAt",
};

const openDatabases: Dexie[] = [];
const databaseNames = new Set<string>();

afterEach(async () => {
  for (const database of openDatabases.splice(0)) database.close();
  await Promise.all([...databaseNames].map((name) => Dexie.delete(name)));
  databaseNames.clear();
});

describe("LocalChatDB v5 migration", () => {
  it("moves owner-only and old character messages to current conversations", async () => {
    const legacy = await createLegacyDatabase(3);
    await legacy.table<LocalMessageRow, number>("messages").bulkAdd([
      messageRow("owner-only", "user:account-1"),
      messageRow(
        "old-elio",
        buildChatConversationKey("user:account-1", "character_elio"),
      ),
      messageRow(
        "old-maya",
        buildChatConversationKey("user:account-1", "character_maya"),
      ),
      messageRow(
        "old-nayeli",
        buildChatConversationKey("device:guest-1", "character_nayeli"),
      ),
      messageRow(
        "current-elio",
        buildChatConversationKey("user:account-2", "elio"),
      ),
      messageRow(
        "unknown-character",
        buildChatConversationKey("user:account-2", "character_aria"),
      ),
    ]);
    legacy.close();

    const upgraded = track(new LocalChatDB(legacy.name));
    await upgraded.open();

    const rows = await upgraded.messages.orderBy("dbId").toArray();
    expect(rows.map(({ id, sessionId }) => ({ id, sessionId }))).toEqual([
      {
        id: "owner-only",
        sessionId: buildChatConversationKey("user:account-1", "elio"),
      },
      {
        id: "old-elio",
        sessionId: buildChatConversationKey("user:account-1", "elio"),
      },
      {
        id: "old-maya",
        sessionId: buildChatConversationKey("user:account-1", "maya-tan"),
      },
      {
        id: "old-nayeli",
        sessionId: buildChatConversationKey(
          "device:guest-1",
          "nayeli-cervantes",
        ),
      },
      {
        id: "current-elio",
        sessionId: buildChatConversationKey("user:account-2", "elio"),
      },
      {
        id: "unknown-character",
        sessionId: buildChatConversationKey(
          "user:account-2",
          "character_aria",
        ),
      },
    ]);
  });

  it("rekeys media while preserving current data and the newest legacy row", async () => {
    const legacy = await createLegacyDatabase(4);
    const owner = "user:account-1";
    const currentElioOwner = buildChatConversationKey(owner, "elio");
    const currentElioKey = buildChatMediaCacheKey({
      ownerKey: currentElioOwner,
      messageId: "current-wins",
      kind: "image",
    });
    await legacy.table<LocalChatMediaRow, string>("media").bulkAdd([
      mediaRow({
        ownerKey: buildChatConversationKey(owner, "character_elio"),
        messageId: "current-wins",
        kind: "image",
        remoteUrl: "https://example.com/legacy-newer.jpg",
        updatedAt: 20,
      }),
      mediaRow({
        ownerKey: currentElioOwner,
        messageId: "current-wins",
        kind: "image",
        remoteUrl: "https://example.com/current.jpg",
        updatedAt: 10,
      }),
      mediaRow({
        ownerKey: owner,
        messageId: "newest-legacy-wins",
        kind: "audio",
        remoteUrl: "https://example.com/older.mp3",
        updatedAt: 5,
      }),
      mediaRow({
        ownerKey: buildChatConversationKey(owner, "character_elio"),
        messageId: "newest-legacy-wins",
        kind: "audio",
        remoteUrl: "https://example.com/newer.mp3",
        updatedAt: 15,
      }),
      mediaRow({
        ownerKey: buildChatConversationKey(owner, "character_maya"),
        messageId: "maya-media",
        kind: "image",
        remoteUrl: "https://example.com/maya.jpg",
        updatedAt: 8,
      }),
    ]);
    legacy.close();

    const upgraded = track(new LocalChatDB(legacy.name));
    await upgraded.open();

    expect(await upgraded.media.get(currentElioKey)).toMatchObject({
      ownerKey: currentElioOwner,
      remoteUrl: "https://example.com/current.jpg",
    });

    const legacyCollisionKey = buildChatMediaCacheKey({
      ownerKey: currentElioOwner,
      messageId: "newest-legacy-wins",
      kind: "audio",
    });
    expect(await upgraded.media.get(legacyCollisionKey)).toMatchObject({
      ownerKey: currentElioOwner,
      remoteUrl: "https://example.com/newer.mp3",
    });

    const mayaOwner = buildChatConversationKey(owner, "maya-tan");
    expect(
      await upgraded.media.get(
        buildChatMediaCacheKey({
          ownerKey: mayaOwner,
          messageId: "maya-media",
          kind: "image",
        }),
      ),
    ).toMatchObject({ ownerKey: mayaOwner });
    expect(await upgraded.media.count()).toBe(3);
  });

  it("upgrades an existing v4 database and remains idempotent", async () => {
    const legacy = await createLegacyDatabase(4);
    await legacy.table<LocalMessageRow, number>("messages").add(
      messageRow(
        "old-elio",
        buildChatConversationKey("device:guest-1", "character_elio"),
      ),
    );
    legacy.close();

    const upgraded = track(new LocalChatDB(legacy.name));
    await upgraded.open();
    const firstRows = await upgraded.messages.toArray();
    upgraded.close();

    const reopened = track(new LocalChatDB(legacy.name));
    await reopened.open();
    expect(await reopened.messages.toArray()).toEqual(firstRows);
    expect(firstRows[0]?.sessionId).toBe(
      buildChatConversationKey("device:guest-1", "elio"),
    );
  });
});

async function createLegacyDatabase(version: 3 | 4): Promise<Dexie> {
  const name = `cozsweet-chat-v${version}-${crypto.randomUUID()}`;
  databaseNames.add(name);
  const database = track(new Dexie(name));
  database.version(version).stores(DATABASE_SCHEMA);
  await database.open();
  return database;
}

function track<T extends Dexie>(database: T): T {
  openDatabases.push(database);
  return database;
}

function messageRow(id: string, sessionId: string): LocalMessageRow {
  return {
    id,
    role: "assistant",
    type: "text",
    content: id,
    createdAt: "2026-07-17T00:00:00.000Z",
    sessionId,
  };
}

function mediaRow(
  input: Pick<
    LocalChatMediaRow,
    "ownerKey" | "messageId" | "kind" | "remoteUrl" | "updatedAt"
  >,
): LocalChatMediaRow {
  return {
    ...input,
    cacheKey: buildChatMediaCacheKey(input),
    bytes: new Uint8Array([1, 2, 3]).buffer,
    mimeType: input.kind === "image" ? "image/jpeg" : "audio/mpeg",
    byteSize: 3,
    createdAt: input.updatedAt,
    lastAccessedAt: input.updatedAt,
  };
}
