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

import {
  ChatSendResponse,
  UnlockPrivateResponse,
  type UiMessage,
} from "@/data/dto/chat";
import { chatMachine } from "@/stores/chat/chat-machine";
import type { ChatEvent } from "@/stores/chat/chat-events";

interface LoadMoreHistoryOutput {
  messages: UiMessage[];
  hasMore: boolean;
  newOffset: number;
}

interface SendMessageHttpOutput {
  response: ChatSendResponse;
  reply: UiMessage | null;
}

interface UnlockHistoryOutput {
  unlocked: boolean;
  reason: string;
  shortfallCredits: number;
  messages: UiMessage[];
  hasMore: boolean;
  newOffset: number;
}

interface UnlockMessageOutput {
  messageId: string;
  response: UnlockPrivateResponse;
}

function makeChatSendResponse(): ChatSendResponse {
  return ChatSendResponse.from({
    reply: "",
    audioUrl: "",
    messageId: "msg-1",
    isGuest: false,
    timestamp: Date.now(),
    image: { type: null, url: null },
    lockDetail: {
      locked: false,
      showContent: true,
      showUpgrade: false,
      reason: null,
      hint: null,
      detail: null,
    },
  });
}

function makeUnlockPrivateResponse(
  overrides: Partial<Parameters<typeof UnlockPrivateResponse.from>[0]> = {},
): UnlockPrivateResponse {
  return UnlockPrivateResponse.from({
    unlocked: true,
    content: "unlocked content",
    reason: "ok",
    creditBalance: 90,
    creditsCharged: 10,
    requiredCredits: 10,
    shortfallCredits: 0,
    lockDetail: {
      locked: false,
      showContent: true,
      showUpgrade: false,
      reason: null,
      hint: null,
      detail: null,
    },
    ...overrides,
  });
}

function createTestChatMachine(
  options: {
    historyMessages?: UiMessage[];
    unlockHistoryOutput?: UnlockHistoryOutput;
    unlockMessageOutput?: UnlockMessageOutput;
  } = {},
) {
  return chatMachine.provide({
    actors: {
      loadHistory: createLoadHistoryCallback(options.historyMessages ?? []),
      loadMoreHistory: fromPromise<LoadMoreHistoryOutput, { offset: number }>(
        async () => ({
          messages: [],
          hasMore: false,
          newOffset: 0,
        }),
      ),
      sendMessageHttp: fromPromise<
        SendMessageHttpOutput,
        { content: string }
      >(async () => ({
        response: makeChatSendResponse(),
        reply: null,
      })),
      httpMessageQueue: fromCallback<ChatEvent>(({ receive, sendBack }) => {
        receive((event) => {
          if (event.type !== "ChatSendMessage") return;
          sendBack({ type: "ChatQueuedSendStarted" });
          sendBack({
            type: "ChatQueuedHttpDone",
            output: {
              response: makeChatSendResponse(),
              reply: null,
            },
          });
        });
        return () => undefined;
      }),
      unlockHistory: fromPromise<UnlockHistoryOutput>(async () => ({
        unlocked: true,
        reason: "ok",
        shortfallCredits: 0,
        messages: [],
        hasMore: false,
        newOffset: 0,
        ...options.unlockHistoryOutput,
      })),
      unlockMessage: fromPromise<UnlockMessageOutput, { messageId: string }>(
        async ({ input }) =>
          options.unlockMessageOutput ?? {
            messageId: input.messageId,
            response: makeUnlockPrivateResponse(),
          },
      ),
    },
  });
}

function createLoadHistoryCallback(
  messages: UiMessage[],
  networkMessages: UiMessage[] = messages,
) {
  return fromCallback<ChatEvent>(({ sendBack }) => {
    sendBack({
      type: "ChatLocalHistoryLoaded",
      output: {
        messages,
        hasMore: false,
        newOffset: messages.length,
        localCount: messages.length,
      },
    });
    sendBack({
      type: "ChatNetworkHistoryLoaded",
      output: {
        messages: networkMessages,
        hasMore: false,
        newOffset: networkMessages.length,
        localOverwritten: true,
        localCount: messages.length,
        networkCount: networkMessages.length,
      },
    });
    return () => undefined;
  });
}

