import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";

const mockSession = { user: { id: "user-1" } };

vi.mock("@/lib/api-auth", () => ({
  requireSessionOrApiKey: vi.fn(),
}));

const { mockListFindFirst, mockMemberFindFirst, mockMemberFindMany, mockUserFindFirst, mockInsertValues, mockDeleteWhere } = vi.hoisted(() => ({
  mockListFindFirst: vi.fn(),
  mockMemberFindFirst: vi.fn(),
  mockMemberFindMany: vi.fn(),
  mockUserFindFirst: vi.fn(),
  mockInsertValues: vi.fn().mockResolvedValue(undefined),
  mockDeleteWhere: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("@epicure/db", () => ({
  db: {
    query: {
      shoppingLists: { findFirst: mockListFindFirst },
      shoppingListMembers: { findFirst: mockMemberFindFirst, findMany: mockMemberFindMany },
      users: { findFirst: mockUserFindFirst },
    },
    insert: vi.fn(() => ({ values: mockInsertValues })),
    delete: vi.fn(() => ({ where: mockDeleteWhere })),
  },
  shoppingLists: { id: "id", userId: "user_id" },
  shoppingListMembers: { id: "id", listId: "list_id", userId: "user_id" },
  users: { id: "id", email: "email" },
  eq: vi.fn((a, b) => ({ a, b, op: "eq" })),
  and: vi.fn((...args) => ({ args, op: "and" })),
}));

const { requireSessionOrApiKey } = await import("@/lib/api-auth");
import { GET, POST, DELETE } from "../route";

const ctx = { params: Promise.resolve({ id: "list-1" }) };

function makeRequest(method: string, body?: unknown, search = "") {
  return new NextRequest(`http://localhost/api/v1/shopping-lists/list-1/members${search}`, {
    method,
    headers: { "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
  });
}

beforeEach(() => {
  vi.clearAllMocks();
  vi.mocked(requireSessionOrApiKey).mockResolvedValue({ session: mockSession as never, response: null });
});

describe("GET /api/v1/shopping-lists/[id]/members", () => {
  it("returns 404 when the caller is not the owner", async () => {
    mockListFindFirst.mockResolvedValue(undefined);
    const res = await GET(makeRequest("GET"), ctx);
    expect(res.status).toBe(404);
  });

  it("returns the member list for the owner", async () => {
    mockListFindFirst.mockResolvedValue({ id: "list-1", userId: "user-1" });
    mockMemberFindMany.mockResolvedValue([
      { id: "m1", userId: "user-2", role: "viewer", createdAt: new Date(), user: { name: "Bob", username: "bob", avatarUrl: null } },
    ]);
    const res = await GET(makeRequest("GET"), ctx);
    expect(res.status).toBe(200);
    const body = await res.json() as unknown[];
    expect(body).toHaveLength(1);
  });
});

describe("POST /api/v1/shopping-lists/[id]/members", () => {
  beforeEach(() => {
    mockListFindFirst.mockResolvedValue({ id: "list-1", userId: "user-1" });
  });

  it("returns 404 when the caller is not the owner", async () => {
    mockListFindFirst.mockResolvedValue(undefined);
    const res = await POST(makeRequest("POST", { email: "b@test.com", role: "viewer" }), ctx);
    expect(res.status).toBe(404);
  });

  it("returns 400 on invalid body", async () => {
    const res = await POST(makeRequest("POST", { role: "viewer" }), ctx);
    expect(res.status).toBe(400);
  });

  it("returns 404 when the target user doesn't exist", async () => {
    mockUserFindFirst.mockResolvedValue(undefined);
    const res = await POST(makeRequest("POST", { email: "b@test.com", role: "viewer" }), ctx);
    expect(res.status).toBe(404);
  });

  it("returns 400 when inviting yourself", async () => {
    mockUserFindFirst.mockResolvedValue({ id: "user-1", email: "a@test.com" });
    const res = await POST(makeRequest("POST", { email: "a@test.com", role: "viewer" }), ctx);
    expect(res.status).toBe(400);
  });

  it("returns 409 when already a member", async () => {
    mockUserFindFirst.mockResolvedValue({ id: "user-2", email: "b@test.com" });
    mockMemberFindFirst.mockResolvedValue({ id: "existing" });
    const res = await POST(makeRequest("POST", { email: "b@test.com", role: "viewer" }), ctx);
    expect(res.status).toBe(409);
  });

  it("creates the membership on success", async () => {
    mockUserFindFirst.mockResolvedValue({ id: "user-2", email: "b@test.com" });
    mockMemberFindFirst.mockResolvedValue(undefined);
    const res = await POST(makeRequest("POST", { email: "b@test.com", role: "editor" }), ctx);
    expect(res.status).toBe(201);
    expect(mockInsertValues).toHaveBeenCalledWith(expect.objectContaining({ listId: "list-1", userId: "user-2", role: "editor" }));
  });
});

describe("DELETE /api/v1/shopping-lists/[id]/members", () => {
  it("returns 400 when memberId is missing", async () => {
    const res = await DELETE(makeRequest("DELETE"), ctx);
    expect(res.status).toBe(400);
  });

  it("returns 404 when the member doesn't exist", async () => {
    mockMemberFindFirst.mockResolvedValue(undefined);
    const res = await DELETE(makeRequest("DELETE", undefined, "?memberId=m1"), ctx);
    expect(res.status).toBe(404);
  });

  it("returns 403 when caller is neither owner nor the member themselves", async () => {
    mockMemberFindFirst.mockResolvedValue({ id: "m1", userId: "user-2" });
    mockListFindFirst.mockResolvedValue({ id: "list-1", userId: "user-3" });
    const res = await DELETE(makeRequest("DELETE", undefined, "?memberId=m1"), ctx);
    expect(res.status).toBe(403);
  });

  it("allows the owner to remove a member", async () => {
    mockMemberFindFirst.mockResolvedValue({ id: "m1", userId: "user-2" });
    mockListFindFirst.mockResolvedValue({ id: "list-1", userId: "user-1" });
    const res = await DELETE(makeRequest("DELETE", undefined, "?memberId=m1"), ctx);
    expect(res.status).toBe(204);
  });

  it("allows a member to remove themselves", async () => {
    mockMemberFindFirst.mockResolvedValue({ id: "m1", userId: "user-1" });
    mockListFindFirst.mockResolvedValue({ id: "list-1", userId: "user-3" });
    const res = await DELETE(makeRequest("DELETE", undefined, "?memberId=m1"), ctx);
    expect(res.status).toBe(204);
  });
});
