import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { Readable } from 'node:stream'

import { assertCanReadUploadedFile } from '../src/lite/api.mjs'
import {
  canAdminReadUploadedFile,
  canUserReadUploadedFile,
  markUploadedFilesOwner,
  normalizeUploadedFileOwnerType,
  uploadedFileJson,
} from '../src/lite/uploaded-files-repository.mjs'
import { rawUploadMaxBytes } from '../src/lite/file-storage.mjs'

function requestFromChunks(chunks, headers = {}) {
  const request = Readable.from(chunks)
  request.headers = headers
  return request
}

async function withRawUploadEnv(overrides, action) {
  const previous = {
    FILE_BUCKET_DIR: process.env.FILE_BUCKET_DIR,
    RAW_UPLOAD_MAX_BYTES: process.env.RAW_UPLOAD_MAX_BYTES,
    OBJECT_STORAGE_DRIVER: process.env.OBJECT_STORAGE_DRIVER,
    S3_ENDPOINT: process.env.S3_ENDPOINT,
    S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
    S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
    S3_BUCKET_PRIVATE: process.env.S3_BUCKET_PRIVATE,
  }
  Object.assign(process.env, overrides)
  try {
    return await action()
  } finally {
    for (const [key, value] of Object.entries(previous)) {
      if (value === undefined) delete process.env[key]
      else process.env[key] = value
    }
  }
}

test('private uploaded file without owner type is forbidden instead of anonymously readable', async () => {
  const file = {
    id: 'orphan-file',
    visibility: 'private',
    ownerType: '',
    ownerId: '',
  }

  await assert.rejects(
    () => assertCanReadUploadedFile({ headers: {} }, file),
    (error) => error.statusCode === 403 && error.code === 'file_forbidden',
  )
  assert.equal(await canAdminReadUploadedFile(file, (permission) => permission === '*'), false)
  assert.equal(await canUserReadUploadedFile(file, 'user-1'), false)
})

test('withdrawal proof owner type accepts legacy expert_withdrawal_proof records', async () => {
  assert.equal(normalizeUploadedFileOwnerType('expert_withdrawal_proof'), 'withdrawal_proof')
  assert.equal(uploadedFileJson({ id: 'file-1', ownerType: 'expert_withdrawal_proof' }).ownerType, 'withdrawal_proof')
  assert.equal(
    await canAdminReadUploadedFile(
      { visibility: 'private', ownerType: 'expert_withdrawal_proof' },
      (permission) => permission === 'settlement:read',
    ),
    true,
  )
})

test('uploaded file owner reassignment requires an explicit current owner guard', async () => {
  await assert.rejects(
    () => markUploadedFilesOwner(['11111111-1111-4111-8111-111111111111'], {
      ownerType: 'expert_material',
      ownerId: 'user-1',
      visibility: 'private',
    }),
    (error) => error.statusCode === 500 && error.code === 'uploaded_file_owner_guard_required',
  )
})

test('uploaded file owner reassignment rejects files outside the current owner guard', async () => {
  const previousPrisma = globalThis.__dlbdi_prisma_client__
  let updateArgs = null
  globalThis.__dlbdi_prisma_client__ = {
    uploadedFile: {
      updateMany: async (args) => {
        updateArgs = args
        return { count: 0 }
      },
    },
  }
  try {
    await assert.rejects(
      () => markUploadedFilesOwner(['22222222-2222-4222-8222-222222222222'], {
        ownerType: 'expert_material',
        ownerId: 'user-2',
        visibility: 'private',
        currentOwnerTypes: ['user_upload', 'expert_material'],
        currentOwnerId: 'user-2',
      }),
      (error) => error.statusCode === 403 && error.code === 'uploaded_file_owner_forbidden',
    )
    assert.deepEqual(updateArgs?.where?.OR, [{ ownerId: 'user-2' }])
    assert.deepEqual(updateArgs?.where?.ownerType, { in: ['user_upload', 'expert_material'] })
  } finally {
    if (previousPrisma === undefined) delete globalThis.__dlbdi_prisma_client__
    else globalThis.__dlbdi_prisma_client__ = previousPrisma
  }
})

