/**
 * Tests for KB file authorization (`verifyKBFileAccess` via `verifyFileAccess`).
 *
 * These lock in the security-critical contract: access is granted only when a
 * trusted ownership binding names a workspace the caller can access AND an active
 * document still references the exact key. A planted `document.fileUrl` (the
 * reported vulnerability) can never grant access because ownership comes from the
 * binding, not the document.
 *
 * @vitest-environment node
 */
import { dbChainMock, dbChainMockFns } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockGetFileMetadataByKey, mockGetUserEntityPermissions, mockGetFileMetadata } = vi.hoisted(
  () => ({
    mockGetFileMetadataByKey: vi.fn(),
    mockGetUserEntityPermissions: vi.fn(),
    mockGetFileMetadata: vi.fn(),
  })
)

vi.mock('@sim/db', () => dbChainMock)

vi.mock('@/lib/uploads', () => ({
  getFileMetadata: mockGetFileMetadata,
}))

vi.mock('@/lib/uploads/config', () => ({
  BLOB_CHAT_CONFIG: {},
  S3_CHAT_CONFIG: {},
}))

vi.mock('@/lib/uploads/server/metadata', () => ({
  getFileMetadataByKey: mockGetFileMetadataByKey,
}))

vi.mock('@/lib/uploads/utils/file-utils', () => ({
  inferContextFromKey: vi.fn(() => 'knowledge-base'),
}))

vi.mock('@/lib/workspaces/permissions/utils', () => ({
  getUserEntityPermissions: mockGetUserEntityPermissions,
}))

vi.mock('@/executor/constants', () => ({
  isUuid: vi.fn(() => false),
}))

import { verifyFileAccess, verifyKBFileWriteAccess } from '@/app/api/files/authorization'

const CLOUD_KEY = 'kb/1780162789495-secret.txt'
const USER_ID = 'user-1'

function grantAccess(cloudKey: string) {
  return verifyFileAccess(cloudKey, USER_ID, undefined, 'knowledge-base')
}

describe('verifyKBFileAccess (binding-only)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    // Default liveness query result: one active document references the exact storage key.
    dbChainMockFns.limit.mockResolvedValue([{ id: 'doc-1' }])
  })

  it('grants access when the binding owner workspace is accessible and an active document references the key', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'ws-1', deletedAt: null })
    mockGetUserEntityPermissions.mockResolvedValue('read')

    await expect(grantAccess(CLOUD_KEY)).resolves.toBe(true)
    expect(mockGetUserEntityPermissions).toHaveBeenCalledWith(USER_ID, 'workspace', 'ws-1')
  })

  it('denies when the caller lacks permission on the owner workspace (cross-tenant)', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'victim-ws', deletedAt: null })
    mockGetUserEntityPermissions.mockResolvedValue(null)

    await expect(grantAccess(CLOUD_KEY)).resolves.toBe(false)
  })

  it('denies when there is no ownership binding (planted or un-backfilled key)', async () => {
    mockGetFileMetadataByKey.mockResolvedValue(null)

    await expect(grantAccess(CLOUD_KEY)).resolves.toBe(false)
    // Authorization never consults workspace permissions without a binding.
    expect(mockGetUserEntityPermissions).not.toHaveBeenCalled()
  })

  it('denies when the binding is soft-deleted', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'ws-1', deletedAt: new Date() })

    await expect(grantAccess(CLOUD_KEY)).resolves.toBe(false)
    expect(mockGetUserEntityPermissions).not.toHaveBeenCalled()
  })

  it('denies when the binding has no workspace owner', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: null, deletedAt: null })

    await expect(grantAccess(CLOUD_KEY)).resolves.toBe(false)
    expect(mockGetUserEntityPermissions).not.toHaveBeenCalled()
  })

  it('denies when no active document references the key (archived/soft-deleted KB liveness)', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'ws-1', deletedAt: null })
    mockGetUserEntityPermissions.mockResolvedValue('admin')
    dbChainMockFns.limit.mockResolvedValue([])

    await expect(grantAccess(CLOUD_KEY)).resolves.toBe(false)
  })

  it('fails closed when the binding lookup throws', async () => {
    mockGetFileMetadataByKey.mockRejectedValue(new Error('db down'))

    await expect(grantAccess(CLOUD_KEY)).resolves.toBe(false)
  })
})

