import { beforeEach, describe, expect, it, vi } 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 USER_ID = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
const ENTITY_ID = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
const ATTACHMENT_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'

const {
  listAttachmentsForEntityMock,
  getAttachmentMock,
  insertAttachmentMock,
  softDeleteAttachmentMock,
  getExpenseMock,
  getVendorMock,
  getTaskMessageByIdMock,
  getTicketMessageByIdMock,
  getArticleByIdMock,
  kvStore,
  queueSendMock,
} = vi.hoisted(() => ({
  listAttachmentsForEntityMock: vi.fn(),
  getAttachmentMock: vi.fn(),
  insertAttachmentMock: vi.fn(),
  softDeleteAttachmentMock: vi.fn(),
  getExpenseMock: vi.fn(),
  getVendorMock: vi.fn(),
  getTaskMessageByIdMock: vi.fn(),
  getTicketMessageByIdMock: vi.fn(),
  getArticleByIdMock: vi.fn(),
  kvStore: new Map<string, string>(),
  queueSendMock: vi.fn(),
}))

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

vi.mock('@zync/db/queries', () => ({
  createDb: vi.fn(() => ({})),
  listAttachmentsForEntity: listAttachmentsForEntityMock,
  getAttachment: getAttachmentMock,
  insertAttachment: insertAttachmentMock,
  softDeleteAttachment: softDeleteAttachmentMock,
  getExpense: getExpenseMock,
  getVendor: getVendorMock,
  getTaskMessageById: getTaskMessageByIdMock,
  getTicketMessageById: getTicketMessageByIdMock,
  kbTenantQuery: vi.fn(() => ({
    getArticleById: getArticleByIdMock,
  })),
}))

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

function makeSession(overrides: Partial<SessionPayload> = {}): SessionPayload {
  return {
    sub: USER_ID,
    tid: TENANT,
    role: 'MEMBER',
    permissions: ['expenses:read', 'expenses:write'],
    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: {
    put: vi.fn().mockResolvedValue(undefined),
    delete: vi.fn().mockResolvedValue(undefined),
    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)
    }),
  },
  QUEUE: {
    send: queueSendMock,
  },
} as unknown as AppEnv['Bindings']

describe('unified attachments spec surface', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    kvStore.clear()
    queueSendMock.mockResolvedValue(undefined)
    getExpenseMock.mockResolvedValue({ id: ENTITY_ID })
    getVendorMock.mockResolvedValue(null)
    getTaskMessageByIdMock.mockResolvedValue(null)
    getTicketMessageByIdMock.mockResolvedValue(null)
    getArticleByIdMock.mockResolvedValue(null)
  })

  it('lists attachments via query params in created_at ascending order without signed urls', async () => {
    listAttachmentsForEntityMock.mockResolvedValue([
      {
        id: 'second',
        tenant_id: TENANT,
        uploader_id: USER_ID,
        entity_type: 'expense',
        entity_id: ENTITY_ID,
        filename: 'second.pdf',
        mime_type: 'application/pdf',
        file_size_bytes: 22,
        r2_key: 'r2/second',
        created_at: '2026-06-10T12:10:00.000Z',
      },
      {
        id: 'first',
        tenant_id: TENANT,
        uploader_id: USER_ID,
        entity_type: 'expense',
        entity_id: ENTITY_ID,
        filename: 'first.pdf',
        mime_type: 'application/pdf',
        file_size_bytes: 11,
        r2_key: 'r2/first',
        created_at: '2026-06-10T12:00:00.000Z',
      },
    ])

    const res = await appForSession(makeSession()).request(
      `/api/attachments?entity_type=expense&entity_id=${ENTITY_ID}`,
      { method: 'GET' },
      mockEnv,
    )

    expect(res.status).toBe(200)
    expect(await res.json()).toEqual({
      attachments: [
        {
          id: 'first',
          filename: 'first.pdf',
          mime_type: 'application/pdf',
          file_size_bytes: 11,
          uploader_id: USER_ID,
          created_at: '2026-06-10T12:00:00.000Z',
        },
        {
          id: 'second',
          filename: 'second.pdf',
          mime_type: 'application/pdf',
          file_size_bytes: 22,
          uploader_id: USER_ID,
          created_at: '2026-06-10T12:10:00.000Z',
        },
      ],
    })
    expect(mockEnv.STORAGE.createPresignedUrl).not.toHaveBeenCalled()
  })

  it('uploads via multipart body fields and returns the spec response shape', async () => {
    insertAttachmentMock.mockResolvedValue({
      id: ATTACHMENT_ID,
      tenant_id: TENANT,
      uploader_id: USER_ID,
      entity_type: 'expense',
      entity_id: ENTITY_ID,
      filename: 'receipt.pdf',
      mime_type: 'application/pdf',
      file_size_bytes: 4,
      r2_key: `${TENANT}/expense/${ENTITY_ID}/${ATTACHMENT_ID}-receipt.pdf`,
      created_at: '2026-06-10T12:00:00.000Z',
    })

    const form = new FormData()
    form.set('entity_type', 'expense')
    form.set('entity_id', ENTITY_ID)
    form.set('file', new File([new Uint8Array([0x25, 0x50, 0x44, 0x46])], 'receipt.pdf', { type: 'application/pdf' }))

    const res = await appForSession(makeSession()).request(
      '/api/attachments',
      { method: 'POST', body: form },
      mockEnv,
    )

    expect(res.status).toBe(201)
    expect(await res.json()).toEqual({
      id: ATTACHMENT_ID,
      filename: 'receipt.pdf',
      mime_type: 'application/pdf',
      file_size_bytes: 4,
      created_at: '2026-06-10T12:00:00.000Z',
    })
    expect(insertAttachmentMock).toHaveBeenCalledWith(
      expect.anything(),
      TENANT,
      expect.objectContaining({
        entity_type: 'expense',
        entity_id: ENTITY_ID,
        filename: 'receipt.pdf',
        mime_type: 'application/pdf',
        file_size_bytes: 4,
      }),
    )
  })

  it('allows admin deletion even without module write permission and evicts the spec cache key', async () => {
    getAttachmentMock.mockResolvedValue({
      id: ATTACHMENT_ID,
      tenant_id: TENANT,
      uploader_id: 'other-user',
      entity_type: 'expense',
      entity_id: ENTITY_ID,
      filename: 'receipt.pdf',
      mime_type: 'application/pdf',
      file_size_bytes: 4,
      r2_key: `${TENANT}/expense/${ENTITY_ID}/${ATTACHMENT_ID}-receipt.pdf`,
      created_at: '2026-06-10T12:00:00.000Z',
    })
    softDeleteAttachmentMock.mockResolvedValue({ r2_key: `${TENANT}/expense/${ENTITY_ID}/${ATTACHMENT_ID}-receipt.pdf` })

    const res = await appForSession(
      makeSession({ role: 'ADMIN', permissions: [] }),
    ).request(`/api/attachments/${ATTACHMENT_ID}`, { method: 'DELETE' }, mockEnv)

    expect(res.status).toBe(204)
    expect(mockEnv.ATTACHMENT_URL_CACHE.delete).toHaveBeenCalledWith(`url:${ATTACHMENT_ID}`)
    expect(queueSendMock).toHaveBeenCalledWith({
      type: 'r2.delete',
      r2Key: `${TENANT}/expense/${ENTITY_ID}/${ATTACHMENT_ID}-receipt.pdf`,
      tenantId: TENANT,
    })
  })
})