test('raw upload default limit is 1GB', () => {
  const previous = process.env.RAW_UPLOAD_MAX_BYTES
  delete process.env.RAW_UPLOAD_MAX_BYTES
  try {
    assert.equal(rawUploadMaxBytes(), 1024 * 1024 * 1024)
  } finally {
    if (previous === undefined) delete process.env.RAW_UPLOAD_MAX_BYTES
    else process.env.RAW_UPLOAD_MAX_BYTES = previous
  }
})

test('buffer compatibility uploads are materialized into file storage instead of database payloads', async () => {
  const bucketDir = await mkdtemp(path.join(os.tmpdir(), 'dlbdi-buffer-upload-'))
  try {
    await withRawUploadEnv({
      FILE_BUCKET_DIR: bucketDir,
      OBJECT_STORAGE_DRIVER: '',
      S3_ENDPOINT: '',
      S3_ACCESS_KEY_ID: '',
      S3_SECRET_ACCESS_KEY: '',
    }, async () => {
      const storage = await import(`../src/lite/file-storage.mjs?buffer-upload=${Date.now()}`)
      const stored = await storage.saveBufferToFileStorage(Buffer.from('hello bucket'), 'legacy.txt', 'text/plain')
      assert.equal(stored.storageDriver, 'local')
      assert.equal(stored.contentBase64 || '', '')
      assert.equal(stored.fileSize, 12)
      assert.equal(await readFile(stored.storagePath, 'utf8'), 'hello bucket')
    })
  } finally {
    await rm(bucketDir, { recursive: true, force: true })
  }
})

test('raw local uploads reject streamed bodies over RAW_UPLOAD_MAX_BYTES with 413', async () => {
  const bucketDir = await mkdtemp(path.join(os.tmpdir(), 'dlbdi-local-upload-'))
  try {
    await withRawUploadEnv({
      FILE_BUCKET_DIR: bucketDir,
      RAW_UPLOAD_MAX_BYTES: '1024',
      OBJECT_STORAGE_DRIVER: '',
      S3_ENDPOINT: '',
      S3_ACCESS_KEY_ID: '',
      S3_SECRET_ACCESS_KEY: '',
    }, async () => {
      const storage = await import(`../src/lite/file-storage.mjs?local-limit=${Date.now()}`)
      await assert.rejects(
        () => storage.saveRequestBodyToLocalBucket(
          requestFromChunks([Buffer.alloc(800), Buffer.alloc(300)], {
            'content-type': 'application/octet-stream',
          }),
          'too-large.bin',
        ),
        (error) => error.statusCode === 413 && error.code === 'request_entity_too_large',
      )
      assert.deepEqual(await readdir(bucketDir), [])
    })
  } finally {
    await rm(bucketDir, { recursive: true, force: true })
  }
})

test('raw S3 uploads reject streamed bodies over RAW_UPLOAD_MAX_BYTES before object PUT', async () => {
  const bucketDir = await mkdtemp(path.join(os.tmpdir(), 'dlbdi-s3-upload-'))
  const originalFetch = globalThis.fetch
  let fetchCalled = false
  globalThis.fetch = async () => {
    fetchCalled = true
    return { ok: true }
  }
  try {
    await withRawUploadEnv({
      FILE_BUCKET_DIR: bucketDir,
      RAW_UPLOAD_MAX_BYTES: '1024',
      OBJECT_STORAGE_DRIVER: 's3',
      S3_ENDPOINT: 'http://127.0.0.1:9000',
      S3_ACCESS_KEY_ID: 'test-access-key',
      S3_SECRET_ACCESS_KEY: 'test-secret-key',
      S3_BUCKET_PRIVATE: 'private-test-bucket',
    }, async () => {
      const storage = await import(`../src/lite/file-storage.mjs?s3-limit=${Date.now()}`)
      await assert.rejects(
        () => storage.saveRequestBodyToLocalBucket(
          requestFromChunks([Buffer.alloc(700), Buffer.alloc(400)], {
            'content-type': 'application/octet-stream',
          }),
          'too-large.bin',
        ),
        (error) => error.statusCode === 413 && error.code === 'request_entity_too_large',
      )
      assert.equal(fetchCalled, false)
      assert.deepEqual(await readdir(path.join(bucketDir, '.tmp')).catch(() => []), [])
    })
  } finally {
    globalThis.fetch = originalFetch
    await rm(bucketDir, { recursive: true, force: true })
  }
})
