import { describe, it, expect, vi, beforeEach } from "vitest";
import { prismaMock, resetPrismaMock } from "@/__tests__/helpers/prisma-mock";
import { createRequest, parseJsonResponse } from "@/__tests__/helpers/api-test-utils";
import { TEST_USER } from "@/__tests__/helpers/fixtures";
import { ApiError } from "@/lib/api";

vi.mock("@/lib/auth", () => ({
  getAuthUserId: vi.fn().mockResolvedValue("user-1"),
}));

vi.mock("bcryptjs", () => ({
  default: {
    compare: vi.fn(),
    hash: vi.fn().mockResolvedValue("$2a$10$newhash"),
  },
}));

import bcrypt from "bcryptjs";
import { GET, PUT } from "./route";
import { getAuthUserId } from "@/lib/auth";

const mockCtx = { params: Promise.resolve({}) };

beforeEach(() => {
  resetPrismaMock();
  vi.mocked(bcrypt.compare).mockReset();
  vi.mocked(getAuthUserId).mockResolvedValue("user-1");
});

describe("GET /api/user", () => {
  it("returns null when no userId provided", async () => {
    const req = createRequest("/api/user");
    const res = await GET(req, mockCtx);
    const { data } = await parseJsonResponse(res);
    expect(data).toBeNull();
  });

  it("returns null when user not found", async () => {
    prismaMock.user.findUnique.mockResolvedValue(null as never);

    const req = createRequest("/api/user?id=nonexistent");
    const res = await GET(req, mockCtx);
    const { data } = await parseJsonResponse(res);
    expect(data).toBeNull();
  });

  it("returns user info with decision count", async () => {
    prismaMock.user.findUnique.mockResolvedValue(TEST_USER as never);
    prismaMock.decision.count.mockResolvedValue(5 as never);

    const req = createRequest("/api/user?id=user-1");
    const res = await GET(req, mockCtx);
    const { status, data } = await parseJsonResponse(res);

    expect(status).toBe(200);
    expect(data.id).toBe("user-1");
    expect(data.username).toBe("testuser");
    expect(data.decisionCount).toBe(5);
  });
});

describe("PUT /api/user", () => {
  it("updates username", async () => {
    prismaMock.user.findUnique
      .mockResolvedValueOnce(TEST_USER as never)
      .mockResolvedValueOnce(null as never);
    prismaMock.user.update.mockResolvedValue({
      ...TEST_USER,
      username: "newname",
      preferences: "{}",
    } as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { username: "newname" },
    });
    const res = await PUT(req, mockCtx);
    const { status, data } = await parseJsonResponse(res);

    expect(status).toBe(200);
    expect(data.username).toBe("newname");
  });

  it("returns 409 when new username is taken", async () => {
    prismaMock.user.findUnique
      .mockResolvedValueOnce(TEST_USER as never)
      .mockResolvedValueOnce({ id: "other" } as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { username: "takenname" },
    });
    const res = await PUT(req, mockCtx);
    expect(res.status).toBe(409);
  });

  it("updates password with correct current password", async () => {
    prismaMock.user.findUnique.mockResolvedValue(TEST_USER as never);
    vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
    prismaMock.user.update.mockResolvedValue({ ...TEST_USER, preferences: "{}" } as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { currentPassword: "old", newPassword: "newpass123" },
    });
    const res = await PUT(req, mockCtx);
    expect(res.status).toBe(200);
  });

  it("returns 403 when current password is wrong", async () => {
    prismaMock.user.findUnique.mockResolvedValue(TEST_USER as never);
    vi.mocked(bcrypt.compare).mockResolvedValue(false as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { currentPassword: "wrong", newPassword: "newpass123" },
    });
    const res = await PUT(req, mockCtx);
    expect(res.status).toBe(403);
  });

  it("returns 400 when no current password for password change", async () => {
    prismaMock.user.findUnique.mockResolvedValue(TEST_USER as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { newPassword: "newpass123" },
    });
    const res = await PUT(req, mockCtx);
    expect(res.status).toBe(400);
  });

  it("returns 401 when not authenticated", async () => {
    vi.mocked(getAuthUserId).mockRejectedValueOnce(new ApiError("请先登录", 401));
    const req = createRequest("/api/user", {
      method: "PUT",
      body: { username: "test" },
    });
    const res = await PUT(req, mockCtx);
    expect(res.status).toBe(401);
  });

  it("updates avatar", async () => {
    prismaMock.user.findUnique.mockResolvedValue(TEST_USER as never);
    prismaMock.user.update.mockResolvedValue({ ...TEST_USER, avatar: "🦊", preferences: "{}" } as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { avatar: "🦊" },
    });
    const res = await PUT(req, mockCtx);
    const { data } = await parseJsonResponse(res);
    expect(data.avatar).toBe("🦊");
  });

  it("updates email with validation", async () => {
    prismaMock.user.findUnique.mockResolvedValue(TEST_USER as never);
    prismaMock.user.update.mockResolvedValue({
      ...TEST_USER,
      email: "new@example.com",
      preferences: "{}",
    } as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { email: "new@example.com" },
    });
    const res = await PUT(req, mockCtx);
    expect(res.status).toBe(200);
  });

  it("rejects invalid email", async () => {
    prismaMock.user.findUnique.mockResolvedValue(TEST_USER as never);

    const req = createRequest("/api/user", {
      method: "PUT",
      body: { email: "notanemail" },
    });
    const res = await PUT(req, mockCtx);
    expect(res.status).toBe(400);
  });
});
