/**
 * @vitest-environment node
 */
import { authMockFns, permissionsMock, permissionsMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
  mockIsUsingCloudStorage,
  mockGetStorageProvider,
  mockGetStorageConfig,
  mockCompleteS3MultipartUpload,
  mockCompleteBlobMultipartUpload,
  mockDeriveBlobBlockId,
  mockVerifyUploadToken,
  mockSignUploadToken,
} = vi.hoisted(() => ({
  mockIsUsingCloudStorage: vi.fn(),
  mockGetStorageProvider: vi.fn(),
  mockGetStorageConfig: vi.fn(),
  mockCompleteS3MultipartUpload: vi.fn(),
  mockCompleteBlobMultipartUpload: vi.fn(),
  mockDeriveBlobBlockId: vi.fn(),
  mockVerifyUploadToken: vi.fn(),
  mockSignUploadToken: vi.fn(),
}))

vi.mock('@/lib/uploads', () => ({
  isUsingCloudStorage: mockIsUsingCloudStorage,
  getStorageProvider: mockGetStorageProvider,
  getStorageConfig: mockGetStorageConfig,
}))

vi.mock('@/lib/uploads/core/upload-token', () => ({
  signUploadToken: mockSignUploadToken,
  verifyUploadToken: mockVerifyUploadToken,
}))

vi.mock('@/lib/uploads/providers/s3/client', () => ({
  completeS3MultipartUpload: mockCompleteS3MultipartUpload,
  initiateS3MultipartUpload: mockInitiateS3MultipartUpload,
  getS3MultipartPartUrls: vi.fn(),
  abortS3MultipartUpload: vi.fn(),
}))

vi.mock('@/lib/uploads/providers/blob/client', () => ({
  completeMultipartUpload: mockCompleteBlobMultipartUpload,
  deriveBlobBlockId: mockDeriveBlobBlockId,
  initiateMultipartUpload: vi.fn(),
  getMultipartPartUrls: vi.fn(),
  abortMultipartUpload: vi.fn(),
}))

vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)

const { mockCheckStorageQuota, mockInitiateS3MultipartUpload } = vi.hoisted(() => ({
  mockCheckStorageQuota: vi.fn(),
  mockInitiateS3MultipartUpload: vi.fn(),
}))

vi.mock('@/lib/billing/storage', () => ({
  checkStorageQuota: mockCheckStorageQuota,
}))

import { POST } from '@/app/api/files/multipart/route'

const tokenPayload = {
  uploadId: 'upload-1',
  key: 'workspace/ws-1/123-abc-file.bin',
  userId: 'user-1',
  workspaceId: 'ws-1',
  context: 'workspace' as const,
}