describe("chatMachine transitions", () => {
  it("keeps guest logout disabled and supports user to guest transition", async () => {
    const actor = createActor(createTestChatMachine()).start();

    actor.send({ type: "ChatGuestLogin" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ guestSession: "ready" }),
    );

    actor.send({ type: "ChatLogout" });
    expect(actor.getSnapshot().matches({ guestSession: "ready" })).toBe(true);

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

    actor.send({ type: "ChatGuestLogin" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ guestSession: "ready" }),
    );

    actor.stop();
  });

  it("allows explicit logout from user session", async () => {
    const actor = createActor(createTestChatMachine()).start();

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

    actor.send({ type: "ChatLogout" });
    expect(actor.getSnapshot().matches("idle")).toBe(true);

    actor.stop();
  });

  it("enters user ready after history is loaded", async () => {
    const actor = createActor(createTestChatMachine()).start();

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

    const snapshot = actor.getSnapshot();
    expect(snapshot.context.historyLoaded).toBe(true);

    actor.stop();
  });

  it("renders local history before network history sync finishes", async () => {
    let resolveNetwork!: () => void;
    const networkReleased = new Promise<void>((resolve) => {
      resolveNetwork = resolve;
    });
    const localMessage: UiMessage = {
      id: "local-msg",
      content: "cached local message",
      isFromAI: true,
      date: "2026-07-02",
    };
    const networkMessage: UiMessage = {
      id: "network-msg",
      content: "fresh network message",
      isFromAI: true,
      date: "2026-07-02",
    };
    const machine = chatMachine.provide({
      actors: {
        loadHistory: fromCallback<ChatEvent>(({ sendBack }) => {
          sendBack({
            type: "ChatLocalHistoryLoaded",
            output: {
              messages: [localMessage],
              hasMore: false,
              newOffset: 1,
              localCount: 1,
            },
          });
          void networkReleased.then(() => {
            sendBack({
              type: "ChatNetworkHistoryLoaded",
              output: {
                messages: [networkMessage],
                hasMore: false,
                newOffset: 1,
                localOverwritten: true,
                localCount: 1,
                networkCount: 1,
              },
            });
          });
          return () => undefined;
        }),
      },
    });
    const actor = createActor(machine).start();

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

    expect(actor.getSnapshot().context.messages).toMatchObject([
      { id: "local-msg", content: "cached local message" },
    ]);

    resolveNetwork();
    await waitFor(
      actor,
      (snapshot) => snapshot.context.messages[0]?.id === "network-msg",
    );

    expect(actor.getSnapshot().context.messages).toMatchObject([
      { id: "network-msg", content: "fresh network message" },
    ]);

    actor.stop();
  });

  it("switches to guest session when guest login arrives during user session", async () => {
    const actor = createActor(createTestChatMachine()).start();

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

    actor.send({ type: "ChatGuestLogin" });
    await waitFor(actor, (snapshot) =>
      snapshot.matches({ guestSession: "ready" }),
    );

    actor.stop();
  });

  it("ignores repeated user login while an authenticated user session is active", async () => {
    const actor = createActor(createTestChatMachine()).start();

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

    actor.send({ type: "ChatSendMessage", content: "keep this message" });
    await waitFor(
      actor,
      (snapshot) => snapshot.context.messages.length === 1,
    );

    actor.send({ type: "ChatUserLogin", token: "another-token" });

    expect(actor.getSnapshot().matches({ userSession: "ready" })).toBe(true);
    expect(actor.getSnapshot().context.messages).toMatchObject([
      { content: "keep this message", isFromAI: false },
    ]);

    actor.stop();
  });

  it("allows multiple messages to be queued without leaving ready state", async () => {
    const actor = createActor(createTestChatMachine()).start();

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

    actor.send({ type: "ChatSendMessage", content: "hello" });
    actor.send({ type: "ChatSendMessage", content: "still there?" });

    expect(actor.getSnapshot().matches({ userSession: "ready" })).toBe(true);
    expect(actor.getSnapshot().context.messages).toMatchObject([
      { content: "hello", isFromAI: false },
      { content: "still there?", isFromAI: false },
    ]);

    await waitFor(
      actor,
      (snapshot) => snapshot.context.pendingReplyCount === 0,
    );
    expect(actor.getSnapshot().context.isReplyingAI).toBe(false);

    actor.stop();
  });

  it("tracks replying state by queued batch instead of user message count", async () => {
    const machine = chatMachine.provide({
      actors: {
        loadHistory: createLoadHistoryCallback([]),
        loadMoreHistory: fromPromise<LoadMoreHistoryOutput, { offset: number }>(
          async () => ({
            messages: [],
            hasMore: false,
            newOffset: 0,
          }),
        ),
        sendMessageHttp: fromPromise<
          SendMessageHttpOutput,
          { content: string }
        >(async () => ({
          response: makeChatSendResponse(),
          reply: null,
        })),
        httpMessageQueue: fromCallback<ChatEvent>(({ receive, sendBack }) => {
          const pending: string[] = [];
          receive((event) => {
            if (event.type !== "ChatSendMessage") return;
            pending.push(event.content);
            if (pending.length !== 2) return;
            sendBack({ type: "ChatQueuedSendStarted" });
            sendBack({
              type: "ChatQueuedHttpDone",
              output: {
                response: makeChatSendResponse(),
                reply: null,
              },
            });
          });
          return () => undefined;
        }),
        unlockHistory: fromPromise<UnlockHistoryOutput>(async () => ({
          unlocked: true,
          reason: "ok",
          shortfallCredits: 0,
          messages: [],
          hasMore: false,
          newOffset: 0,
        })),
      },
    });
    const actor = createActor(machine).start();

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

    actor.send({ type: "ChatSendMessage", content: "first" });
    actor.send({ type: "ChatSendMessage", content: "second" });

    await waitFor(
      actor,
      (snapshot) => snapshot.context.messages.length === 2,
    );

    expect(actor.getSnapshot().context.pendingReplyCount).toBe(0);
    expect(actor.getSnapshot().context.isReplyingAI).toBe(false);

    actor.stop();
  });

  it("prompts before unlocking multiple locked history messages after payment", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "private_message",
          },
          {
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "voice_message",
          },
        ],
      }),
    ).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("auto unlocks a single locked image message after payment", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            id: "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: [
            {
              id: "msg-image-locked",
              content: "",
              isFromAI: true,
              date: "2026-06-29",
              imageUrl: "https://example.com/unlocked.jpg",
              locked: false,
              lockReason: null,
            },
          ],
          hasMore: false,
          newOffset: 1,
        },
      }),
    ).start();

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

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

    expect(actor.getSnapshot().context.unlockHistoryPromptVisible).toBe(false);
    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        id: "msg-image-locked",
        imageUrl: "https://example.com/unlocked.jpg",
        locked: false,
      },
    ]);

    actor.stop();
  });

  it("unlocks history after the prompt is confirmed", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "private_message",
          },
          {
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            locked: true,
            lockReason: "voice_message",
          },
        ],
        unlockHistoryOutput: {
          unlocked: true,
          reason: "ok",
          shortfallCredits: 0,
          messages: [
            {
              content: "unlocked",
              isFromAI: true,
              date: "2026-06-29",
              locked: false,
              lockReason: null,
            },
          ],
          hasMore: false,
          newOffset: 1,
        },
      }),
    ).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("unlocks a single private message without navigating to payment", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            id: "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: "This response content must be ignored.",
          }),
        },
      }),
    ).start();

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

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

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

    expect(actor.getSnapshot().context.unlockPaywallRequest).toBeNull();
    expect(actor.getSnapshot().context.isUnlockingMessage).toBe(false);
    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        id: "msg-private-locked",
        content: "Original 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: [
          {
            id: "msg-shared-id",
            content: "User original question",
            isFromAI: false,
            date: "2026-06-29",
          },
          {
            id: "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: "This response content must be ignored.",
          }),
        },
      }),
    ).start();

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

    actor.send({
      type: "ChatUnlockMessageRequested",
      messageId: "msg-shared-id",
      kind: "private",
    });

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

    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        id: "msg-shared-id",
        content: "User original question",
        isFromAI: false,
      },
      {
        id: "msg-shared-id",
        content: "Original AI private 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: [
          {
            id: "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.",
          }),
        },
      }),
    ).start();

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

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

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

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

    actor.stop();
  });

  it("keeps voice message content unchanged after unlock succeeds", async () => {
    const actor = createActor(
      createTestChatMachine({
        historyMessages: [
          {
            id: "msg-voice-locked",
            content: "Original voice transcript.",
            isFromAI: true,
            date: "2026-06-29",
            audioUrl: "https://example.com/voice.mp3",
            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.",
          }),
        },
      }),
    ).start();

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

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

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

    expect(actor.getSnapshot().context.messages).toMatchObject([
      {
        id: "msg-voice-locked",
        content: "Original voice transcript.",
        audioUrl: "https://example.com/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: [
          {
            id: "msg-voice-locked",
            content: "",
            isFromAI: true,
            date: "2026-06-29",
            audioUrl: "https://example.com/voice.mp3",
            locked: true,
            lockReason: "voice_message",
            privateMessageHint: "A voice message is waiting.",
          },
        ],
        unlockMessageOutput: {
          messageId: "msg-voice-locked",
          response: makeUnlockPrivateResponse({
            unlocked: false,
            content: "",
            reason: "insufficient_balance",
            creditBalance: 3,
            creditsCharged: 0,
            requiredCredits: 10,
            shortfallCredits: 7,
            lockDetail: {
              locked: true,
              showContent: false,
              showUpgrade: true,
              reason: "insufficient_balance",
              hint: "Insufficient credits.",
              detail: { messageId: "msg-voice-locked" },
            },
          }),
        },
      }),
    ).start();

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

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

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

    expect(actor.getSnapshot().context.unlockPaywallRequest).toEqual({
      messageId: "msg-voice-locked",
      kind: "voice",
      reason: "insufficient_balance",
      creditBalance: 3,
      requiredCredits: 10,
      shortfallCredits: 7,
    });
    expect(actor.getSnapshot().context.isUnlockingMessage).toBe(false);

    actor.stop();
  });

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

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

    actor.send({
      type: "ChatQueuedHttpDone",
      output: {
        response: ChatSendResponse.from({
          reply: "",
          audioUrl: "",
          messageId: "",
          isGuest: false,
          timestamp: Date.now(),
          image: { type: null, url: null },
          lockDetail: {
            locked: true,
            showContent: false,
            showUpgrade: true,
            reason: "insufficient_credits",
            hint: "Insufficient credits.",
            detail: {
              requiredCredits: 2,
              currentCredits: 0,
              shortfallCredits: 2,
            },
          },
          canSendMessage: false,
          cannotSendReason: "insufficient_credits",
          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);
    expect(actor.getSnapshot().context.cannotSendReason).toBeNull();

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