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

import {
  DEFAULT_CHARACTER,
  DEFAULT_CHARACTER_ID,
} from "@/data/constants/character";
import {
  ChatSendResponseSchema,
  UnlockPrivateResponseSchema,
  type ChatSendResponse,
  type ChatSendResponseInput,
} from "@/data/schemas/chat";
import type { ChatState } from "@/stores/chat/chat-state";
import {
  applyHttpSendOutput,
  applyNetworkHistoryLoadedOutput,
  applySingleUnlockOutput,
  countLockedHistoryMessages,
  localMessagesToUi,
  sendResponseToUiMessage,
} from "@/stores/chat/helper";

function makeResponse(
  overrides: Partial<ChatSendResponseInput> = {},
): ChatSendResponse {
  return ChatSendResponseSchema.parse({
    reply: "AI reply",
    messageId: "msg-1",
    isGuest: false,
    timestamp: 1782356425363,
    image: { type: null, url: null },
    lockDetail: {
      locked: false,
      reason: null,
    },
    ...overrides,
  });
}

function makeChatState(overrides: Partial<ChatState> = {}): ChatState {
  return {
    characterId: DEFAULT_CHARACTER_ID,
    characterErrorCode: null,
    emptyChatGreeting: DEFAULT_CHARACTER.emptyChatGreeting,
    messages: [],
    promotion: null,
    outgoingMessageRevision: 0,
    isReplyingAI: true,
    pendingReplyCount: 1,
    upgradePromptVisible: false,
    upgradeReason: null,
    canSendMessage: true,
    creditBalance: 0,
    creditsCharged: 0,
    requiredCredits: 0,
    shortfallCredits: 0,
    paymentGuidance: null,
    historyLoaded: true,
    networkHistoryLoaded: true,
    historyTotal: 0,
    historyLimit: 50,
    nextHistoryOffset: 0,
    isLoadingMoreHistory: false,
    paymentUnlockPending: false,
    unlockHistoryPromptVisible: false,
    lockedHistoryCount: 0,
    unlockHistoryError: null,
    unlockHistoryPaymentGuidance: null,
    unlockingMessage: null,
    unlockMessageError: null,
    unlockPaywallRequest: null,
    ...overrides,
  };
}

