import { describe, it, expect, vi, beforeEach } from "vitest";
import { TRPCError } from "@trpc/server";

// Query mocks
const mockMemberFindFirst = vi.fn();
const mockMemberFindMany = vi.fn();
const mockGroupFindFirst = vi.fn();
const mockInviteFindFirst = vi.fn();
const mockUserFindFirst = vi.fn();

// Insert chain
const mockInsertReturning = vi.fn();

// Delete chain
const mockDeleteWhere = vi.fn();

// Update chain
const mockUpdateSetWhereReturning = vi.fn();

vi.mock("~/server/db", () => ({
  db: {
    query: {
      familyGroupMembers: {
        findFirst: mockMemberFindFirst,
        findMany: mockMemberFindMany,
      },
      familyGroups: {
        findFirst: mockGroupFindFirst,
      },
      invitations: {
        findFirst: mockInviteFindFirst,
      },
      users: {
        findFirst: mockUserFindFirst,
      },
    },
    insert: vi.fn(() => ({
      values: vi.fn(() => ({
        returning: mockInsertReturning,
      })),
    })),
    delete: vi.fn(() => ({
      where: mockDeleteWhere,
    })),
    update: vi.fn(() => ({
      set: vi.fn(() => ({
        where: vi.fn(() => ({
          returning: mockUpdateSetWhereReturning,
        })),
      })),
    })),
    transaction: vi.fn(async (cb: (tx: unknown) => Promise<void>) => {
      const tx = {
        update: vi.fn(() => ({
          set: vi.fn(() => ({
            where: vi.fn(() => ({
              returning: vi.fn(),
            })),
          })),
        })),
        insert: vi.fn(() => ({
          values: vi.fn(() => ({
            returning: vi.fn(),
          })),
        })),
      };
      await cb(tx);
    }),
  },
}));

const mockMember = {
  id: "member-1",
  groupId: "group-1",
  userId: "user-1",
  role: "owner",
  joinedAt: new Date(),
  createdAt: new Date(),
  updatedAt: new Date(),
  user: {
    id: "user-1",
    email: "alice@example.com",
    name: "Alice",
    image: null,
    role: "user",
  },
};

const mockGroup = {
  id: "group-1",
  name: "Test Family",
  ownerId: "user-1",
  createdAt: new Date(),
  updatedAt: new Date(),
  members: [mockMember],
  owner: { id: "user-1", email: "alice@example.com", name: "Alice" },
};

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

describe("getFamilyGroup", () => {
  it("returns family group with members", async () => {
    mockMemberFindFirst.mockResolvedValue({
      group: mockGroup,
    });

    const { getFamilyGroup } = await import("./family.service");
    const result = await getFamilyGroup("user-1");

    expect(result.id).toBe("group-1");
    expect(result.members).toHaveLength(1);
    expect(result.owner.id).toBe("user-1");
  });

  it("throws NOT_FOUND when user has no family group", async () => {
    mockMemberFindFirst.mockResolvedValue(undefined);

    const { getFamilyGroup } = await import("./family.service");
    await expect(getFamilyGroup("nonexistent")).rejects.toThrow(TRPCError);
  });
});

