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

import type { UserView } from "@/data/dto/user";
import { userMachine } from "@/stores/user/user-machine";
import type {
  InitData,
  UserFetchData,
} from "@/stores/user/user-machine.helpers";

function makeUser(overrides: Partial<UserView> = {}): UserView {
  return {
    id: "user-1",
    username: "Elio Fan",
    email: "user@example.com",
    country: "Hong Kong",
    countryCode: "HK",
    avatarUrl: "https://example.com/avatar.png",
    intimacy: 42,
    dolBalance: 7,
    creditBalance: 7,
    vipExpiresAt: null,
    relationshipStage: "close_friend",
    currentMood: "happy",
    isGuest: false,
    isVip: false,
    voiceMinutesRemaining: 12,
    ...overrides,
  };
}

function makeEntitlementSnapshot(
  overrides: Partial<InitData["snapshot"]> = {},
): NonNullable<InitData["snapshot"]> {
  return {
    isVip: false,
    creditBalance: 7,
    ...overrides,
  };
}

function createTestUserMachine(
  overrides: Partial<{
    initData: InitData;
    fetchedUser: UserView | null;
    fetchedData: UserFetchData;
    logoutSpy: () => void;
  }> = {},
) {
  const logoutSpy = overrides.logoutSpy ?? vi.fn<() => void>();

  return userMachine.provide({
    actors: {
      userInit: fromPromise<InitData>(async () => ({
        user: overrides.initData?.user ?? makeUser(),
        avatarUrl:
          overrides.initData?.avatarUrl ?? "https://example.com/avatar.png",
        snapshot: overrides.initData?.snapshot ?? null,
      })),
      userFetch: fromPromise<UserFetchData>(async () => {
        if ("fetchedData" in overrides && overrides.fetchedData) {
          return overrides.fetchedData;
        }
        if ("fetchedUser" in overrides) {
          return {
            user: overrides.fetchedUser ?? null,
            snapshot: null,
          };
        }
        return {
          user: makeUser({ username: "Fetched" }),
          snapshot: null,
        };
      }),
      userLogout: fromPromise<void>(async () => {
        logoutSpy();
      }),
    },
  });
}

describe("userMachine", () => {
  it("initializes user and avatar from local storage actor output", async () => {
    const actor = createActor(
      createTestUserMachine({
        initData: {
          user: makeUser({ username: "Local User" }),
          avatarUrl: "https://example.com/local-avatar.png",
          snapshot: null,
        },
        fetchedUser: null,
      }),
    ).start();

    actor.send({ type: "UserInit" });
    await waitFor(actor, (snapshot) => snapshot.matches("idle"));

    expect(actor.getSnapshot().context.currentUser?.username).toBe("Local User");
    expect(actor.getSnapshot().context.avatarUrl).toBe(
      "https://example.com/local-avatar.png",
    );

    actor.stop();
  });

  it("updates user profile synchronously", () => {
    const actor = createActor(createTestUserMachine()).start();

    actor.send({ type: "UserUpdate", user: makeUser({ username: "After" }) });

    expect(actor.getSnapshot().context.currentUser?.username).toBe("After");

    actor.stop();
  });

  it("fetches the latest user and clears loading state when done", async () => {
    const actor = createActor(
      createTestUserMachine({
        fetchedData: {
          user: makeUser({ username: "Fresh User" }),
          snapshot: makeEntitlementSnapshot({
            isVip: true,
            creditBalance: 120,
          }),
        },
      }),
    ).start();

    actor.send({ type: "UserFetch" });
    expect(actor.getSnapshot().context.isLoading).toBe(true);

    await waitFor(actor, (snapshot) => snapshot.matches("idle"));

    const context = actor.getSnapshot().context;
    expect(context.currentUser?.username).toBe("Fresh User");
    expect(context.currentUser?.isVip).toBe(true);
    expect(context.currentUser?.creditBalance).toBe(120);
    expect(context.isVip).toBe(true);
    expect(context.creditBalance).toBe(120);
    expect(context.isLoading).toBe(false);

    actor.stop();
  });

  it("keeps the current user when fetch returns null", async () => {
    const actor = createActor(
      createTestUserMachine({
        fetchedUser: null,
      }),
    ).start();

    actor.send({ type: "UserUpdate", user: makeUser({ username: "Cached" }) });
    actor.send({ type: "UserFetch" });
    await waitFor(actor, (snapshot) => snapshot.matches("idle"));

    const context = actor.getSnapshot().context;
    expect(context.currentUser?.username).toBe("Cached");
    expect(context.isLoading).toBe(false);

    actor.stop();
  });

  it("clears user data after logout", async () => {
    const logoutSpy = vi.fn<() => void>();
    const actor = createActor(createTestUserMachine({ logoutSpy })).start();

    actor.send({ type: "UserUpdate", user: makeUser() });
    actor.send({ type: "UserLogout" });
    await waitFor(actor, (snapshot) => snapshot.matches("idle"));

    const context = actor.getSnapshot().context;
    expect(logoutSpy).toHaveBeenCalledOnce();
    expect(context.currentUser).toBeNull();
    expect(context.avatarUrl).toBeNull();

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