describe("sendResponseToUiMessage", () => {
  it("prefers payment guidance over other commercial actions", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        paymentGuidance: {
          guidanceId: "019c8f8d-17d3-7a42-b1cc-b6927b1927d5",
          characterId: "elio",
          characterName: "Elio Silvestri",
          scene: "insufficientCredits",
          mode: "guide",
          title: "Keep talking with Elio",
          copy: "Baby, I want to keep talking with you.",
          ctaLabel: "View VIP & credit options",
          purchaseOptions: ["vip", "topup"],
          target: "subscription",
          ruleId: "payment_guidance_insufficientCredits",
        },
        chatAction: {
          actionId: "chat-action-1",
          kind: "commercial",
          type: "giftOffer",
          copy: "View gifts.",
          ctaLabel: "View gifts",
          ruleId: "legacy",
          orderId: null,
        },
      }),
    );

    expect(message.paymentGuidance?.characterId).toBe("elio");
    expect(message.chatAction).toBeUndefined();
    expect(message.commercialAction).toBeUndefined();
  });

  it("keeps a typed commercial action on the assistant message", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        commercialAction: {
          actionId: "action-1",
          type: "giftOffer",
          copy: "Buy me a coffee?",
          ctaLabel: "View gifts",
          target: "giftCatalog",
          ruleId: "coffee_after_thanks",
        },
      }),
    );

    expect(message.commercialAction).toEqual({
      actionId: "action-1",
      type: "giftOffer",
      copy: "Buy me a coffee?",
      ctaLabel: "View gifts",
      target: "giftCatalog",
      ruleId: "coffee_after_thanks",
    });
  });

  it("prefers the new chat action over the legacy commercial action", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        chatAction: {
          actionId: "chat-action-1",
          kind: "support",
          type: "openFeedback",
          copy: "Share the screenshot here.",
          ctaLabel: "Open Feedback",
          ruleId: null,
          orderId: null,
        },
        commercialAction: {
          actionId: "legacy-action-1",
          type: "giftOffer",
          copy: "View gifts.",
          ctaLabel: "View gifts",
          target: "giftCatalog",
          ruleId: "legacy-rule",
        },
      }),
    );

    expect(message.chatAction?.actionId).toBe("chat-action-1");
    expect(message.commercialAction).toBeUndefined();
  });

  it("maps locked voice messages without treating them as private text", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        lockDetail: {
          locked: true,
          reason: "voice_message",
        },
      }),
    );

    expect(message.content).toBe("AI reply");
    expect(message.audioUrl).toBeUndefined();
    expect(message.locked).toBe(true);
    expect(message.lockReason).toBe("voice_message");
    expect(message.lockedPrivate).toBeUndefined();
    expect(message.privateMessageHint).toBeUndefined();
  });

  it("does not expose an audio URL from a locked send response", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        audioUrl: "https://example.com/locked-voice.mp3",
        lockDetail: {
          locked: true,
          reason: "voice_message",
        },
      }),
    );

    expect(message.audioUrl).toBeUndefined();
    expect(message.locked).toBe(true);
  });

  it("maps an audio URL from an unlocked send response", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        audioUrl: "https://example.com/unlocked-voice.mp3",
      }),
    );

    expect(message.audioUrl).toBe("https://example.com/unlocked-voice.mp3");
    expect(message.locked).toBe(false);
  });

  it("maps locked private text messages as private messages", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        lockDetail: {
          locked: true,
          reason: "private_message",
        },
      }),
    );

    expect(message.content).toBe("AI reply");
    expect(message.locked).toBe(true);
    expect(message.lockReason).toBe("private_message");
    expect(message.isPrivate).toBe(true);
    expect(message.lockedPrivate).toBe(true);
    expect(message.privateMessageHint).toBeUndefined();
  });

  it("keeps image messages locked without retaining the protected URL", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        image: {
          type: "jpg",
          url: null,
        },
        lockDetail: {
          locked: true,
          reason: "image",
        },
      }),
    );

    expect(message.content).toBe("AI reply");
    expect(message.imageUrl).toBeUndefined();
    expect(message.imagePaywalled).toBeUndefined();
    expect(message.locked).toBe(true);
    expect(message.lockReason).toBe("image");
    expect(message.lockedPrivate).toBeUndefined();
  });

  it("does not mark a message as paywalled when its lock reason is unlocked", () => {
    const message = sendResponseToUiMessage(
      makeResponse({
        image: {
          type: "jpg",
          url: "https://example.com/unlocked-photo.jpg",
        },
        lockDetail: {
          locked: false,
          reason: "image_paywall",
        },
      }),
    );

    expect(message.locked).toBe(false);
    expect(message.lockReason).toBe("image_paywall");
    expect(message.imagePaywalled).toBeUndefined();
  });
});