const makeRequest = (action: string, body: unknown) =>
  new NextRequest(`http://localhost/api/files/multipart?action=${action}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })

describe('POST /api/files/multipart action=complete', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
    permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write')
    mockIsUsingCloudStorage.mockReturnValue(true)
    mockGetStorageConfig.mockReturnValue({ bucket: 'b', region: 'r' })
    mockVerifyUploadToken.mockReturnValue({ valid: true, payload: tokenPayload })
    mockSignUploadToken.mockReturnValue('signed-token')
    mockCompleteS3MultipartUpload.mockResolvedValue({
      location: 'loc',
      path: '/api/files/serve/...',
      key: tokenPayload.key,
    })
    mockCompleteBlobMultipartUpload.mockResolvedValue({
      location: 'loc',
      path: '/api/files/serve/...',
      key: tokenPayload.key,
    })
    mockDeriveBlobBlockId.mockImplementation(
      (n: number) => `block-${n.toString().padStart(6, '0')}`
    )
  })

  it('rejects parts without partNumber', async () => {
    mockGetStorageProvider.mockReturnValue('s3')
    const res = await POST(
      makeRequest('complete', {
        uploadToken: 'tok',
        parts: [{ etag: 'abc' }],
      })
    )
    expect(res.status).toBe(400)
    expect(mockCompleteS3MultipartUpload).not.toHaveBeenCalled()
  })

  it('S3 path requires etag and forwards { ETag, PartNumber }', async () => {
    mockGetStorageProvider.mockReturnValue('s3')

    const missingEtag = await POST(
      makeRequest('complete', {
        uploadToken: 'tok',
        parts: [{ partNumber: 1 }],
      })
    )
    expect(missingEtag.status).toBe(500)

    mockCompleteS3MultipartUpload.mockClear()

    const ok = await POST(
      makeRequest('complete', {
        uploadToken: 'tok',
        parts: [
          { partNumber: 1, etag: 'aaa' },
          { partNumber: 2, etag: 'bbb' },
        ],
      })
    )
    expect(ok.status).toBe(200)
    expect(mockCompleteS3MultipartUpload).toHaveBeenCalledWith(
      tokenPayload.key,
      tokenPayload.uploadId,
      [
        { ETag: 'aaa', PartNumber: 1 },
        { ETag: 'bbb', PartNumber: 2 },
      ],
      expect.any(Object)
    )
  })

  it('Blob path derives blockId from partNumber and ignores etag', async () => {
    mockGetStorageProvider.mockReturnValue('blob')
    mockGetStorageConfig.mockReturnValue({
      containerName: 'c',
      accountName: 'a',
      accountKey: 'k',
    })

    const res = await POST(
      makeRequest('complete', {
        uploadToken: 'tok',
        parts: [{ partNumber: 1, etag: 'irrelevant' }, { partNumber: 2 }],
      })
    )

    expect(res.status).toBe(200)
    expect(mockDeriveBlobBlockId).toHaveBeenCalledWith(1)
    expect(mockDeriveBlobBlockId).toHaveBeenCalledWith(2)
    expect(mockCompleteBlobMultipartUpload).toHaveBeenCalledWith(
      tokenPayload.key,
      [
        { partNumber: 1, blockId: 'block-000001' },
        { partNumber: 2, blockId: 'block-000002' },
      ],
      expect.objectContaining({ containerName: 'c' })
    )
  })

  it('returns 403 when token is invalid', async () => {
    mockGetStorageProvider.mockReturnValue('s3')
    mockVerifyUploadToken.mockReturnValueOnce({ valid: false })
    const res = await POST(
      makeRequest('complete', {
        uploadToken: 'bad',
        parts: [{ partNumber: 1, etag: 'a' }],
      })
    )
    expect(res.status).toBe(403)
  })

  it('batch complete normalizes per upload', async () => {
    mockGetStorageProvider.mockReturnValue('s3')
    const res = await POST(
      makeRequest('complete', {
        uploads: [
          {
            uploadToken: 'tok-a',
            parts: [{ partNumber: 1, etag: 'aaa' }],
          },
          {
            uploadToken: 'tok-b',
            parts: [{ partNumber: 1, etag: 'bbb' }],
          },
        ],
      })
    )
    expect(res.status).toBe(200)
    expect(mockCompleteS3MultipartUpload).toHaveBeenCalledTimes(2)
  })
})

describe('POST /api/files/multipart action=initiate quota enforcement', () => {
  const makeInitiateRequest = (body: unknown) =>
    new NextRequest('http://localhost/api/files/multipart?action=initiate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    })

  beforeEach(() => {
    vi.clearAllMocks()
    authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
    permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write')
    mockIsUsingCloudStorage.mockReturnValue(true)
    mockGetStorageProvider.mockReturnValue('s3')
    mockGetStorageConfig.mockReturnValue({ bucket: 'b', region: 'r' })
    mockSignUploadToken.mockReturnValue('signed-token')
    mockCheckStorageQuota.mockResolvedValue({ allowed: true })
    mockInitiateS3MultipartUpload.mockResolvedValue({ uploadId: 'up-1', key: 'k/file.bin' })
  })

  it('blocks upload when fileSize: 0 exceeds quota', async () => {
    mockCheckStorageQuota.mockResolvedValue({ allowed: false, error: 'Storage limit exceeded' })

    const res = await makeInitiateRequest({
      fileName: 'file.bin',
      contentType: 'application/octet-stream',
      fileSize: 0,
      workspaceId: 'ws-1',
      context: 'knowledge-base',
    })

    const response = await POST(res)
    expect(response.status).toBe(413)
    const body = await response.json()
    expect(body.error).toContain('Storage limit exceeded')
  })

  it('allows quota-enforced contexts that pass the quota check', async () => {
    const res = await makeInitiateRequest({
      fileName: 'doc.pdf',
      contentType: 'application/pdf',
      fileSize: 99999,
      workspaceId: 'ws-1',
      context: 'knowledge-base',
    })

    const response = await POST(res)
    expect(response.status).toBe(200)
    expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 99999)
    expect(mockInitiateS3MultipartUpload).toHaveBeenCalled()
  })

  it.each(['og-images', 'profile-pictures', 'workspace-logos', 'logs'])(
    'rejects quota-exempt context %s — not allowed via the multipart endpoint',
    async (context) => {
      const res = await makeInitiateRequest({
        fileName: 'asset.png',
        contentType: 'image/png',
        fileSize: 100 * 1024 * 1024 * 1024,
        workspaceId: 'ws-1',
        context,
      })

      const response = await POST(res)
      expect(response.status).toBe(400)
      const body = await response.json()
      expect(body.error).toMatch(/invalid storage context/i)
      expect(mockCheckStorageQuota).not.toHaveBeenCalled()
      expect(mockInitiateS3MultipartUpload).not.toHaveBeenCalled()
    }
  )
})
