import { describe, it, expect, afterAll } from 'vitest'
import { TRPCError } from '@trpc/server'
import { prisma } from '../setup'
import {
  createTestProgram,
  createTestCompetition,
  createTestRound,
  createTestProject,
  createTestProjectRoundState,
  createTestUser,
  cleanupTestData,
  uid,
} from '../helpers'
import {
  getFinalDocumentStatusForProject,
  sendManualFinalDocReminders,
  sendDueFinalDocReminders,
} from '@/server/services/final-documents'
import * as applicantRouter from '@/server/routers/applicant'
import * as finalistRouter from '@/server/routers/finalist'
import * as mentorRouter from '@/server/routers/mentor'
import { createCaller } from '../setup'
import { BUCKET_NAME, generateObjectKey } from '@/lib/minio'

const programIds: string[] = []

async function makeFinaleProgram(
  opts: { roundStatus?: 'ROUND_ACTIVE' | 'ROUND_DRAFT' | 'ROUND_CLOSED'; closeAt?: Date; skipRequirements?: boolean; uploadsEnabled?: boolean; optionalRequirements?: boolean } = {},
) {
  const program = await createTestProgram()
  programIds.push(program.id)
  const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
  const round = await createTestRound(comp.id, {
    roundType: 'LIVE_FINAL',
    status: opts.roundStatus ?? 'ROUND_ACTIVE',
    sortOrder: 6,
    windowCloseAt: opts.closeAt ?? new Date(Date.now() + 86_400_000),
    configJson: { allowFinalistRevisedUploads: opts.uploadsEnabled ?? true },
  })
  if (opts.skipRequirements) {
    return { program, comp, round, reqPlan: undefined, reqVideo: undefined }
  }
  const reqPlan = await prisma.fileRequirement.create({
    data: { id: uid('req'), roundId: round.id, name: 'Final Business Plan', acceptedMimeTypes: ['application/pdf'], isRequired: !opts.optionalRequirements, sortOrder: 1 },
  })
  const reqVideo = await prisma.fileRequirement.create({
    data: { id: uid('req'), roundId: round.id, name: '1-minute Video', acceptedMimeTypes: ['video/*'], isRequired: !opts.optionalRequirements, sortOrder: 2 },
  })
  return { program, comp, round, reqPlan, reqVideo }
}

describe('getFinalDocumentStatusForProject', () => {
  afterAll(async () => {
    for (const id of programIds) await cleanupTestData(id)
  })

  it('returns null when the project is not enrolled in the active LIVE_FINAL round', async () => {
    const { program } = await makeFinaleProgram()
    const orphan = await createTestProject(program.id)
    const status = await getFinalDocumentStatusForProject(prisma, orphan.id)
    expect(status).toBeNull()
  })

  it('returns per-requirement status with none uploaded', async () => {
    const { program, round } = await makeFinaleProgram()
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status).not.toBeNull()
    expect(status!.requirements).toHaveLength(2)
    expect(status!.requirements.every((r) => !r.uploaded)).toBe(true)
    expect(status!.allRequiredUploaded).toBe(false)
    expect(status!.deadline?.toISOString()).toBe(round.windowCloseAt!.toISOString())
  })

  it('marks a requirement uploaded and flips allRequiredUploaded when all present', async () => {
    const { program, round, reqPlan, reqVideo } = await makeFinaleProgram()
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    for (const [req, type, mime] of [
      [reqPlan!, 'BUSINESS_PLAN', 'application/pdf'],
      [reqVideo!, 'VIDEO', 'video/mp4'],
    ] as const) {
      await prisma.projectFile.create({
        data: {
          id: uid('file'), projectId: project.id, roundId: round.id, requirementId: req.id,
          fileType: type as any, fileName: `f-${req.id}`, mimeType: mime, size: 10,
          bucket: 'b', objectKey: uid('key'),
        },
      })
    }
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status!.requirements.every((r) => r.uploaded)).toBe(true)
    expect(status!.allRequiredUploaded).toBe(true)
  })

  it('works when the LIVE_FINAL round is DRAFT (pre-event — documents open before the round opens)', async () => {
    const { program, round } = await makeFinaleProgram({ roundStatus: 'ROUND_DRAFT' })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status).not.toBeNull()
    expect(status!.roundId).toBe(round.id)
  })

  it('returns null when the LIVE_FINAL round is CLOSED', async () => {
    const { program, round } = await makeFinaleProgram({ roundStatus: 'ROUND_CLOSED' })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status).toBeNull()
  })

  it('returns null when the admin has NOT enabled revised uploads (toggle off)', async () => {
    const { program, round } = await makeFinaleProgram({ uploadsEnabled: false })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status).toBeNull()
  })

  it('reports allRequiredUploaded false when the round has no required requirements', async () => {
    const { program, round } = await makeFinaleProgram({ skipRequirements: true })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status).not.toBeNull()
    expect(status!.requirements).toHaveLength(0)
    expect(status!.allRequiredUploaded).toBe(false)
  })

  it('all-optional round: hasRequired false, allUploaded flips when every slot has a file', async () => {
    const { program, round, reqPlan, reqVideo } = await makeFinaleProgram({ optionalRequirements: true })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)

    const before = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(before!.hasRequired).toBe(false)
    expect(before!.allUploaded).toBe(false)
    expect(before!.allRequiredUploaded).toBe(false)

    for (const req of [reqPlan!, reqVideo!]) {
      await prisma.projectFile.create({
        data: {
          id: uid('file'), projectId: project.id, roundId: round.id, requirementId: req.id,
          fileType: 'SUPPORTING_DOC', fileName: `f-${req.id}`, mimeType: 'application/pdf', size: 10,
          bucket: 'b', objectKey: uid('key'),
        },
      })
    }
    const after = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(after!.hasRequired).toBe(false)
    expect(after!.allUploaded).toBe(true)
  })

  it('mixed round: hasRequired true; allUploaded only when optional slots are filled too', async () => {
    const { program, round, reqPlan } = await makeFinaleProgram()
    await prisma.fileRequirement.update({ where: { id: reqPlan!.id }, data: { isRequired: false } })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status!.hasRequired).toBe(true) // reqVideo still required
    expect(status!.allUploaded).toBe(false)
  })

  it('zero slots: allUploaded false (no vacuous completeness)', async () => {
    const { program, round } = await makeFinaleProgram({ skipRequirements: true })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const status = await getFinalDocumentStatusForProject(prisma, project.id)
    expect(status!.hasRequired).toBe(false)
    expect(status!.allUploaded).toBe(false)
  })
})

