import { describe, expect, it } from "vitest";
import { createActor, fromPromise, waitFor } from "xstate";

import { ChatSendResponseSchema } from "@/data/schemas/chat";
import { ApiError } from "@/data/services/api";
import type {
  UnlockMessageOutput,
  UnlockMessageRequest,
} from "@/stores/chat/helper/unlock";

import {
  createTestChatMachine,
  makeUnlockPrivateResponse,
  TEST_CHAT_MACHINE_INPUT,
} from "./chat-machine.test-utils";

describe("chat unlock flow", () => {
  it("prompts before unlocking multiple locked history messages after payment", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            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",
          },
        ],
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({ type: "ChatPaymentSucceeded" });

    expect(actor.getSnapshot().context.unlockHistoryPromptVisible).toBe(true);
    expect(actor.getSnapshot().context.lockedHistoryCount).toBe(2);

    actor.stop();
  });

  it("keeps a single locked image message locked after payment", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "msg-image-locked",
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            imageUrl: "https://example.com/locked.jpg",
            imagePaywalled: true,
            locked: true,
            lockReason: "image",
          },
        ],
        unlockHistoryOutput: {
          unlocked: true,
          reason: "ok",
          shortfallCredits: 0,
          messages: [
            {
              displayId: "msg-image-locked",
              content: "",
              isFromAI: true,
              date: "2026-06-29",
              imageUrl: "https://example.com/unlocked.jpg",
              locked: false,
              lockReason: null,
            },
          ],
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({ type: "ChatPaymentSucceeded" });

    expect(actor.getSnapshot().matches({ userSession: "ready" })).toBe(true);
    expect(actor.getSnapshot().context.unlockHistoryPromptVisible).toBe(false);
    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        displayId: "msg-image-locked",
        imageUrl: "https://example.com/locked.jpg",
        imagePaywalled: true,
        locked: true,
      },
    ]);

    actor.stop();
  });

  it("unlocks history after the prompt is confirmed", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            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",
          },
        ],
        unlockHistoryOutput: {
          unlocked: true,
          reason: "ok",
          shortfallCredits: 0,
          messages: [
            {
              displayId: "unlocked-1",
              content: "unlocked",
              isFromAI: true,
              date: "2026-06-29",
              locked: false,
              lockReason: null,
            },
          ],
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({ type: "ChatPaymentSucceeded" });
    actor.send({ type: "ChatUnlockHistoryConfirmed" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    expect(actor.getSnapshot().context.unlockHistoryPromptVisible).toBe(false);
    expect(actor.getSnapshot().context.messages).toMatchObject([
      { content: "unlocked", locked: false },
    ]);

    actor.stop();
  });

  it("restores the unlock prompt when the history actor fails", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            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",
          },
        ],
        unlockHistoryError: new Error("unlock failed"),
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({ type: "ChatPaymentSucceeded" });
    actor.send({ type: "ChatUnlockHistoryConfirmed" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    expect(actor.getSnapshot().context).toMatchObject({
      unlockHistoryPromptVisible: true,
      unlockHistoryError: "Failed to unlock messages. Please try again.",
    });

    actor.stop();
  });

  it("unlocks a single private message without navigating to payment", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "msg-private-locked",
            content: "Original private message content.",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "private_message",
            lockedPrivate: true,
            privateMessageHint: "A private message is waiting.",
          },
        ],
        unlockMessageOutput: {
          messageId: "msg-private-locked",
          response: makeUnlockPrivateResponse({
            content: "Unlocked private message content.",
          }),
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "msg-private-locked",
      remoteMessageId: "msg-private-locked",
      kind: "private",
    });

    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    expect(actor.getSnapshot().context.unlockPaywallRequest).toBeNull();
    expect(actor.getSnapshot().context.unlockingMessage).toBeNull();
    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        displayId: "msg-private-locked",
        content: "Unlocked private message content.",
        locked: false,
        lockReason: null,
        lockedPrivate: false,
        privateMessageHint: null,
      },
    ]);

    actor.stop();
  });

  it("does not overwrite a user message when it shares the private message id", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "server:msg-shared-id:user",
            remoteId: "msg-shared-id",
            content: "User original question",
            isFromAI: false,
            date: "2026-06-29",
          },
          {
            displayId: "server:msg-shared-id:assistant",
            remoteId: "msg-shared-id",
            content: "Original AI private message",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "private_message",
            lockedPrivate: true,
            privateMessageHint: "A private message is waiting.",
          },
        ],
        unlockMessageOutput: {
          messageId: "msg-shared-id",
          response: makeUnlockPrivateResponse({
            content: "Unlocked private AI message.",
          }),
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "server:msg-shared-id:assistant",
      remoteMessageId: "msg-shared-id",
      kind: "private",
    });

    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        displayId: "server:msg-shared-id:user",
        remoteId: "msg-shared-id",
        content: "User original question",
        isFromAI: false,
      },
      {
        displayId: "server:msg-shared-id:assistant",
        remoteId: "msg-shared-id",
        content: "Unlocked private AI message.",
        isFromAI: true,
        locked: false,
        lockReason: null,
        lockedPrivate: false,
        privateMessageHint: null,
      },
    ]);

    actor.stop();
  });

  it("keeps image message text empty after a single image unlock succeeds", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "msg-image-locked",
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            imageUrl: "https://example.com/locked.jpg",
            imagePaywalled: true,
            locked: true,
            lockReason: "image",
          },
        ],
        unlockMessageOutput: {
          messageId: "msg-image-locked",
          response: makeUnlockPrivateResponse({
            content: "This text should not render below the image.",
          }),
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "msg-image-locked",
      remoteMessageId: "msg-image-locked",
      kind: "image",
    });

    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        displayId: "msg-image-locked",
        content: "",
        imageUrl: "https://example.com/locked.jpg",
        imagePaywalled: false,
        locked: false,
        lockReason: null,
      },
    ]);

    actor.stop();
  });

  it("applies the real voice audio url after unlock succeeds", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "msg-voice-locked",
            content: "Original voice transcript.",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "voice_message",
            privateMessageHint: "A voice message is waiting.",
          },
        ],
        unlockMessageOutput: {
          messageId: "msg-voice-locked",
          response: makeUnlockPrivateResponse({
            content: "This response content must be ignored.",
            audioUrl: "https://example.com/unlocked-voice.mp3",
          }),
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "msg-voice-locked",
      remoteMessageId: "msg-voice-locked",
      kind: "voice",
    });

    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        displayId: "msg-voice-locked",
        content: "Original voice transcript.",
        audioUrl: "https://example.com/unlocked-voice.mp3",
        locked: false,
        lockReason: null,
        privateMessageHint: null,
      },
    ]);

    actor.stop();
  });

  it("requests payment when single message unlock lacks credits", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "msg-voice-locked",
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "voice_message",
            privateMessageHint: "A voice message is waiting.",
          },
        ],
        unlockMessageOutput: {
          messageId: "msg-voice-locked",
          response: makeUnlockPrivateResponse({
            unlocked: false,
            content: "",
            reason: "insufficient_credits",
            creditBalance: 3,
            creditsCharged: 0,
            requiredCredits: 10,
            shortfallCredits: 7,
          }),
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "msg-voice-locked",
      remoteMessageId: "msg-voice-locked",
      kind: "voice",
    });

    await waitFor(
      actor,
      (snapshot) => snapshot.context.unlockPaywallRequest !== null,
    );

    expect(actor.getSnapshot().context.unlockPaywallRequest).toEqual({
      displayMessageId: "msg-voice-locked",
      messageId: "msg-voice-locked",
      kind: "voice",
      reason: "insufficient_credits",
      creditBalance: 3,
      requiredCredits: 10,
      shortfallCredits: 7,
    });
    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        displayId: "msg-voice-locked",
        locked: true,
        lockReason: "voice_message",
      },
    ]);
    expect(actor.getSnapshot().context.messages[0]?.audioUrl).toBeUndefined();
    expect(actor.getSnapshot().context.unlockingMessage).toBeNull();

    actor.stop();
  });

  it("does not request payment when the unlock target is not found", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "msg-missing",
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "private_message",
            privateMessageHint: "A private message is waiting.",
          },
        ],
        unlockMessageOutput: {
          messageId: "msg-missing",
          response: makeUnlockPrivateResponse({
            unlocked: false,
            content: "",
            reason: "not_found",
            creditBalance: 3,
            creditsCharged: 0,
            requiredCredits: 0,
            shortfallCredits: 0,
          }),
        },
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "msg-missing",
      remoteMessageId: "msg-missing",
      kind: "private",
    });

    await waitFor(
      actor,
      (snapshot) => snapshot.context.unlockMessageError === "not_found",
    );

    expect(actor.getSnapshot().context).toMatchObject({
      unlockingMessage: null,
      unlockMessageError: "not_found",
      unlockPaywallRequest: null,
    });
    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        displayId: "msg-missing",
        locked: true,
        lockReason: "private_message",
      },
    ]);

    actor.stop();
  });

  it("refreshes history instead of opening payment on character mismatch", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            displayId: "msg-mismatch",
            content: "",
            isFromAI: true,
            date: "2026-07-20",
            locked: true,
            lockReason: "voice_message",
          },
        ],
        unlockMessageError: new ApiError("HTTP_ERROR", "Mismatch", 409, {
          detail: { errorCode: "CHARACTER_MISMATCH" },
        }),
      }),
      { input: TEST_CHAT_MACHINE_INPUT },
    ).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );
    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "msg-mismatch",
      remoteMessageId: "msg-mismatch",
      kind: "voice",
    });
    await waitFor(
      actor,
      (snapshot) =>
        snapshot.context.characterErrorCode === "CHARACTER_MISMATCH",
    );

    expect(actor.getSnapshot().context.unlockPaywallRequest).toBeNull();
    actor.send({ type: "ChatHistoryRefreshRequested" });
    await waitFor(
      actor,
      (snapshot) => snapshot.context.characterErrorCode === null,
    );
    actor.stop();
  });

  it("owns the active request in one object and ignores history refreshes while unlocking", async () => {
    let capturedRequest: UnlockMessageRequest | null = null;
    let resolveUnlock!: (output: UnlockMessageOutput) => void;
    const unlockDeferred = new Promise<UnlockMessageOutput>((resolve) => {
      resolveUnlock = resolve;
    });
    const originalMessage = {
      displayId: "promotion:lock-1",
      content: "",
      isFromAI: true,
      date: "2026-07-16",
      locked: true,
      lockReason: "image_paywall" as const,
      imagePaywalled: true,
    };
    const machine = createTestChatMachine({
      historyMessages: [originalMessage],
    }).provide({
      actors: {
        unlockMessage: fromPromise<
          UnlockMessageOutput,
          UnlockMessageRequest & { characterId: string }
        >(async ({ input }) => {
          capturedRequest = input;
          return unlockDeferred;
        }),
      },
    });
    const actor = createActor(machine, {
      input: TEST_CHAT_MACHINE_INPUT,
    }).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );
    actor.send({
      type: "ChatUnlockMessageRequested",
      displayMessageId: "promotion:lock-1",
      remoteMessageId: "remote-1",
      kind: "image",
      lockType: "image_paywall",
      clientLockId: "lock-1",
    });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "unlockingMessage" }),
    );

    expect(actor.getSnapshot().context.unlockingMessage).toEqual({
      displayMessageId: "promotion:lock-1",
      messageId: "remote-1",
      kind: "image",
      lockType: "image_paywall",
      clientLockId: "lock-1",
    });
    actor.send({
      type: "ChatNetworkHistoryLoaded",
      output: {
        messages: [],
        localDisplayIds: [],
        localOverwritten: true,
        localCount: 0,
        networkCount: 0,
        total: 0,
        limit: 50,
      },
    });
    expect(actor.getSnapshot().context.messages).toEqual([originalMessage]);

    if (!capturedRequest) throw new Error("unlock actor did not start");
    resolveUnlock({
      displayMessageId: "promotion:lock-1",
      request: capturedRequest,
      response: makeUnlockPrivateResponse({
        messageId: "remote-1",
        image: {
          type: "promotion",
          url: "https://example.com/unlocked.jpg",
        },
      }),
    });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );
    expect(actor.getSnapshot().context.unlockingMessage).toBeNull();

    actor.stop();
  });

  it("clears the insufficient credits prompt after payment succeeds", async () => {
    const actor = createActor(createTestChatMachine(), {
      input: TEST_CHAT_MACHINE_INPUT,
    }).start();

    actor.send({ type: "ChatUserLogin", token: "token" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ userSession: "ready" }),
    );

    actor.send({
      type: "ChatQueuedHttpDone",
      output: {
        response: ChatSendResponseSchema.parse({
          reply: "",
          audioUrl: "",
          messageId: "",
          isGuest: false,
          timestamp: Date.now(),
          image: { type: null, url: null },
          lockDetail: {
            locked: false,
            reason: null,
          },
          canSendMessage: false,
          creditBalance: 0,
          requiredCredits: 2,
          shortfallCredits: 2,
        }),
        reply: null,
      },
    });

    expect(actor.getSnapshot().context.upgradePromptVisible).toBe(true);
    expect(actor.getSnapshot().context.upgradeReason).toBe(
      "insufficient_credits",
    );
    expect(actor.getSnapshot().context.canSendMessage).toBe(false);

    actor.send({ type: "ChatPaymentSucceeded" });

    expect(actor.getSnapshot().context.upgradePromptVisible).toBe(false);
    expect(actor.getSnapshot().context.upgradeReason).toBeNull();
    expect(actor.getSnapshot().context.canSendMessage).toBe(true);

    actor.stop();
  });
});
