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

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

interface LoadHistoryOutput {
  messages: UiMessage[];
  hasMore: boolean;
  newOffset: number;
  localOverwritten: boolean;
  localCount: number;
  networkCount: number;
}

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;
}

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 createTestChatMachine(
  options: {
    historyMessages?: UiMessage[];
    unlockHistoryOutput?: UnlockHistoryOutput;
  } = {},
) {
  return chatMachine.provide({
    actors: {
      loadHistory: fromPromise<LoadHistoryOutput>(async () => ({
        messages: options.historyMessages ?? [],
        hasMore: false,
        newOffset: 0,
        localOverwritten: true,
        localCount: 0,
        networkCount: 0,
      })),
      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,
      })),
    },
  });
}

describe("chatMachine transitions", () => {
  it("keeps guest logout disabled while allowing upgrade to user login", 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: "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("ignores guest 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: "ChatGuestLogin" });
    expect(actor.getSnapshot().matches({ userSession: "ready" })).toBe(true);

    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: fromPromise<LoadHistoryOutput>(async () => ({
          messages: [],
          hasMore: false,
          newOffset: 0,
          localOverwritten: true,
          localCount: 0,
          networkCount: 0,
        })),
        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("clears the weekly limit 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: "weekly_limit",
            hint: "Free message limit reached.",
            detail: {
              type: "weekly_msg_limit",
              usedThisWeek: 3,
              limit: 3,
            },
          },
          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("weekly_limit");
    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();
  });
});
