import { describe, it, expect, vi, beforeEach } from "vitest";
import { initTRPC, TRPCError } from "@trpc/server";
import { wrap } from "@typeschema/valibot";
import { UpdateUserSchema, InviteMemberSchema, RemoveMemberSchema, UpdateRoleSchema } from "../schemas/user";

vi.mock("~/server/services/user.service", () => ({
  getUserById: vi.fn(),
  updateUser: vi.fn(),
  deleteUser: vi.fn(),
}));

vi.mock("~/server/services/family.service", () => ({
  getFamilyGroup: vi.fn(),
  inviteMember: vi.fn(),
  removeMember: vi.fn(),
  updateMemberRole: vi.fn(),
}));

import { getUserById, updateUser, deleteUser } from "~/server/services/user.service";
import { getFamilyGroup, inviteMember, removeMember, updateMemberRole } from "~/server/services/family.service";

const mockGetUserById = vi.mocked(getUserById);
const mockUpdateUser = vi.mocked(updateUser);
const mockDeleteUser = vi.mocked(deleteUser);
const mockGetFamilyGroup = vi.mocked(getFamilyGroup);
const mockInviteMember = vi.mocked(inviteMember);
const mockRemoveMember = vi.mocked(removeMember);
const mockUpdateMemberRole = vi.mocked(updateMemberRole);

type User = {
  id: string; email: string; name: string | null; image: string | null;
  role: "user" | "family_admin" | "family_member" | "support"; emailVerified: Date | null; deletedAt: Date | null;
  stripeCustomerId: string | null;
  createdAt: Date; updatedAt: Date;
};
type Ctx = { db: object; user: User | null; apiKey: string | null };

function createCaller(user: User | null) {
  const t = initTRPC.context<Ctx>().create();
  const isAuthed = t.middleware(({ ctx, next }) => {
    if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
    return next({ ctx: { ...ctx, user: ctx.user } });
  });

  const router = t.router({
    me: t.procedure.use(isAuthed).query(async ({ ctx }) => {
      return mockGetUserById(ctx.user.id);
    }),
    update: t.procedure.use(isAuthed)
      .input(wrap(UpdateUserSchema))
      .mutation(async ({ ctx, input }) => {
        return mockUpdateUser(ctx.user.id, input as Record<string, unknown>);
      }),
    delete: t.procedure.use(isAuthed)
      .mutation(async ({ ctx }) => {
        await mockDeleteUser(ctx.user.id);
        return { success: true };
      }),
    listFamilyMembers: t.procedure.use(isAuthed)
      .query(async ({ ctx }) => {
        const group = await mockGetFamilyGroup(ctx.user.id);
        return group.members;
      }),
    inviteFamilyMember: t.procedure.use(isAuthed)
      .input(wrap(InviteMemberSchema))
      .mutation(async ({ ctx, input }) => {
        const group = await mockGetFamilyGroup(ctx.user.id);
        const callerMember = group.members.find(
          (m: { userId: string }) => m.userId === ctx.user.id,
        );
        if (!callerMember || (callerMember.role !== "owner" && callerMember.role !== "admin")) {
          throw new TRPCError({ code: "FORBIDDEN" });
        }
        const i = input as { email: string; role: "admin" | "member" };
        return mockInviteMember(group.id, i.email, ctx.user.id, i.role);
      }),
    removeFamilyMember: t.procedure.use(isAuthed)
      .input(wrap(RemoveMemberSchema))
      .mutation(async ({ ctx, input }) => {
        const group = await mockGetFamilyGroup(ctx.user.id);
        const i = input as { userId: string };
        await mockRemoveMember(group.id, i.userId, ctx.user.id);
        return { success: true };
      }),
    updateFamilyMemberRole: t.procedure.use(isAuthed)
      .input(wrap(UpdateRoleSchema))
      .mutation(async ({ ctx, input }) => {
        const group = await mockGetFamilyGroup(ctx.user.id);
        const i = input as { userId: string; role: "owner" | "admin" | "member" };
        return mockUpdateMemberRole(group.id, i.userId, i.role, ctx.user.id);
      }),
  });

  const caller = t.createCallerFactory(router);
  return caller({ db: {} as never, user, apiKey: null });
}

const baseUser: User = {
  id: "user-1", email: "a@b.com", name: "Test", image: null,
  role: "user", emailVerified: null, deletedAt: null,
  stripeCustomerId: null,
  createdAt: new Date(), updatedAt: new Date(),
};

function makeUser(overrides: Partial<User> = {}): User {
  return { ...baseUser, ...overrides };
}

const now = new Date();

beforeEach(() => {
  vi.clearAllMocks();
});

describe("user.me", () => {
  it("returns authenticated user", async () => {
    mockGetUserById.mockResolvedValue({
      ...makeUser(), accounts: [], sessions: [], deviceTokens: [],
      familyGroups: [], familyGroupOwned: [], subscriptions: [],
    });
    const api = createCaller(makeUser());
    expect((await api.me()).id).toBe("user-1");
  });

  it("rejects unauthenticated", async () => {
    const api = createCaller(null);
    await expect(api.me()).rejects.toThrow(TRPCError);
  });
});

