import { beforeEach, describe, expect, it, vi } from "vitest";
import { createStorage } from "unstorage";
import memoryDriver from "unstorage/drivers/memory";

import { StorageKeys } from "@/data/storage/storage_keys";
import { SessionAsyncUtil } from "@/utils/session-storage";

import {
  createPendingChatPromotion,
  NavigationStorage,
} from "../navigation_storage";

const CHARACTER_ID = "elio";
const OTHER_CHARACTER_ID = "maya-tan";

describe("NavigationStorage", () => {
  beforeEach(() => {
    SessionAsyncUtil.setStorage(createStorage({ driver: memoryDriver() }));
    vi.useRealTimers();
  });

  it("saves, peeks, and consumes pending chat unlock sessions", async () => {
    await NavigationStorage.savePendingChatUnlock({
      characterId: CHARACTER_ID,
      messageId: "msg_1",
      kind: "image",
      returnUrl: "/chat?image=msg_1",
      stage: "payment",
    });

    await expect(
      NavigationStorage.peekPendingChatUnlock(CHARACTER_ID),
    ).resolves.toMatchObject({
      characterId: CHARACTER_ID,
      messageId: "msg_1",
      kind: "image",
      returnUrl: "/chat?image=msg_1",
      stage: "payment",
    });

    await expect(
      NavigationStorage.consumePendingChatUnlock(CHARACTER_ID),
    ).resolves.toMatchObject({
      characterId: CHARACTER_ID,
      messageId: "msg_1",
      kind: "image",
    });
    await expect(
      NavigationStorage.peekPendingChatUnlock(CHARACTER_ID),
    ).resolves.toBeNull();
  });

  it("drops expired pending chat unlock sessions", async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-07-03T00:00:00.000Z"));

    await NavigationStorage.savePendingChatUnlock({
      characterId: CHARACTER_ID,
      messageId: "msg_1",
      kind: "private",
      returnUrl: "/chat",
      stage: "auth",
    });

    vi.setSystemTime(new Date("2026-07-03T00:31:00.000Z"));

    await expect(
      NavigationStorage.peekPendingChatUnlock(CHARACTER_ID),
    ).resolves.toBeNull();
  });

  it("stores a one-time promotion and carries it through unlock navigation", async () => {
    const promotion = await NavigationStorage.savePendingChatPromotion(
      "image",
      CHARACTER_ID,
    );

    expect(promotion).toMatchObject({
      characterId: CHARACTER_ID,
      promotionType: "image",
      lockType: "image_paywall",
    });
    expect(promotion.clientLockId).toMatch(/^promotion_/);
    await expect(
      NavigationStorage.consumePendingChatPromotion(CHARACTER_ID),
    ).resolves.toEqual(promotion);
    await expect(
      NavigationStorage.consumePendingChatPromotion(CHARACTER_ID),
    ).resolves.toBeNull();

    await NavigationStorage.savePendingChatUnlock({
      characterId: CHARACTER_ID,
      displayMessageId: `promotion:${promotion.clientLockId}`,
      kind: "image",
      lockType: promotion.lockType,
      clientLockId: promotion.clientLockId,
      promotion,
      returnUrl: "/chat",
      stage: "auth",
    });
    await expect(
      NavigationStorage.peekPendingChatUnlock(CHARACTER_ID),
    ).resolves.toMatchObject({
      characterId: CHARACTER_ID,
      lockType: "image_paywall",
      clientLockId: promotion.clientLockId,
      promotion,
    });
  });

  it("creates manual promotions without persisting them", async () => {
    const image = createPendingChatPromotion("image", CHARACTER_ID);
    const voice = createPendingChatPromotion("voice", CHARACTER_ID);

    expect(image).toMatchObject({
      characterId: CHARACTER_ID,
      promotionType: "image",
      lockType: "image_paywall",
    });
    expect(voice).toMatchObject({
      characterId: CHARACTER_ID,
      promotionType: "voice",
      lockType: "voice_message",
    });
    expect(image.clientLockId).toMatch(/^promotion_/);
    expect(voice.clientLockId).toMatch(/^promotion_/);
    expect(voice.clientLockId).not.toBe(image.clientLockId);
    await expect(
      NavigationStorage.consumePendingChatPromotion(CHARACTER_ID),
    ).resolves.toBeNull();
  });

  it("saves and consumes pending chat image return sessions", async () => {
    await NavigationStorage.savePendingChatImageReturn({
      characterId: CHARACTER_ID,
      messageId: "msg_1",
      returnUrl: "/chat?image=msg_1",
    });

    await expect(
      NavigationStorage.consumePendingChatImageReturn(CHARACTER_ID),
    ).resolves.toMatchObject({
      characterId: CHARACTER_ID,
      messageId: "msg_1",
      returnUrl: "/chat?image=msg_1",
    });
    await expect(
      NavigationStorage.consumePendingChatImageReturn(CHARACTER_ID),
    ).resolves.toBeNull();
  });

  it("leaves another character's pending navigation state untouched", async () => {
    const promotion = await NavigationStorage.savePendingChatPromotion(
      "voice",
      CHARACTER_ID,
    );
    await NavigationStorage.savePendingChatUnlock({
      characterId: CHARACTER_ID,
      displayMessageId: `promotion:${promotion.clientLockId}`,
      kind: "voice",
      lockType: promotion.lockType,
      clientLockId: promotion.clientLockId,
      promotion,
      returnUrl: "/characters/elio/chat",
      stage: "auth",
    });
    await NavigationStorage.savePendingChatImageReturn({
      characterId: CHARACTER_ID,
      messageId: "msg_1",
      returnUrl: "/characters/elio/chat?image=msg_1",
    });

    await expect(
      NavigationStorage.consumePendingChatPromotion(OTHER_CHARACTER_ID),
    ).resolves.toBeNull();
    await expect(
      NavigationStorage.consumePendingChatUnlock(OTHER_CHARACTER_ID),
    ).resolves.toBeNull();
    await expect(
      NavigationStorage.consumePendingChatImageReturn(OTHER_CHARACTER_ID),
    ).resolves.toBeNull();

    await expect(
      NavigationStorage.consumePendingChatPromotion(CHARACTER_ID),
    ).resolves.toEqual(promotion);
    await expect(
      NavigationStorage.consumePendingChatUnlock(CHARACTER_ID),
    ).resolves.toMatchObject({ characterId: CHARACTER_ID });
    await expect(
      NavigationStorage.consumePendingChatImageReturn(CHARACTER_ID),
    ).resolves.toMatchObject({ characterId: CHARACTER_ID });
  });

  it("normalizes old character ids in pending navigation sessions", async () => {
    const createdAt = Date.now();
    const legacyPromotion = {
      characterId: "character_elio",
      promotionType: "image" as const,
      lockType: "image_paywall" as const,
      clientLockId: "promotion_legacy",
      createdAt,
    };
    await SessionAsyncUtil.setJson(
      StorageKeys.pendingChatPromotion,
      legacyPromotion,
    );
    await SessionAsyncUtil.setJson(StorageKeys.pendingChatUnlock, {
      reason: "single_message_unlock",
      characterId: "character_elio",
      displayMessageId: "promotion:promotion_legacy",
      kind: "image",
      lockType: "image_paywall",
      clientLockId: "promotion_legacy",
      promotion: legacyPromotion,
      returnUrl: "/characters/elio/chat",
      stage: "payment",
      createdAt,
    });
    await SessionAsyncUtil.setJson(StorageKeys.pendingChatImageReturn, {
      reason: "image_paywall",
      characterId: "character_elio",
      messageId: "message_legacy",
      returnUrl: "/characters/elio/chat?image=message_legacy",
      createdAt,
    });

    await expect(
      NavigationStorage.consumePendingChatPromotion(CHARACTER_ID),
    ).resolves.toMatchObject({ characterId: CHARACTER_ID });
    await expect(
      NavigationStorage.peekPendingChatUnlock(CHARACTER_ID),
    ).resolves.toMatchObject({
      characterId: CHARACTER_ID,
      promotion: { characterId: CHARACTER_ID },
    });
    await expect(
      NavigationStorage.consumePendingChatImageReturn(CHARACTER_ID),
    ).resolves.toMatchObject({ characterId: CHARACTER_ID });
  });

});