describe("applyHttpSendOutput", () => {
  it("stores guidance for a blocked send and clears it after normal chat resumes", () => {
    const guidance = {
      guidanceId: "019c8f8d-17d3-7a42-b1cc-b6927b1927d5",
      characterId: "elio",
      characterName: "Elio Silvestri",
      scene: "insufficientCredits" as const,
      mode: "guide" as const,
      title: "Keep talking with Elio",
      copy: "Baby, I want to keep talking with you.",
      ctaLabel: "View VIP & credit options",
      purchaseOptions: ["vip", "topup"] as const,
      target: "subscription" as const,
      ruleId: "payment_guidance_insufficientCredits",
    };
    const blocked = applyHttpSendOutput(makeChatState(), {
      response: makeResponse({
        reply: "",
        canSendMessage: false,
        paymentGuidance: guidance,
      }),
      reply: null,
    });
    expect(blocked.paymentGuidance).toEqual(guidance);

    const resumed = applyHttpSendOutput(
      makeChatState({ paymentGuidance: guidance }),
      {
        response: makeResponse({ reply: "Back to our conversation." }),
        reply: sendResponseToUiMessage(
          makeResponse({ reply: "Back to our conversation." }),
        ),
      },
    );
    expect(resumed.paymentGuidance).toBeNull();
  });

  it("stores insufficient credit state and removes failed optimistic messages", () => {
    const context = makeChatState({
      messages: [
        {
          displayId: "client:message:failed",
          content: "hello",
          isFromAI: false,
          date: "2026-06-25",
        },
      ],
    });

    const nextState = applyHttpSendOutput(context, {
      response: makeResponse({
        reply: "",
        messageId: "",
        canSendMessage: false,
        creditBalance: 0,
        creditsCharged: 0,
        requiredCredits: 2,
        shortfallCredits: 2,
        lockDetail: {
          locked: false,
          reason: null,
        },
      }),
      reply: null,
    });

    expect(nextState.messages).toEqual([]);
    expect(nextState.isReplyingAI).toBe(false);
    expect(nextState.upgradePromptVisible).toBe(true);
    expect(nextState.upgradeReason).toBe("insufficient_credits");
    expect(nextState.canSendMessage).toBe(false);
    expect(nextState.requiredCredits).toBe(2);
    expect(nextState.shortfallCredits).toBe(2);
  });

  it("keeps a valid reply while marking future sends as unavailable", () => {
    const context = makeChatState({
      messages: [
        {
          displayId: "client:message:successful",
          content: "hello",
          isFromAI: false,
          date: "2026-06-25",
        },
      ],
    });
    const reply = sendResponseToUiMessage(
      makeResponse({
        reply: "This one went through, but you need credits next.",
        canSendMessage: false,
        creditBalance: 0,
        requiredCredits: 2,
        shortfallCredits: 2,
      }),
    );

    const nextState = applyHttpSendOutput(context, {
      response: makeResponse({
        reply: "This one went through, but you need credits next.",
        canSendMessage: false,
        creditBalance: 0,
        requiredCredits: 2,
        shortfallCredits: 2,
      }),
      reply,
    });

    expect(nextState.messages).toHaveLength(2);
    expect(nextState.messages?.[1]).toMatchObject({
      content: "This one went through, but you need credits next.",
      isFromAI: true,
    });
    expect(nextState.upgradePromptVisible).toBe(true);
    expect(nextState.upgradeReason).toBe("insufficient_credits");
    expect(nextState.canSendMessage).toBe(false);
  });
});

describe("chat history pagination helpers", () => {
  it("falls back to the bounded history limit when the backend limit is zero", () => {
    const nextState = applyNetworkHistoryLoadedOutput(makeChatState(), {
      messages: [],
      localDisplayIds: [],
      localCount: 0,
      total: 120,
      limit: 0,
    });

    expect(nextState.networkHistoryLoaded).toBe(true);

    expect(nextState.historyLimit).toBe(50);
    expect(nextState.nextHistoryOffset).toBe(50);
    expect(nextState.historyTotal).toBe(120);
  });

  it("preserves messages added after the local history snapshot", () => {
    const localDisplayId = "server:history-1:assistant";
    const optimisticMessage = {
      displayId: "client:message:optimistic-1",
      clientId: "optimistic-1",
      content: "Sent while refreshing",
      isFromAI: false,
      date: "2026-07-20",
    };
    const nextState = applyNetworkHistoryLoadedOutput(
      makeChatState({
        messages: [
          {
            displayId: localDisplayId,
            remoteId: "history-1",
            content: "Cached",
            isFromAI: true,
            date: "2026-07-20",
          },
          optimisticMessage,
        ],
      }),
      {
        messages: [
          {
            displayId: localDisplayId,
            remoteId: "history-1",
            content: "Fresh",
            isFromAI: true,
            date: "2026-07-20",
          },
        ],
        localDisplayIds: [localDisplayId],
        localCount: 1,
        total: 1,
        limit: 50,
      },
    );

    expect(nextState.messages).toEqual([
      expect.objectContaining({
        displayId: localDisplayId,
        content: "Fresh",
      }),
      optimisticMessage,
    ]);
  });
});