describe("user.update", () => {
  it("updates name", async () => {
    mockUpdateUser.mockResolvedValue(makeUser({ name: "New" }));
    const api = createCaller(makeUser());
    expect((await api.update({ name: "New" })).name).toBe("New");
  });

  it("updates email", async () => {
    mockUpdateUser.mockResolvedValue(makeUser({ email: "new@b.com" }));
    const api = createCaller(makeUser());
    expect((await api.update({ email: "new@b.com" })).email).toBe("new@b.com");
  });
});

describe("user.delete", () => {
  it("deletes user", async () => {
    mockDeleteUser.mockResolvedValue(makeUser({ deletedAt: now }));
    const api = createCaller(makeUser());
    expect((await api.delete()).success).toBe(true);
  });
});

describe("user.listFamilyMembers", () => {
  it("returns family members", async () => {
    mockGetFamilyGroup.mockResolvedValue({
      id: "g1", name: "Fam", ownerId: "u1", createdAt: now, updatedAt: now,
      members: [{
        id: "m1", groupId: "g1", userId: "u1", role: "owner",
        joinedAt: now, createdAt: now, updatedAt: now,
        user: { id: "u1", email: "a@b.com", name: "A", image: null, role: "user" } as unknown as User,
      }],
      owner: makeUser(),
    });
    const api = createCaller(makeUser());
    const result = await api.listFamilyMembers();
    expect(result).toHaveLength(1);
    expect(result[0].role).toBe("owner");
  });
});

describe("user.inviteFamilyMember", () => {
  it("creates invitation", async () => {
    mockGetFamilyGroup.mockResolvedValue({
      id: "g1", name: "Fam", ownerId: "user-1", createdAt: now, updatedAt: now,
      members: [{
        id: "m1", groupId: "g1", userId: "user-1", role: "owner",
        joinedAt: now, createdAt: now, updatedAt: now,
        user: { id: "user-1", email: "a@b.com", name: "A", image: null, role: "user" } as unknown as User,
      }],
      owner: makeUser(),
    });
    mockInviteMember.mockResolvedValue({
      id: "i1", groupId: "g1", email: "new@b.com", role: "member",
      invitedBy: "user-1", status: "pending", expiresAt: now, createdAt: now, updatedAt: now,
    });
    const api = createCaller(makeUser());
    expect((await api.inviteFamilyMember({ email: "new@b.com" })).status).toBe("pending");
  });

  it("rejects non-admin", async () => {
    mockGetFamilyGroup.mockResolvedValue({
      id: "g1", name: "Fam", ownerId: "u1", createdAt: now, updatedAt: now,
      members: [{
        id: "m1", groupId: "g1", userId: "u2", role: "member",
        joinedAt: now, createdAt: now, updatedAt: now,
        user: { id: "u2", email: "b@b.com", name: "B", image: null, role: "user" } as unknown as User,
      }],
      owner: makeUser(),
    });
    const api = createCaller(makeUser({ id: "u2" }));
    await expect(api.inviteFamilyMember({ email: "new@b.com" })).rejects.toThrow(TRPCError);
  });
});

describe("user.removeFamilyMember", () => {
  it("removes a member", async () => {
    mockGetFamilyGroup.mockResolvedValue({
      id: "g1", name: "Fam", ownerId: "u1", createdAt: now, updatedAt: now,
      members: [{
        id: "m1", groupId: "g1", userId: "u1", role: "owner",
        joinedAt: now, createdAt: now, updatedAt: now,
        user: { id: "u1", email: "a@b.com", name: "A", image: null, role: "user" } as unknown as User,
      }],
      owner: makeUser(),
    });
    mockRemoveMember.mockResolvedValue(undefined);
    const api = createCaller(makeUser());
    expect((await api.removeFamilyMember({ userId: "u3" })).success).toBe(true);
  });
});

describe("user.updateFamilyMemberRole", () => {
  it("updates role", async () => {
    mockGetFamilyGroup.mockResolvedValue({
      id: "g1", name: "Fam", ownerId: "u1", createdAt: now, updatedAt: now,
      members: [{
        id: "m1", groupId: "g1", userId: "u1", role: "owner",
        joinedAt: now, createdAt: now, updatedAt: now,
        user: { id: "u1", email: "a@b.com", name: "A", image: null, role: "user" } as unknown as User,
      }],
      owner: makeUser(),
    });
    mockUpdateMemberRole.mockResolvedValue({
      id: "m2", groupId: "g1", userId: "u2", role: "admin",
      joinedAt: now, createdAt: now, updatedAt: now,
    });
    const api = createCaller(makeUser());
    expect(
      (await api.updateFamilyMemberRole({ userId: "u2", role: "admin" })).role,
    ).toBe("admin");
  });
});
