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 ensureWhereMock = vi.fn(() => ({ limit: ensureLimitMock }))
  const ensureInnerJoinSecondMock = vi.fn(() => ({ where: ensureWhereMock }))
  const ensureInnerJoinFirstMock = vi.fn(() => ({ innerJoin: ensureInnerJoinSecondMock }))
  const countWhereMock = vi.fn()
  const selectFromMock = vi.fn(() => ({ innerJoin: ensureInnerJoinFirstMock, where: countWhereMock }))
  const selectMock = vi.fn(() => ({ from: selectFromMock }))

  const assignmentFindFirstMock = vi.fn()
  const assignmentTargetFindFirstMock = vi.fn()
  const submissionFindFirstMock = vi.fn()
  const txAnswerFindFirstMock = vi.fn()

  const insertValuesMock = vi.fn()
  const insertMock = vi.fn(() => ({ values: insertValuesMock }))
  const txInsertValuesMock = vi.fn()
  const txInsertMock = vi.fn(() => ({ values: txInsertValuesMock }))

  const updateWhereMock = vi.fn()
  const updateSetMock = vi.fn(() => ({ where: updateWhereMock }))
  const updateMock = vi.fn(() => ({ set: updateSetMock }))
  const txUpdateWhereMock = vi.fn()
  const txUpdateSetMock = vi.fn(() => ({ where: txUpdateWhereMock }))
  const txUpdateMock = vi.fn(() => ({ set: txUpdateSetMock }))
  const transactionMock = vi.fn(async (callback: (tx: unknown) => Promise<unknown>) =>
    callback({
      query: { homeworkAnswers: { findFirst: txAnswerFindFirstMock } },
      update: txUpdateMock,
      insert: txInsertMock,
    })
  )

  return {
    requirePermissionMock,
    revalidatePathMock,
    createIdMock,
    ensureLimitMock,
    countWhereMock,
    selectMock,
    assignmentFindFirstMock,
    assignmentTargetFindFirstMock,
    submissionFindFirstMock,
    txAnswerFindFirstMock,
    insertValuesMock,
    insertMock,
    txInsertValuesMock,
    txInsertMock,
    updateSetMock,
    updateMock,
    txUpdateSetMock,
    txUpdateMock,
    transactionMock,
  }
})

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", () => ({
  db: {
    select: mocks.selectMock,
    query: {
      homeworkAssignments: { findFirst: mocks.assignmentFindFirstMock },
      homeworkAssignmentTargets: { findFirst: mocks.assignmentTargetFindFirstMock },
      homeworkSubmissions: { findFirst: mocks.submissionFindFirstMock },
    },
    insert: mocks.insertMock,
    update: mocks.updateMock,
    transaction: mocks.transactionMock,
  },
}))

vi.mock("@/shared/db/schema", () => ({
  classes: { id: "id", teacherId: "teacherId" },
  classEnrollments: { classId: "classId", studentId: "studentId", status: "status" },
  classSubjectTeachers: { classId: "classId", teacherId: "teacherId", subjectId: "subjectId" },
  exams: { id: "id" },
  homeworkAnswers: { id: "id", submissionId: "submissionId", questionId: "questionId" },
  homeworkAssignmentQuestions: { assignmentId: "assignmentId" },
  homeworkAssignmentTargets: { assignmentId: "assignmentId", studentId: "studentId" },
  homeworkAssignments: { id: "id", status: "status" },
  homeworkSubmissions: {
    id: "id",
    assignmentId: "assignmentId",
    studentId: "studentId",
    status: "status",
    submittedAt: "submittedAt",
    isLate: "isLate",
    updatedAt: "updatedAt",
    score: "score",
  },
}))

import {
  gradeHomeworkSubmissionAction,
  saveHomeworkAnswerAction,
  startHomeworkSubmissionAction,
  submitHomeworkAction,
} from "@/modules/homework/actions"

function studentCtx(userId = "u_student") {
  return {
    userId,
    roles: ["student"],
    permissions: ["homework:submit"],
    dataScope: { type: "class_members" as const },
  }
}

function teacherCtx(userId = "u_teacher") {
  return {
    userId,
    roles: ["teacher"],
    permissions: ["homework:grade"],
    dataScope: { type: "class_taught" as const, classIds: [] },
  }
}

