/**
 * Unified attachment URL authz tests — S8-002 / S8-003 regression.
 */
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { Hono } from 'hono'
import type { SessionPayload } from '@zync/types'
import type { AppEnv } from '../src/types'

const TENANT = '11111111-1111-1111-1111-111111111111'
const ATTACHMENT_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
const USER_A = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
const USER_B = 'cccccccc-cccc-cccc-cccc-cccccccccccc'

const mockAttachment = {
  id: ATTACHMENT_ID,
  tenant_id: TENANT,
  uploader_id: USER_A,
  entity_type: 'expense' as const,
  entity_id: 'dddddddd-dddd-dddd-dddd-dddddddddddd',
  filename: 'receipt.pdf',
  mime_type: 'application/pdf',
  file_size_bytes: 1024,
  r2_key: `${TENANT}/expense/dddddddd-dddd-dddd-dddd-dddddddddddd/${ATTACHMENT_ID}-receipt.pdf`,
  created_at: '2026-06-10T12:00:00.000Z',
}

const { getAttachmentMock, kvStore } = vi.hoisted(() => {
  const getAttachmentMock = vi.fn()
  const kvStore = new Map<string, string>()
  return { getAttachmentMock, kvStore }
})

vi.mock('../src/middleware/auth', () => ({
  authMiddleware: async (_c: unknown, next: () => Promise<void>) => next(),
}))

vi.mock('@zync/db/queries', () => ({
  createDb: vi.fn(() => ({})),
  getAttachment: getAttachmentMock,
  listAttachmentsForEntity: vi.fn(),
  insertAttachment: vi.fn(),
  softDeleteAttachment: vi.fn(),
}))

import { unifiedAttachmentsRouter } from '../src/routes/unified-attachments'

function makeSession(overrides: Partial<SessionPayload> = {}): SessionPayload {
  return {
    sub: USER_B,
    tid: TENANT,
    role: 'MEMBER',
    permissions: [],
    tier: 'business',
    type: 'user',
    v: 1,
    enforce_2fa: false,
    two_factor_verified: false,
    exp: Math.floor(Date.now() / 1000) + 3600,
    iat: Math.floor(Date.now() / 1000),
    ...overrides,
  }
}

function appForSession(session: SessionPayload) {
  const app = new Hono<AppEnv>()
  app.use('*', async (c, next) => {
    c.set('session', session)
    c.set('db', {} as AppEnv['Variables']['db'])
    await next()
  })
  app.route('/api/attachments', unifiedAttachmentsRouter)
  return app
}

const mockEnv = {
  STORAGE: {
    createPresignedUrl: vi.fn().mockResolvedValue('https://r2.example/signed'),
  },
  ATTACHMENT_URL_CACHE: {
    get: vi.fn(async (key: string) => kvStore.get(key) ?? null),
    put: vi.fn(async (key: string, value: string) => {
      kvStore.set(key, value)
    }),
    delete: vi.fn(async (key: string) => {
      kvStore.delete(key)
    }),
  },
} as unknown as AppEnv['Bindings']

describe('GET /api/attachments/:id/url authz (S8-002, S8-003)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    kvStore.clear()
    getAttachmentMock.mockResolvedValue(mockAttachment)
  })

  it('returns 403 on fresh path when caller lacks entity read permission', async () => {
    const res = await appForSession(makeSession({ permissions: ['tasks:read'] })).request(
      `/api/attachments/${ATTACHMENT_ID}/url`,
      { method: 'GET' },
      mockEnv,
    )

    expect(res.status).toBe(403)
    const body = (await res.json()) as { error: string }
    expect(body.error).toBe('FORBIDDEN')
    expect(mockEnv.STORAGE.createPresignedUrl).not.toHaveBeenCalled()
    expect(mockEnv.ATTACHMENT_URL_CACHE.get).not.toHaveBeenCalled()
  })

  it('returns 403 on cached path when caller lacks entity read permission', async () => {
    const cacheKey = `url:${ATTACHMENT_ID}`
    kvStore.set(
      cacheKey,
      JSON.stringify({
        url: 'https://r2.example/cached',
        expires_at: '2026-06-10T13:00:00.000Z',
      }),
    )

    const res = await appForSession(makeSession({ permissions: ['tasks:read'] })).request(
      `/api/attachments/${ATTACHMENT_ID}/url`,
      { method: 'GET' },
      mockEnv,
    )

    expect(res.status).toBe(403)
    const body = (await res.json()) as { error: string }
    expect(body.error).toBe('FORBIDDEN')
    expect(mockEnv.ATTACHMENT_URL_CACHE.get).not.toHaveBeenCalled()
  })

  it('does not serve another user cached URL without read permission', async () => {
    const userACacheKey = `url:${ATTACHMENT_ID}`
    kvStore.set(
      userACacheKey,
      JSON.stringify({
        url: 'https://r2.example/user-a-cached',
        expires_at: '2026-06-10T13:00:00.000Z',
      }),
    )

    const res = await appForSession(
      makeSession({ sub: USER_B, permissions: ['tasks:read'] }),
    ).request(`/api/attachments/${ATTACHMENT_ID}/url`, { method: 'GET' }, mockEnv)

    expect(res.status).toBe(403)
    expect(mockEnv.ATTACHMENT_URL_CACHE.get).not.toHaveBeenCalled()
  })

  it('returns cached URL for authorized caller after authz check', async () => {
    const cacheKey = `url:${ATTACHMENT_ID}`
    kvStore.set(
      cacheKey,
      JSON.stringify({
        url: 'https://r2.example/cached',
        expires_at: '2026-06-10T13:00:00.000Z',
      }),
    )

    const res = await appForSession(makeSession({ permissions: ['expenses:read'] })).request(
      `/api/attachments/${ATTACHMENT_ID}/url`,
      { method: 'GET' },
      mockEnv,
    )

    expect(res.status).toBe(200)
    const body = (await res.json()) as { url: string; expires_at: string }
    expect(body.url).toBe('https://r2.example/cached')
    expect(mockEnv.ATTACHMENT_URL_CACHE.get).toHaveBeenCalledWith(cacheKey)
    expect(mockEnv.STORAGE.createPresignedUrl).not.toHaveBeenCalled()
  })
})
