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

const mocks = vi.hoisted(() => {
  const requirePermissionMock = vi.fn()
  const revalidatePathMock = vi.fn()
  const createIdMock = vi.fn()

  const ensureLimitMock = vi.fn()
  const classLimitMock = vi.fn()
  const enrollmentWhereMock = vi.fn()
  const subjectTeacherWhereMock = vi.fn()
  const examFindFirstMock = vi.fn()

  const txInsertValuesMock = vi.fn()
  const txInsertMock = vi.fn(() => ({ values: txInsertValuesMock }))
  const transactionMock = vi.fn(async (callback: (tx: { insert: typeof txInsertMock }) => unknown) =>
    callback({ insert: txInsertMock })
  )

  const schema = {
    classes: { id: "id", teacherId: "teacherId" },
    classEnrollments: { classId: "classId", studentId: "studentId", status: "status" },
    classSubjectTeachers: { classId: "classId", teacherId: "teacherId", subjectId: "subjectId" },
    exams: { id: "id", subjectId: "subjectId", title: "title", structure: "structure" },
    homeworkAnswers: { id: "id" },
    homeworkAssignmentQuestions: { assignmentId: "assignmentId", questionId: "questionId" },
    homeworkAssignmentTargets: { assignmentId: "assignmentId", studentId: "studentId" },
    homeworkAssignments: { id: "id" },
    homeworkSubmissions: { id: "id" },
  }

  return {
    requirePermissionMock,
    revalidatePathMock,
    createIdMock,
    ensureLimitMock,
    classLimitMock,
    enrollmentWhereMock,
    subjectTeacherWhereMock,
    examFindFirstMock,
    txInsertValuesMock,
    txInsertMock,
    transactionMock,
    schema,
  }
})

vi.mock("@/shared/lib/auth-guard", () => ({
  requirePermission: mocks.requirePermissionMock,
  PermissionDeniedError: class PermissionDeniedError extends Error {
    constructor(permission: string) {
      super(`Permission denied: ${permission}`)
      this.name = "PermissionDeniedError"
    }
  },
}))

vi.mock("next/cache", () => ({
  revalidatePath: mocks.revalidatePathMock,
}))

vi.mock("@paralleldrive/cuid2", () => ({
  createId: mocks.createIdMock,
}))

vi.mock("@/shared/db/schema", () => mocks.schema)

vi.mock("@/shared/db", () => ({
  db: {
    select: () => ({
      from: (table: unknown) => {
        if (table === mocks.schema.users) {
          return {
            innerJoin: () => ({
              innerJoin: () => ({
                where: () => ({
                  limit: mocks.ensureLimitMock,
                }),
              }),
            }),
          }
        }

        if (table === mocks.schema.classes) {
          return {
            where: () => ({
              limit: mocks.classLimitMock,
            }),
          }
        }

        if (table === mocks.schema.classEnrollments) {
          return {
            innerJoin: () => ({
              where: mocks.enrollmentWhereMock,
            }),
          }
        }

        if (table === mocks.schema.classSubjectTeachers) {
          return {
            where: mocks.subjectTeacherWhereMock,
          }
        }

        return {
          where: () => ({
            limit: vi.fn().mockResolvedValue([]),
          }),
        }
      },
    }),
    query: {
      exams: {
        findFirst: mocks.examFindFirstMock,
      },
    },
    transaction: mocks.transactionMock,
  },
}))

import { createHomeworkAssignmentAction } from "@/modules/homework/actions"

/** Helper to create a default admin auth context */
function adminCtx() {
  return {
    userId: "u_admin",
    roles: ["admin"],
    permissions: ["homework:create"],
    dataScope: { type: "all" as const },
  }
}

/** Helper to create a teacher auth context */
function teacherCtx(userId = "u_teacher") {
  return {
    userId,
    roles: ["teacher"],
    permissions: ["homework:create"],
    dataScope: { type: "class_taught" as const, classIds: ["class_5"], subjectIds: ["subject_science"] },
  }
}

