import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Hono } from 'hono'
import type { AppEnv } from '../src/types'

const TENANT_ID = '00000000-0000-4000-8000-000000000001'
const USER_ID = '00000000-0000-4000-8000-000000000099'
const CUSTOMER_ID = '00000000-0000-4000-8000-000000000010'

const mockGetBulkGeneratePreview = vi.fn()
const mockCreateBulkGenerationJob = vi.fn()
const mockGetBulkGenerationJobStatus = vi.fn()
const mockGetCustomerBillableItems = vi.fn()
const mockCreateInvoice = vi.fn()
const mockListInvoices = vi.fn()

let mockSession: Record<string, unknown> = {
  type: 'user',
  sub: USER_ID,
  tid: TENANT_ID,
  role: 'ADMIN',
  permissions: ['invoices:write', 'invoices:read'],
}

let mockDb: {
  execute: ReturnType<typeof vi.fn>
} = {
  execute: vi.fn(),
}

vi.mock('../src/middleware/guards', () => ({
  requirePermission: (permission: string) => {
    return async (
      c: { get: (key: string) => unknown; json: (body: unknown, status: number) => Response },
      next: () => Promise<void>,
    ) => {
      const session = c.get('session') as { permissions?: string[] } | undefined
      if (!session?.permissions?.includes(permission)) {
        return c.json({ error: 'Forbidden' }, 403)
      }
      await next()
    }
  },
}))

vi.mock('../src/middleware/auth', () => ({
  authMiddleware: async (
    c: { set: (key: string, value: unknown) => void; env: Record<string, unknown> | undefined },
    next: () => Promise<void>,
  ) => {
    c.set('db', mockDb)
    c.set('session', mockSession)
    if (c.env && !c.env.QUEUE) {
      c.env.QUEUE = { send: vi.fn().mockResolvedValue(undefined) }
    }
    await next()
  },
}))

vi.mock('../src/middleware/require-module-enabled', () => ({
  requireModuleEnabled: () => async (_c: unknown, next: () => Promise<void>) => {
    await next()
  },
}))

vi.mock('../src/middleware/bump-financials-version', () => ({
  bumpFinancialsVersionOnWrite: async (_c: unknown, next: () => Promise<void>) => {
    await next()
  },
}))

vi.mock('@zync/db/queries', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@zync/db/queries')>()
  return {
    ...actual,
    getBulkGeneratePreview: (...args: unknown[]) => mockGetBulkGeneratePreview(...args),
    createBulkGenerationJob: (...args: unknown[]) => mockCreateBulkGenerationJob(...args),
    getBulkGenerationJobStatus: (...args: unknown[]) => mockGetBulkGenerationJobStatus(...args),
    getCustomerBillableItems: (...args: unknown[]) => mockGetCustomerBillableItems(...args),
    createInvoice: (...args: unknown[]) => mockCreateInvoice(...args),
    listInvoices: (...args: unknown[]) => mockListInvoices(...args),
    getTenantFieldRules: vi.fn().mockResolvedValue([]),
  }
})

import { invoiceGenerateRoutes } from '../src/routes/invoices/generate'
import { invoiceRoutes } from '../src/routes/invoices/index'

function appWithGenerateRoutes() {
  const app = new Hono<AppEnv>()
  app.route('/api/invoices/bulk-generate', invoiceGenerateRoutes)
  return app
}

function appWithInvoiceRoutes() {
  const app = new Hono<AppEnv>()
  app.route('/api/invoices', invoiceRoutes)
  return app
}

describe('bulk invoice generation routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      role: 'ADMIN',
      permissions: ['invoices:write', 'invoices:read'],
    }
    mockDb = {
      execute: vi.fn(),
    }
    mockGetBulkGeneratePreview.mockResolvedValue({ customers: [] })
    mockCreateBulkGenerationJob.mockResolvedValue('job-1')
    mockGetCustomerBillableItems.mockResolvedValue({
      timeLines: [],
      expenseLines: [],
      milestoneLines: [],
      timeEntryIds: [],
      expenseIds: [],
      milestoneIds: [],
      isEmpty: true,
    })
    mockCreateInvoice.mockResolvedValue({ id: 'inv-1', invoiceNumber: null })
    mockListInvoices.mockResolvedValue({ items: [], total: 0, nextCursor: null })
  })

  it('rejects customer ids outside the tenant on inline generate', async () => {
    mockDb.execute.mockResolvedValueOnce([{ id: CUSTOMER_ID, name: 'Acme Corp' }])

    const res = await appWithGenerateRoutes().request(
      '/api/invoices/bulk-generate',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          periodStart: '2026-05-01',
          periodEnd: '2026-05-31',
          includeTime: true,
          includeExpenses: false,
          includeMilestones: false,
          customerIds: [CUSTOMER_ID, '00000000-0000-4000-8000-000000000011'],
        }),
      },
      { QUEUE: { send: vi.fn().mockResolvedValue(undefined) } } as unknown as AppEnv['Bindings'],
    )

    expect(res.status).toBe(400)
    expect(mockCreateInvoice).not.toHaveBeenCalled()
  })

  it('returns snake_case job_id for background batches', async () => {
    mockDb.execute.mockResolvedValue(
      Array.from({ length: 21 }, (_, index) => ({
        id: `00000000-0000-4000-8000-${String(index + 10).padStart(12, '0')}`,
        name: `Customer ${index + 1}`,
      })),
    )

    const app = appWithGenerateRoutes()
    const res = await app.request(
      '/api/invoices/bulk-generate',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          periodStart: '2026-05-01',
          periodEnd: '2026-05-31',
          includeTime: true,
          includeExpenses: true,
          includeMilestones: false,
          customerIds: Array.from({ length: 21 }, (_, index) =>
            `00000000-0000-4000-8000-${String(index + 10).padStart(12, '0')}`,
          ),
        }),
      },
      { QUEUE: { send: vi.fn().mockResolvedValue(undefined) } } as unknown as AppEnv['Bindings'],
    )

    expect(res.status).toBe(202)
    expect((await res.json()) as { job_id: string }).toEqual({ job_id: 'job-1' })
  })
})

describe('invoice list route', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      role: 'ADMIN',
      permissions: ['invoices:read', 'invoices:write'],
    }
    mockListInvoices.mockResolvedValue({ items: [], total: 0, nextCursor: null })
  })

  it('passes job_id through to listInvoices', async () => {
    const app = appWithInvoiceRoutes()
    const res = await app.request(
      `/api/invoices?status=DRAFT&job_id=00000000-0000-4000-8000-000000000123`,
      { method: 'GET' },
      { QUEUE: { send: vi.fn().mockResolvedValue(undefined) } } as unknown as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    expect(mockListInvoices).toHaveBeenCalledWith(
      expect.anything(),
      TENANT_ID,
      expect.objectContaining({ status: 'DRAFT', jobId: '00000000-0000-4000-8000-000000000123' }),
    )
  })
})