describe("inviteMember", () => {
  it("creates invitation record", async () => {
    mockInviteFindFirst.mockResolvedValue(undefined);
    mockUserFindFirst.mockResolvedValue(undefined);
    const mockInvitation = {
      id: "invite-1",
      groupId: "group-1",
      email: "new@example.com",
      role: "member",
      invitedBy: "user-1",
      status: "pending",
      expiresAt: new Date(),
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    mockInsertReturning.mockResolvedValue([mockInvitation]);

    const { inviteMember } = await import("./family.service");
    const result = await inviteMember("group-1", "new@example.com", "user-1");

    expect(result.id).toBe("invite-1");
    expect(result.email).toBe("new@example.com");
  });

  it("rejects duplicate pending invitations", async () => {
    mockInviteFindFirst.mockResolvedValue({ id: "existing-invite" });

    const { inviteMember } = await import("./family.service");
    await expect(
      inviteMember("group-1", "existing@example.com", "user-1"),
    ).rejects.toMatchObject({ code: "CONFLICT" });
  });

  it("rejects invite for existing group member", async () => {
    mockInviteFindFirst.mockResolvedValue(undefined);
    mockUserFindFirst.mockResolvedValue({ id: "existing-member", email: "existing@example.com" });
    mockMemberFindFirst.mockResolvedValue({ id: "member-exists" });

    const { inviteMember } = await import("./family.service");
    await expect(
      inviteMember("group-1", "existing@example.com", "user-1"),
    ).rejects.toMatchObject({ code: "CONFLICT" });
  });
});

describe("removeMember", () => {
  const mockCallerOwner = {
    ...mockMember,
    role: "owner",
  };
  const mockCallerMember = {
    ...mockMember,
    userId: "user-2",
    role: "member",
  };
  const mockTargetMember = {
    ...mockMember,
    userId: "user-3",
    role: "member",
  };

  it("allows owner to remove a member", async () => {
    mockMemberFindFirst
      .mockResolvedValueOnce(mockCallerOwner)
      .mockResolvedValueOnce(mockTargetMember);
    mockDeleteWhere.mockResolvedValue(undefined);

    const { removeMember } = await import("./family.service");
    await expect(
      removeMember("group-1", "user-3", "user-1"),
    ).resolves.toBeUndefined();
  });

  it("rejects non-admin callers", async () => {
    mockMemberFindFirst.mockResolvedValue(mockCallerMember);

    const { removeMember } = await import("./family.service");
    await expect(
      removeMember("group-1", "user-3", "user-2"),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });

  it("prevents removing the group owner", async () => {
    const mockCallerAdmin = { ...mockCallerOwner, userId: "user-2", role: "admin" };
    const mockOwnerMember = { ...mockMember, userId: "user-1", role: "owner" };
    mockMemberFindFirst
      .mockResolvedValueOnce(mockCallerAdmin)
      .mockResolvedValueOnce(mockOwnerMember);

    const { removeMember } = await import("./family.service");
    await expect(
      removeMember("group-1", "user-1", "user-2"),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });
});

describe("updateMemberRole", () => {
  it("allows owner to update roles", async () => {
    mockMemberFindFirst
      .mockResolvedValueOnce({ ...mockMember, role: "owner" })
      .mockResolvedValueOnce({ ...mockMember, userId: "user-2", role: "member" });

    const updatedMember = { ...mockMember, userId: "user-2", role: "admin" };
    mockUpdateSetWhereReturning.mockResolvedValue([updatedMember]);

    const { updateMemberRole } = await import("./family.service");
    const result = await updateMemberRole("group-1", "user-2", "admin", "user-1");

    expect(result.role).toBe("admin");
  });

  it("rejects non-owner callers", async () => {
    mockMemberFindFirst
      .mockResolvedValueOnce({ ...mockMember, userId: "user-2", role: "member" });

    const { updateMemberRole } = await import("./family.service");
    await expect(
      updateMemberRole("group-1", "user-3", "admin", "user-2"),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });
});

describe("transferOwnership", () => {
  it("transfers ownership to a new owner", async () => {
    mockGroupFindFirst.mockResolvedValue(mockGroup);
    mockMemberFindFirst.mockResolvedValue({ ...mockMember, userId: "user-2", role: "member" });

    const { transferOwnership } = await import("./family.service");
    await expect(
      transferOwnership("group-1", "user-2", "user-1"),
    ).resolves.toBeUndefined();
  });

  it("rejects non-owner callers", async () => {
    mockGroupFindFirst.mockResolvedValue(mockGroup);

    const { transferOwnership } = await import("./family.service");
    await expect(
      transferOwnership("group-1", "user-2", "not-owner"),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });
});