describe("createHomeworkAssignmentAction", () => {
  beforeEach(() => {
    vi.resetAllMocks()
  })

  it("creates published assignment from exam with targets", async () => {
    mocks.requirePermissionMock.mockResolvedValue(adminCtx())
    mocks.classLimitMock.mockResolvedValue([{ id: "class_1", teacherId: "teacher_1" }])
    mocks.examFindFirstMock.mockResolvedValue({
      id: "exam_1",
      title: "Exam A",
      subjectId: "subject_1",
      structure: { sections: [] },
      questions: [{ questionId: "q_1", score: 10, order: 1 }],
    })
    mocks.enrollmentWhereMock.mockResolvedValue([{ studentId: "stu_1" }, { studentId: "stu_2" }])
    mocks.createIdMock.mockReturnValue("assignment_1")

    const formData = new FormData()
    formData.set("sourceExamId", "exam_1")
    formData.set("classId", "class_1")
    formData.set("publish", "true")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: true, message: "Assignment created", data: "assignment_1" })
    expect(mocks.txInsertValuesMock).toHaveBeenCalledTimes(3)
    expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/assignments")
    expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/submissions")
  })

  it("returns not found when source exam does not exist", async () => {
    mocks.requirePermissionMock.mockResolvedValue(adminCtx())
    mocks.classLimitMock.mockResolvedValue([{ id: "class_1", teacherId: "teacher_1" }])
    mocks.examFindFirstMock.mockResolvedValue(null)

    const formData = new FormData()
    formData.set("sourceExamId", "missing_exam")
    formData.set("classId", "class_1")
    formData.set("publish", "true")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: false, message: "Exam not found" })
    expect(mocks.transactionMock).not.toHaveBeenCalled()
  })

  it("blocks publish when class has no active students", async () => {
    mocks.requirePermissionMock.mockResolvedValue(adminCtx())
    mocks.classLimitMock.mockResolvedValue([{ id: "class_2", teacherId: "teacher_2" }])
    mocks.examFindFirstMock.mockResolvedValue({
      id: "exam_2",
      title: "Exam B",
      subjectId: "subject_2",
      structure: { sections: [] },
      questions: [{ questionId: "q_2", score: 5, order: 1 }],
    })
    mocks.enrollmentWhereMock.mockResolvedValue([])

    const formData = new FormData()
    formData.set("sourceExamId", "exam_2")
    formData.set("classId", "class_2")
    formData.set("publish", "true")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: false, message: "No active students in this class" })
    expect(mocks.transactionMock).not.toHaveBeenCalled()
  })

  it("blocks teacher when not assigned to class", async () => {
    const ctx = teacherCtx("u_teacher")
    mocks.requirePermissionMock.mockResolvedValue(ctx)
    mocks.classLimitMock.mockResolvedValue([{ id: "class_3", teacherId: "owner_teacher" }])
    mocks.examFindFirstMock.mockResolvedValue({
      id: "exam_3",
      title: "Exam C",
      subjectId: "subject_3",
      structure: { sections: [] },
      questions: [{ questionId: "q_3", score: 5, order: 1 }],
    })
    mocks.subjectTeacherWhereMock.mockResolvedValue([])

    const formData = new FormData()
    formData.set("sourceExamId", "exam_3")
    formData.set("classId", "class_3")
    formData.set("publish", "true")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: false, message: "Not assigned to this class" })
    expect(mocks.transactionMock).not.toHaveBeenCalled()
  })

  it("blocks teacher when exam subject is not assigned", async () => {
    const ctx = teacherCtx("u_teacher")
    mocks.requirePermissionMock.mockResolvedValue(ctx)
    mocks.classLimitMock.mockResolvedValue([{ id: "class_4", teacherId: "owner_teacher" }])
    mocks.examFindFirstMock.mockResolvedValue({
      id: "exam_4",
      title: "Exam D",
      subjectId: "subject_math",
      structure: { sections: [] },
      questions: [{ questionId: "q_4", score: 10, order: 1 }],
    })
    mocks.subjectTeacherWhereMock.mockResolvedValue([{ subjectId: "subject_english" }])

    const formData = new FormData()
    formData.set("sourceExamId", "exam_4")
    formData.set("classId", "class_4")
    formData.set("publish", "true")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: false, message: "Not assigned to this subject" })
    expect(mocks.transactionMock).not.toHaveBeenCalled()
  })

  it("allows teacher assigned subject to publish", async () => {
    const ctx = teacherCtx("u_teacher")
    mocks.requirePermissionMock.mockResolvedValue(ctx)
    mocks.classLimitMock.mockResolvedValue([{ id: "class_5", teacherId: "owner_teacher" }])
    mocks.examFindFirstMock.mockResolvedValue({
      id: "exam_5",
      title: "Exam E",
      subjectId: "subject_science",
      structure: { sections: [] },
      questions: [{ questionId: "q_5", score: 8, order: 1 }],
    })
    mocks.subjectTeacherWhereMock.mockResolvedValue([{ subjectId: "subject_science" }])
    mocks.enrollmentWhereMock.mockResolvedValue([{ studentId: "stu_5" }])
    mocks.createIdMock.mockReturnValue("assignment_5")

    const formData = new FormData()
    formData.set("sourceExamId", "exam_5")
    formData.set("classId", "class_5")
    formData.set("publish", "true")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: true, message: "Assignment created", data: "assignment_5" })
    expect(mocks.txInsertValuesMock).toHaveBeenCalledTimes(3)
  })

  it("returns exam subject missing for teacher-assigned class", async () => {
    const ctx = teacherCtx("u_teacher")
    mocks.requirePermissionMock.mockResolvedValue(ctx)
    mocks.classLimitMock.mockResolvedValue([{ id: "class_6", teacherId: "owner_teacher" }])
    mocks.examFindFirstMock.mockResolvedValue({
      id: "exam_6",
      title: "Exam F",
      subjectId: null,
      structure: { sections: [] },
      questions: [{ questionId: "q_6", score: 10, order: 1 }],
    })
    mocks.subjectTeacherWhereMock.mockResolvedValue([{ subjectId: "subject_history" }])

    const formData = new FormData()
    formData.set("sourceExamId", "exam_6")
    formData.set("classId", "class_6")
    formData.set("publish", "true")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: false, message: "Exam subject not set" })
    expect(mocks.transactionMock).not.toHaveBeenCalled()
  })

  it("returns class not found when class is missing", async () => {
    mocks.requirePermissionMock.mockResolvedValue(adminCtx())
    mocks.classLimitMock.mockResolvedValue([])

    const formData = new FormData()
    formData.set("sourceExamId", "exam_7")
    formData.set("classId", "missing_class")
    const result = await createHomeworkAssignmentAction(null, formData)

    expect(result).toEqual({ success: false, message: "Class not found" })
    expect(mocks.transactionMock).not.toHaveBeenCalled()
  })
})
