import { beforeEach, describe, expect, it, vi } from "vitest"

const mocks = vi.hoisted(() => {
  const whereMock = vi.fn()
  const innerJoinMock = vi.fn(() => ({ where: whereMock }))
  const fromMock = vi.fn(() => ({ innerJoin: innerJoinMock, where: whereMock }))
  const selectMock = vi.fn(() => ({ from: fromMock }))
  const updateWhereMock = vi.fn()
  const updateSetMock = vi.fn(() => ({ where: updateWhereMock }))
  const updateMock = vi.fn(() => ({ set: updateSetMock }))
  const insertOnDuplicateKeyUpdateMock = vi.fn()
  const insertValuesMock = vi.fn(() => ({ onDuplicateKeyUpdate: insertOnDuplicateKeyUpdateMock }))
  const insertMock = vi.fn(() => ({ values: insertValuesMock }))
  const enrollStudentByInvitationCodeMock = vi.fn()
  return {
    authMock: vi.fn(),
    whereMock,
    fromMock,
    selectMock,
    roleFindFirstMock: vi.fn(),
    updateWhereMock,
    updateSetMock,
    updateMock,
    insertOnDuplicateKeyUpdateMock,
    insertValuesMock,
    insertMock,
    enrollStudentByInvitationCodeMock,
  }
})

vi.mock("@/auth", () => ({
  auth: mocks.authMock,
}))

vi.mock("@/shared/db", () => ({
  db: {
    select: mocks.selectMock,
    query: {
      roles: {
        findFirst: mocks.roleFindFirstMock,
      },
    },
    update: mocks.updateMock,
    insert: mocks.insertMock,
  },
}))

vi.mock("@/shared/db/schema", () => ({
  classes: { id: "id", invitationCode: "invitationCode" },
  classSubjectTeachers: { classId: "classId", subjectId: "subjectId", teacherId: "teacherId", updatedAt: "updatedAt" },
  roles: { id: "id", name: "name" },
  users: { id: "id", onboardedAt: "onboardedAt", name: "name", phone: "phone", address: "address" },
  usersToRoles: { userId: "userId", roleId: "roleId" },
  subjects: { id: "id", name: "name" },
}))

vi.mock("@/modules/classes/data-access", () => ({
  enrollStudentByInvitationCode: mocks.enrollStudentByInvitationCodeMock,
}))

import { POST } from "@/app/api/onboarding/complete/route"

describe("POST /api/onboarding/complete", () => {
  beforeEach(() => {
    vi.resetAllMocks()
    mocks.whereMock.mockResolvedValue([])
    mocks.roleFindFirstMock.mockResolvedValue({ id: "role_student" })
  })

  it("returns 401 when session is missing", async () => {
    mocks.authMock.mockResolvedValue(null)
    const req = new Request("http://localhost/api/onboarding/complete", {
      method: "POST",
      body: JSON.stringify({ role: "student", name: "A" }),
      headers: { "Content-Type": "application/json" },
    })

    const response = await POST(req)
    const data = await response.json()

    expect(response.status).toBe(401)
    expect(data).toEqual({ success: false, message: "Unauthorized" })
  })

  it("returns 400 for invalid payload structure", async () => {
    mocks.authMock.mockResolvedValue({ user: { id: "u_1" } })
    const req = new Request("http://localhost/api/onboarding/complete", {
      method: "POST",
      body: "not-json",
      headers: { "Content-Type": "application/json" },
    })

    const response = await POST(req)
    const data = await response.json()

    expect(response.status).toBe(400)
    expect(data).toEqual({ success: false, message: "Invalid payload" })
  })

  it("returns 400 for invalid role", async () => {
    mocks.authMock.mockResolvedValue({ user: { id: "u_1" } })
    const req = new Request("http://localhost/api/onboarding/complete", {
      method: "POST",
      body: JSON.stringify({ role: "guest", name: "A" }),
      headers: { "Content-Type": "application/json" },
    })

    const response = await POST(req)
    const data = await response.json()

    expect(response.status).toBe(400)
    expect(data).toEqual({ success: false, message: "Invalid role" })
  })

  it("returns 403 when non-admin selects admin role", async () => {
    mocks.authMock.mockResolvedValue({ user: { id: "u_2" } })
    mocks.whereMock.mockResolvedValue([{ name: "teacher" }])
    const req = new Request("http://localhost/api/onboarding/complete", {
      method: "POST",
      body: JSON.stringify({ role: "admin", name: "Admin User" }),
      headers: { "Content-Type": "application/json" },
    })

    const response = await POST(req)
    const data = await response.json()

    expect(response.status).toBe(403)
    expect(data).toEqual({ success: false, message: "Forbidden" })
  })

  it("returns 400 when name is missing", async () => {
    mocks.authMock.mockResolvedValue({ user: { id: "u_3" } })
    const req = new Request("http://localhost/api/onboarding/complete", {
      method: "POST",
      body: JSON.stringify({ role: "student", name: " " }),
      headers: { "Content-Type": "application/json" },
    })

    const response = await POST(req)
    const data = await response.json()

    expect(response.status).toBe(400)
    expect(data).toEqual({ success: false, message: "Name is required" })
  })

  it("completes student onboarding and enrolls deduplicated class codes", async () => {
    mocks.authMock.mockResolvedValue({ user: { id: "u_student" } })
    const req = new Request("http://localhost/api/onboarding/complete", {
      method: "POST",
      body: JSON.stringify({
        role: "student",
        name: "Student A",
        classCodes: "C1, C1;C2  C3",
      }),
      headers: { "Content-Type": "application/json" },
    })

    const response = await POST(req)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data).toEqual({ success: true })
    expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenCalledTimes(3)
    expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenNthCalledWith(1, "u_student", "C1")
    expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenNthCalledWith(2, "u_student", "C2")
    expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenNthCalledWith(3, "u_student", "C3")
    expect(mocks.updateSetMock).toHaveBeenCalled()
    expect(mocks.insertValuesMock).toHaveBeenCalled()
  })

  it("completes teacher onboarding and upserts class-subject mapping", async () => {
    mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } })
    mocks.whereMock
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ id: "class_1", invitationCode: "T1" }])
      .mockResolvedValueOnce([
        { id: "sub_math", name: "数学" },
        { id: "sub_music", name: "音乐" },
      ])
    const req = new Request("http://localhost/api/onboarding/complete", {
      method: "POST",
      body: JSON.stringify({
        role: "teacher",
        name: "Teacher A",
        classCodes: "T1",
        teacherSubjects: ["数学", "音乐", "invalid"],
      }),
      headers: { "Content-Type": "application/json" },
    })

    const response = await POST(req)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data).toEqual({ success: true })
    expect(mocks.enrollStudentByInvitationCodeMock).not.toHaveBeenCalled()
    expect(mocks.insertOnDuplicateKeyUpdateMock).toHaveBeenCalledTimes(3)
  })
})
