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

// --- Mock dependencies ---
const mockSession = {
  user: { id: "user-1", name: "Test User", email: "test@test.com", tier: "free", role: "user" },
};

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

vi.mock("@/lib/tiers", () => ({
  checkTierLimitInTransaction: vi.fn(),
  TierLimitError: class TierLimitError extends Error {},
}));

vi.mock("@/lib/webhooks", () => ({
  dispatchWebhook: vi.fn(),
}));

vi.mock("@/lib/rate-limit", () => ({
  applyRateLimit: vi.fn().mockResolvedValue(null),
}));

const { mockTransaction, mockInsertValues, mockSelectChain } = vi.hoisted(() => {
  const mockInsertValues = vi.fn().mockResolvedValue(undefined);
  const mockSelectChain = {
    from: vi.fn().mockReturnThis(),
    where: vi.fn().mockReturnThis(),
    orderBy: vi.fn().mockReturnThis(),
    limit: vi.fn().mockReturnThis(),
    offset: vi.fn().mockResolvedValue([]),
  };
  return { mockTransaction: vi.fn(), mockInsertValues, mockSelectChain };
});

vi.mock("@epicure/db", () => ({
  db: {
    select: vi.fn(() => mockSelectChain),
    insert: vi.fn(() => ({ values: mockInsertValues })),
    transaction: mockTransaction,
    query: {
      recipes: { findFirst: vi.fn().mockResolvedValue({ id: "new-id", title: "Test Recipe" }) },
    },
  },
  recipes: { id: "id", authorId: "author_id", title: "title", visibility: "visibility" },
  recipeIngredients: {},
  recipeSteps: {},
  eq: vi.fn((a, b) => ({ a, b, op: "eq" })),
  desc: vi.fn((a) => ({ a, op: "desc" })),
  and: vi.fn((...args) => ({ args, op: "and" })),
  count: vi.fn(() => ({ op: "count" })),
  ilike: vi.fn((a, b) => ({ a, b, op: "ilike" })),
  or: vi.fn((...args) => ({ args, op: "or" })),
  sql: vi.fn(),
}));

const { requireSessionOrApiKey } = await import("@/lib/api-auth");

import { GET, POST } from "../route";

function makeRequest(method: string, body?: unknown, search = "") {
  const url = `http://localhost/api/v1/recipes${search}`;
  return new NextRequest(url, {
    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 });
  mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) => {
    const tx = {
      insert: vi.fn(() => ({ values: vi.fn().mockResolvedValue(undefined) })),
    };
    return fn(tx);
  });
});

describe("GET /api/v1/recipes", () => {
  it("returns 200 with empty list", async () => {
    mockSelectChain.offset.mockResolvedValue([]);
    const res = await GET(makeRequest("GET"));
    expect(res.status).toBe(200);
    const body = await res.json() as { data: unknown[] };
    expect(Array.isArray(body.data)).toBe(true);
  });

  it("returns 401 when not authenticated", async () => {
    vi.mocked(requireSessionOrApiKey).mockResolvedValue({
      session: null,
      response: new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }),
    } as never);

    const res = await GET(makeRequest("GET"));
    expect(res.status).toBe(401);
  });
});

describe("POST /api/v1/recipes", () => {
  const validRecipe = {
    title: "Test Recipe",
    baseServings: 4,
    visibility: "private",
    ingredients: [{ rawName: "flour", quantity: "2", unit: "cups" }],
    steps: [{ instruction: "Mix everything" }],
  };

  it("returns 201 on valid recipe creation", async () => {
    const res = await POST(makeRequest("POST", validRecipe));
    expect(res.status).toBe(201);
    const body = await res.json() as { id: string };
    expect(body.id).toBeTruthy();
  });

  it("returns 400 on invalid body (missing title)", async () => {
    const res = await POST(makeRequest("POST", { baseServings: 4 }));
    expect(res.status).toBe(400);
  });

  it("returns 400 when title is empty", async () => {
    const res = await POST(makeRequest("POST", { ...validRecipe, title: "" }));
    expect(res.status).toBe(400);
  });

  it("returns 400 when title is too long", async () => {
    const res = await POST(makeRequest("POST", { ...validRecipe, title: "x".repeat(201) }));
    expect(res.status).toBe(400);
  });

  it("returns 403 when tier limit reached", async () => {
    const { checkTierLimitInTransaction, TierLimitError } = await import("@/lib/tiers");
    vi.mocked(checkTierLimitInTransaction).mockRejectedValue(new TierLimitError("recipe", "free"));

    const res = await POST(makeRequest("POST", validRecipe));
    expect(res.status).toBe(403);
  });

  it("returns 401 when not authenticated", async () => {
    vi.mocked(requireSessionOrApiKey).mockResolvedValue({
      session: null,
      response: new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }),
    } as never);

    const res = await POST(makeRequest("POST", validRecipe));
    expect(res.status).toBe(401);
  });
});