describe("localMessagesToUi", () => {
  it("uses distinct stable display ids for user and assistant records sharing a remote id", () => {
    const records = [
      {
        id: "shared-1",
        role: "user",
        content: "Hello",
        createdAt: "2026-06-25T12:00:00.000Z",
      },
      {
        id: "shared-1",
        role: "assistant",
        content: "Hi",
        createdAt: "2026-06-25T12:00:01.000Z",
      },
      {
        id: "shared-1",
        role: "assistant",
        content: "A duplicated backend id",
        createdAt: "2026-06-25T12:00:02.000Z",
      },
    ];

    const first = localMessagesToUi(records);
    const second = localMessagesToUi(records);

    expect(first.map((message) => message.displayId)).toEqual([
      "server:shared-1:user",
      "server:shared-1:assistant",
      "server:shared-1:assistant:1",
    ]);
    expect(first.map((message) => message.remoteId)).toEqual([
      "shared-1",
      "shared-1",
      "shared-1",
    ]);
    expect(second.map((message) => message.displayId)).toEqual(
      first.map((message) => message.displayId),
    );
  });

  it("creates a stable legacy display id when history has no remote id", () => {
    const records = [
      {
        role: "assistant",
        type: "text",
        content: "Legacy reply",
        createdAt: "2026-06-25T12:00:00.000Z",
      },
    ];

    const first = localMessagesToUi(records)[0];
    const second = localMessagesToUi(records)[0];

    expect(first?.displayId).toMatch(/^legacy:/);
    expect(second?.displayId).toBe(first?.displayId);
    expect(first?.remoteId).toBeUndefined();
  });

  it("maps locked voice messages from history", () => {
    const [message] = localMessagesToUi([
      {
        id: "voice-1",
        role: "assistant",
        type: "voice",
        content: "hidden voice transcript",
        createdAt: "2026-06-25T12:00:00.000Z",
        audioUrl: "https://example.com/locked-history-voice.mp3",
        image: { type: null, url: null },
        lockDetail: {
          locked: true,
          reason: "voice_message",
        },
      },
    ]);

    expect(message.content).toBe("hidden voice transcript");
    expect(message.audioUrl).toBeUndefined();
    expect(message.locked).toBe(true);
    expect(message.lockReason).toBe("voice_message");
    expect(message.lockedPrivate).toBeUndefined();
    expect(message.privateMessageHint).toBeUndefined();
  });

  it("hides text content for image history messages", () => {
    const [message] = localMessagesToUi([
      {
        id: "image-1",
        role: "assistant",
        type: "image",
        content: "This text should not render with an image.",
        createdAt: "2026-06-25T12:00:00.000Z",
        audioUrl: null,
        image: {
          type: "jpg",
          url: "https://example.com/image.jpg",
        },
        lockDetail: {
          locked: false,
          reason: null,
        },
      },
    ]);

    expect(message.content).toBe("");
    expect(message.imageUrl).toBe("https://example.com/image.jpg");
    expect(message.imagePaywalled).toBeUndefined();
  });
});

describe("applySingleUnlockOutput", () => {
  it("preserves display identity while adding the backend id", () => {
    const displayId = "promotion:lock-1";
    const [message] = applySingleUnlockOutput(
      [
        {
          displayId,
          content: "",
          isFromAI: true,
          date: "2026-07-20",
          locked: true,
          lockReason: "image_paywall",
          imagePaywalled: true,
        },
      ],
      {
        displayMessageId: displayId,
        request: {
          displayMessageId: displayId,
          lockType: "image_paywall",
          clientLockId: "lock-1",
        },
        response: UnlockPrivateResponseSchema.parse({
          unlocked: true,
          messageId: "remote-1",
          image: {
            type: "promotion",
            url: "https://example.com/unlocked.jpg",
          },
        }),
      },
    );

    expect(message).toMatchObject({
      displayId,
      remoteId: "remote-1",
      locked: false,
    });
  });
});

describe("countLockedHistoryMessages", () => {
  it("counts only unlockable locked AI messages", () => {
    expect(
      countLockedHistoryMessages([
        {
          displayId: "private-1",
          content: "",
          isFromAI: true,
          date: "2026-06-29",
          locked: true,
          lockReason: "private_message",
        },
        {
          displayId: "voice-1",
          content: "",
          isFromAI: true,
          date: "2026-06-29",
          locked: true,
          lockReason: "voice_message",
        },
        {
          displayId: "image-1",
          content: "",
          isFromAI: true,
          date: "2026-06-29",
          locked: true,
          imageUrl: "https://example.com/locked.jpg",
          imagePaywalled: true,
          lockReason: "image",
        },
        {
          displayId: "other-1",
          content: "",
          isFromAI: true,
          date: "2026-06-29",
          locked: true,
          lockReason: "insufficient_credits",
        },
        {
          displayId: "user-1",
          content: "user text",
          isFromAI: false,
          date: "2026-06-29",
          locked: true,
          lockReason: "private_message",
        },
      ]),
    ).toBe(3);
  });
});