describe('applicant.getFinalDocumentStatus', () => {
  const localPrograms: string[] = []
  const localUsers: string[] = []
  afterAll(async () => {
    for (const id of localPrograms) await cleanupTestData(id, localUsers)
  })

  it('returns the status for the caller\'s enrolled finalist project', async () => {
    const program = await createTestProgram()
    localPrograms.push(program.id)
    const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
    const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } })
    await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const user = await createTestUser('APPLICANT')
    localUsers.push(user.id)
    await prisma.teamMember.create({ data: { projectId: project.id, userId: user.id, role: 'LEAD' } })

    const caller = createCaller(applicantRouter.applicantRouter, user)
    const status = await caller.getFinalDocumentStatus()
    expect(status?.roundId).toBe(round.id)
    expect(status?.requirements).toHaveLength(1)
  })

  it('returns null when the caller has no project', async () => {
    const user = await createTestUser('APPLICANT')
    localUsers.push(user.id)
    const caller = createCaller(applicantRouter.applicantRouter, user)
    expect(await caller.getFinalDocumentStatus()).toBeNull()
  })
})

describe('sendManualFinalDocReminders', () => {
  const localPrograms: string[] = []
  const localUsers: string[] = []
  afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) })

  it('sends a reminder only to finalist teams with missing required docs', async () => {
    const program = await createTestProgram()
    localPrograms.push(program.id)
    const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
    const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } })
    await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const lead = await createTestUser('APPLICANT')
    localUsers.push(lead.id)
    await prisma.teamMember.create({ data: { projectId: project.id, userId: lead.id, role: 'LEAD' } })

    const result = await sendManualFinalDocReminders(prisma, { programId: program.id, actorId: lead.id })
    expect(result.sent).toBe(1)
    const notif = await prisma.inAppNotification.findFirst({ where: { userId: lead.id, type: 'GRAND_FINAL_DOCS_REMINDER' } })
    expect(notif).not.toBeNull()
  })
})

describe('sendDueFinalDocReminders', () => {
  const localPrograms: string[] = []
  const localUsers: string[] = []
  afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) })

  it('reminds once when within the window and stamps finalDocsReminderSentAt', async () => {
    const program = await createTestProgram(); localPrograms.push(program.id)
    const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
    const round = await createTestRound(comp.id, {
      roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6,
      windowCloseAt: new Date(Date.now() + 3_600_000), // 1h out → within 48h window
      configJson: { finalDocsReminderHoursBeforeDeadline: 48, allowFinalistRevisedUploads: true },
    })
    await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const lead = await createTestUser('APPLICANT'); localUsers.push(lead.id)
    await prisma.teamMember.create({ data: { projectId: project.id, userId: lead.id, role: 'LEAD' } })
    await prisma.finalistConfirmation.create({ data: { id: uid('fc'), projectId: project.id, status: 'CONFIRMED', category: 'STARTUP', deadline: new Date(Date.now() + 3_600_000), token: uid('tok') } })

    const first = await sendDueFinalDocReminders(prisma)
    expect(first.remindersSent).toBe(1)
    const second = await sendDueFinalDocReminders(prisma)
    expect(second.remindersSent).toBe(0) // idempotent: already stamped
  })
})