describe("homework action flow", () => {
  beforeEach(() => {
    vi.resetAllMocks()
  })

  it("starts submission for assigned student", async () => {
    mocks.requirePermissionMock.mockResolvedValue(studentCtx())
    mocks.assignmentFindFirstMock.mockResolvedValue({
      id: "a_1",
      status: "published",
      availableAt: null,
      maxAttempts: 2,
    })
    mocks.assignmentTargetFindFirstMock.mockResolvedValue({ assignmentId: "a_1", studentId: "u_student" })
    mocks.countWhereMock.mockResolvedValue([{ c: 0 }])
    mocks.createIdMock.mockReturnValue("sub_1")

    const formData = new FormData()
    formData.set("assignmentId", "a_1")
    const result = await startHomeworkSubmissionAction(null, formData)

    expect(result).toEqual({ success: true, message: "Started", data: "sub_1" })
    expect(mocks.insertValuesMock).toHaveBeenCalledWith(
      expect.objectContaining({
        id: "sub_1",
        assignmentId: "a_1",
        studentId: "u_student",
        attemptNo: 1,
        status: "started",
      })
    )
    expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/student/learning/assignments")
  })

  it("blocks submission when assignment is past due", async () => {
    mocks.requirePermissionMock.mockResolvedValue(studentCtx())
    mocks.submissionFindFirstMock.mockResolvedValue({
      id: "sub_1",
      studentId: "u_student",
      status: "started",
      assignment: {
        dueAt: new Date(Date.now() - 60_000),
        allowLate: false,
        lateDueAt: null,
      },
    })

    const formData = new FormData()
    formData.set("submissionId", "sub_1")
    const result = await submitHomeworkAction(null, formData)

    expect(result).toEqual({ success: false, message: "Past due" })
  })

  it("submits started homework before due time", async () => {
    mocks.requirePermissionMock.mockResolvedValue(studentCtx())
    mocks.submissionFindFirstMock.mockResolvedValue({
      id: "sub_2",
      studentId: "u_student",
      status: "started",
      assignment: {
        dueAt: new Date(Date.now() + 60_000),
        allowLate: false,
        lateDueAt: null,
      },
    })

    const formData = new FormData()
    formData.set("submissionId", "sub_2")
    const result = await submitHomeworkAction(null, formData)

    expect(result).toEqual({ success: true, message: "Submitted", data: "sub_2" })
    expect(mocks.updateSetMock).toHaveBeenCalledWith(expect.objectContaining({ status: "submitted", isLate: false }))
    expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/submissions")
    expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/student/learning/assignments")
  })

  it("blocks start when attempts are exhausted", async () => {
    mocks.requirePermissionMock.mockResolvedValue(studentCtx())
    mocks.assignmentFindFirstMock.mockResolvedValue({
      id: "a_2",
      status: "published",
      availableAt: null,
      maxAttempts: 1,
    })
    mocks.assignmentTargetFindFirstMock.mockResolvedValue({ assignmentId: "a_2", studentId: "u_student" })
    mocks.countWhereMock.mockResolvedValue([{ c: 1 }])

    const formData = new FormData()
    formData.set("assignmentId", "a_2")
    const result = await startHomeworkSubmissionAction(null, formData)

    expect(result).toEqual({ success: false, message: "No attempts left" })
  })

  it("grades submission and writes total score", async () => {
    mocks.requirePermissionMock.mockResolvedValue(teacherCtx())

    const formData = new FormData()
    formData.set("submissionId", "sub_1")
    formData.set(
      "answersJson",
      JSON.stringify([
        { id: "ans_1", score: 5, feedback: "good" },
        { id: "ans_2", score: 3, feedback: "" },
      ])
    )

    const result = await gradeHomeworkSubmissionAction(null, formData)

    expect(result).toEqual({ success: true, message: "Grading saved" })
    expect(mocks.updateMock).toHaveBeenCalledTimes(3)
    expect(mocks.updateSetMock).toHaveBeenCalledWith(expect.objectContaining({ score: 8, status: "graded" }))
    expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/submissions")
  })

  it("saves new answer for started submission", async () => {
    mocks.requirePermissionMock.mockResolvedValue(studentCtx())
    mocks.submissionFindFirstMock.mockResolvedValue({
      id: "sub_3",
      studentId: "u_student",
      status: "started",
      assignment: {},
    })
    mocks.txAnswerFindFirstMock.mockResolvedValue(null)
    mocks.createIdMock.mockReturnValue("ans_new")

    const formData = new FormData()
    formData.set("submissionId", "sub_3")
    formData.set("questionId", "q_3")
    formData.set("answerJson", JSON.stringify({ text: "answer content" }))
    const result = await saveHomeworkAnswerAction(null, formData)

    expect(result).toEqual({ success: true, message: "Saved", data: "sub_3" })
    expect(mocks.txInsertValuesMock).toHaveBeenCalledWith(
      expect.objectContaining({
        id: "ans_new",
        submissionId: "sub_3",
        questionId: "q_3",
      })
    )
  })

  it("updates existing answer for started submission", async () => {
    mocks.requirePermissionMock.mockResolvedValue(studentCtx())
    mocks.submissionFindFirstMock.mockResolvedValue({
      id: "sub_4",
      studentId: "u_student",
      status: "started",
      assignment: {},
    })
    mocks.txAnswerFindFirstMock.mockResolvedValue({ id: "ans_existing" })

    const formData = new FormData()
    formData.set("submissionId", "sub_4")
    formData.set("questionId", "q_4")
    formData.set("answerJson", JSON.stringify({ text: "updated answer" }))
    const result = await saveHomeworkAnswerAction(null, formData)

    expect(result).toEqual({ success: true, message: "Saved", data: "sub_4" })
    expect(mocks.txUpdateMock).toHaveBeenCalledTimes(1)
    expect(mocks.txUpdateSetMock).toHaveBeenCalledWith(expect.objectContaining({ answerContent: { text: "updated answer" } }))
  })
})