describe('verifyKBFileWriteAccess (binding-only delete authorization)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('grants delete when the caller has write on the owner workspace', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'ws-1', deletedAt: null })
    mockGetUserEntityPermissions.mockResolvedValue('write')

    await expect(verifyKBFileWriteAccess(CLOUD_KEY, USER_ID)).resolves.toBe(true)
  })

  it('grants delete when the caller is admin on the owner workspace', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'ws-1', deletedAt: null })
    mockGetUserEntityPermissions.mockResolvedValue('admin')

    await expect(verifyKBFileWriteAccess(CLOUD_KEY, USER_ID)).resolves.toBe(true)
  })

  it('denies delete when the caller has only read on the owner workspace', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'ws-1', deletedAt: null })
    mockGetUserEntityPermissions.mockResolvedValue('read')

    await expect(verifyKBFileWriteAccess(CLOUD_KEY, USER_ID)).resolves.toBe(false)
  })

  it('denies delete when there is no binding (no fallback)', async () => {
    mockGetFileMetadataByKey.mockResolvedValue(null)

    await expect(verifyKBFileWriteAccess(CLOUD_KEY, USER_ID)).resolves.toBe(false)
    expect(mockGetUserEntityPermissions).not.toHaveBeenCalled()
  })

  it('fails closed when the binding lookup throws', async () => {
    mockGetFileMetadataByKey.mockRejectedValue(new Error('db down'))

    await expect(verifyKBFileWriteAccess(CLOUD_KEY, USER_ID)).resolves.toBe(false)
  })
})

describe('public-context access (profile-pictures / og-images / workspace-logos)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  function read(cloudKey: string, context: 'profile-pictures' | 'og-images' | 'workspace-logos') {
    return verifyFileAccess(cloudKey, USER_ID, undefined, context, false)
  }

  function write(cloudKey: string, context: 'profile-pictures' | 'og-images' | 'workspace-logos') {
    return verifyFileAccess(cloudKey, USER_ID, undefined, context, false, { requireWrite: true })
  }

  it('grants public reads without any ownership check', async () => {
    await expect(read('og-images/banner.png', 'og-images')).resolves.toBe(true)
    await expect(read('profile-pictures/123-avatar.png', 'profile-pictures')).resolves.toBe(true)
    await expect(read('workspace-logos/123-logo.png', 'workspace-logos')).resolves.toBe(true)
    expect(mockGetUserEntityPermissions).not.toHaveBeenCalled()
    expect(mockGetFileMetadata).not.toHaveBeenCalled()
  })

  it('denies a cross-tenant delete that names a workspace key under a public context', async () => {
    await expect(write('workspace/victim-ws/123-report.pdf', 'og-images')).resolves.toBe(false)
    expect(mockGetUserEntityPermissions).not.toHaveBeenCalled()
  })

  it('grants a profile-picture delete only to the owning user', async () => {
    mockGetFileMetadata.mockResolvedValue({ userId: USER_ID })
    await expect(write('profile-pictures/123-avatar.png', 'profile-pictures')).resolves.toBe(true)
  })

  it('denies a profile-picture delete for a non-owner', async () => {
    mockGetFileMetadata.mockResolvedValue({ userId: 'other-user' })
    await expect(write('profile-pictures/123-avatar.png', 'profile-pictures')).resolves.toBe(false)
  })

  it('grants a workspace-logo delete to write/admin on the owning workspace', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'ws-1' })
    mockGetUserEntityPermissions.mockResolvedValue('admin')
    await expect(write('workspace-logos/123-logo.png', 'workspace-logos')).resolves.toBe(true)
    expect(mockGetUserEntityPermissions).toHaveBeenCalledWith(USER_ID, 'workspace', 'ws-1')
  })

  it('denies a workspace-logo delete for a non-member of the owning workspace', async () => {
    mockGetFileMetadataByKey.mockResolvedValue({ workspaceId: 'victim-ws' })
    mockGetUserEntityPermissions.mockResolvedValue(null)
    await expect(write('workspace-logos/123-logo.png', 'workspace-logos')).resolves.toBe(false)
  })

  it('denies a workspace-logo delete when no ownership binding exists', async () => {
    mockGetFileMetadataByKey.mockResolvedValue(null)
    await expect(write('workspace-logos/123-logo.png', 'workspace-logos')).resolves.toBe(false)
    expect(mockGetUserEntityPermissions).not.toHaveBeenCalled()
  })
})