describe('finalist.listReviewDocuments', () => {
  const localPrograms: string[] = []
  const localUsers: string[] = []
  afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) })

  async function setup() {
    const program = await createTestProgram()
    localPrograms.push(program.id)
    const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
    const jg = await prisma.juryGroup.create({ data: { id: uid('jg'), competitionId: comp.id, name: 'Finals Jury', slug: uid('jg') } })
    const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } })
    await prisma.round.update({ where: { id: round.id }, data: { juryGroupId: jg.id } })
    await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
    const project = await createTestProject(program.id, { competitionCategory: 'STARTUP' })
    await createTestProjectRoundState(project.id, round.id)
    return { program, comp, jg, round, project }
  }

  it('admin sees all finalist teams', async () => {
    const { program } = await setup()
    const admin = await createTestUser('PROGRAM_ADMIN'); localUsers.push(admin.id)
    const caller = createCaller(finalistRouter.finalistRouter, admin)
    const result = await caller.listReviewDocuments({ programId: program.id })
    expect(result.teams).toHaveLength(1)
    expect(result.totalCount).toBe(1)
  })

  it('a finals jury-group member is allowed', async () => {
    const { program, jg } = await setup()
    const juror = await createTestUser('JURY_MEMBER'); localUsers.push(juror.id)
    await prisma.juryGroupMember.create({ data: { juryGroupId: jg.id, userId: juror.id, role: 'MEMBER' } })
    const caller = createCaller(finalistRouter.finalistRouter, juror)
    const result = await caller.listReviewDocuments({ programId: program.id })
    expect(result.teams).toHaveLength(1)
  })

  it('a non-finals jury member is forbidden', async () => {
    const { program } = await setup()
    const juror = await createTestUser('JURY_MEMBER'); localUsers.push(juror.id)
    const caller = createCaller(finalistRouter.finalistRouter, juror)
    await expect(caller.listReviewDocuments({ programId: program.id })).rejects.toThrow(TRPCError)
  })
})

describe('mentor.getProjectFinalDocuments', () => {
  const localPrograms: string[] = []
  const localUsers: string[] = []
  afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) })

  it('returns status for a project the mentor is assigned to', async () => {
    const program = await createTestProgram(); localPrograms.push(program.id)
    const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
    const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } })
    await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const mentor = await createTestUser('MENTOR'); localUsers.push(mentor.id)
    await prisma.mentorAssignment.create({ data: { projectId: project.id, mentorId: mentor.id } })

    const caller = createCaller(mentorRouter.mentorRouter, mentor)
    const status = await caller.getProjectFinalDocuments({ projectId: project.id })
    expect(status?.roundId).toBe(round.id)
  })

  it('forbids a mentor not assigned to the project', async () => {
    const program = await createTestProgram(); localPrograms.push(program.id)
    const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
    const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6 })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const mentor = await createTestUser('MENTOR'); localUsers.push(mentor.id)
    const caller = createCaller(mentorRouter.mentorRouter, mentor)
    await expect(caller.getProjectFinalDocuments({ projectId: project.id })).rejects.toThrow()
  })
})

describe('saveFileMetadata → GRAND_FINAL_DOCS_SUBMITTED', () => {
  const localPrograms: string[] = []
  const localUsers: string[] = []
  afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) })

  it('notifies the mentor when a finalist uploads a LIVE_FINAL document', async () => {
    const program = await createTestProgram(); localPrograms.push(program.id)
    const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
    const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6 })
    const req = await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
    const project = await createTestProject(program.id)
    await createTestProjectRoundState(project.id, round.id)
    const lead = await createTestUser('APPLICANT'); localUsers.push(lead.id)
    await prisma.teamMember.create({ data: { projectId: project.id, userId: lead.id, role: 'LEAD' } })
    const mentor = await createTestUser('MENTOR'); localUsers.push(mentor.id)
    await prisma.mentorAssignment.create({ data: { projectId: project.id, mentorId: mentor.id } })

    const caller = createCaller(applicantRouter.applicantRouter, lead)
    await caller.saveFileMetadata({
      projectId: project.id, fileName: 'exec.pdf', mimeType: 'application/pdf', size: 100,
      fileType: 'EXEC_SUMMARY', bucket: BUCKET_NAME, objectKey: generateObjectKey(project.title, 'exec.pdf'), roundId: round.id, requirementId: req.id,
    })
    const notif = await prisma.inAppNotification.findFirst({ where: { userId: mentor.id, type: 'GRAND_FINAL_DOCS_SUBMITTED' } })
    expect(notif).not.toBeNull()
  })
})
