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 INVOICE_ID = '00000000-0000-4000-8000-0000000000aa'
const EXPENSE_ID = '00000000-0000-4000-8000-0000000000bb'
const CUSTOMER_ID = '00000000-0000-4000-8000-0000000000cc'

const mockListExpenses = vi.fn()
const mockGetExpenseForInvoicing = vi.fn()
const mockAddExpenseToInvoiceLine = vi.fn()
const mockGetInvoiceWithLines = vi.fn()
const mockGetInvoice = vi.fn()

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

const mockDb = {
  transaction: async <T,>(fn: (tx: unknown) => Promise<T>) => 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 }, next: () => Promise<void>) => {
    c.set('db', mockDb)
    c.set('session', mockSession)
    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,
    listExpenses: (...args: unknown[]) => mockListExpenses(...args),
    getExpenseForInvoicing: (...args: unknown[]) => mockGetExpenseForInvoicing(...args),
    addExpenseToInvoiceLine: (...args: unknown[]) => mockAddExpenseToInvoiceLine(...args),
    getInvoiceWithLines: (...args: unknown[]) => mockGetInvoiceWithLines(...args),
    getInvoice: (...args: unknown[]) => mockGetInvoice(...args),
  }
})

import { expenseRoutes } from '../src/routes/expenses/index'
import { invoiceRoutes } from '../src/routes/invoices/index'

function expenseApp() {
  const app = new Hono<AppEnv>()
  app.route('/api/expenses', expenseRoutes)
  return app
}

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

describe('expense-to-invoice-line routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['expenses:read', 'invoices:write'],
      tier: 'business',
    }
    mockListExpenses.mockResolvedValue({ items: [], nextCursor: null, total: 0 })
    mockGetExpenseForInvoicing.mockResolvedValue({
      id: EXPENSE_ID,
      tenantId: TENANT_ID,
      projectId: '00000000-0000-4000-8000-000000000011',
      customerId: CUSTOMER_ID,
      description: 'Office supplies',
      amount: '347.50',
      expenseDate: '2026-05-28',
      vendorName: 'Supersol Deal',
      billedAt: null,
      invoiceId: null,
      status: 'COMPLETED',
    })
    mockGetInvoice.mockResolvedValue({
      id: INVOICE_ID,
      tenantId: TENANT_ID,
      customerId: CUSTOMER_ID,
      projectId: null,
      invoiceNumber: null,
      proformaNumber: 'INV-0042',
      status: 'DRAFT',
      currency: 'ILS',
      issueDate: null,
      taxIssueDate: null,
      dueDate: null,
      vatRate: '0.18',
      subtotal: '0.00',
      vatAmount: '0.00',
      total: '0.00',
      amountPaid: '0.00',
      notes: null,
      source: 'manual',
      sentAt: null,
      approvedAt: null,
      approvedBy: null,
      approvalNote: null,
      rejectionReason: null,
      rejectionNotifyCustomer: false,
      taxIssuedAt: null,
      paidAt: null,
      externalId: null,
      externalProvider: null,
      voidReason: null,
      voidedAt: null,
      voidedBy: null,
      parentInvoiceId: null,
      htmlSnapshotUrl: null,
      createdBy: USER_ID,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
      customerName: 'Acme',
    })
    mockAddExpenseToInvoiceLine.mockResolvedValue({
      invoiceLineId: '00000000-0000-4000-8000-0000000000dd',
      invoiceId: INVOICE_ID,
      expenseId: EXPENSE_ID,
    })
    mockGetInvoiceWithLines.mockResolvedValue({
      id: INVOICE_ID,
      tenantId: TENANT_ID,
      customerId: CUSTOMER_ID,
      projectId: null,
      invoiceNumber: null,
      proformaNumber: 'INV-0042',
      status: 'DRAFT',
      currency: 'ILS',
      issueDate: null,
      taxIssueDate: null,
      dueDate: null,
      vatRate: '0.18',
      subtotal: '347.50',
      vatAmount: '62.55',
      total: '410.05',
      amountPaid: '0.00',
      notes: null,
      source: 'manual',
      sentAt: null,
      approvedAt: null,
      approvedBy: null,
      approvalNote: null,
      rejectionReason: null,
      rejectionNotifyCustomer: false,
      taxIssuedAt: null,
      paidAt: null,
      externalId: null,
      externalProvider: null,
      voidReason: null,
      voidedAt: null,
      voidedBy: null,
      parentInvoiceId: null,
      htmlSnapshotUrl: null,
      createdBy: USER_ID,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
      customerName: 'Acme',
      lines: [{
        id: '00000000-0000-4000-8000-0000000000dd',
        invoiceId: INVOICE_ID,
        tenantId: TENANT_ID,
        description: 'Office supplies',
        quantity: '1',
        unitPrice: '347.50',
        discountPct: '0',
        lineTotal: '347.50',
        taxable: true,
        position: 1,
        expenseId: EXPENSE_ID,
        createdAt: '2026-06-01T00:00:00.000Z',
      }],
    })
  })

  it('passes billable=true through the main expenses list route', async () => {
    const res = await expenseApp().request('/api/expenses?billable=true&limit=10')

    expect(res.status).toBe(200)
    expect(mockListExpenses).toHaveBeenCalledWith(
      mockDb,
      TENANT_ID,
      expect.objectContaining({ billable: true, limit: 10 }),
    )
  })

  it('creates an invoice line from an expense on the invoice route', async () => {
    const res = await invoiceApp().request(`/api/invoices/${INVOICE_ID}/lines/from-expense`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ expense_id: EXPENSE_ID, description: 'Office supplies', amount: 347.5 }),
    })

    expect(res.status).toBe(201)
    expect(mockAddExpenseToInvoiceLine).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        tenantId: TENANT_ID,
        invoiceId: INVOICE_ID,
        expenseId: EXPENSE_ID,
        description: 'Office supplies',
        amount: '347.50',
      }),
    )
    const body = await res.json()
    expect(body.line.expenseId).toBe(EXPENSE_ID)
    expect(body.invoice.id).toBe(INVOICE_ID)
  })

  it('falls back to the expense vendor and date when description is omitted', async () => {
    const res = await invoiceApp().request(`/api/invoices/${INVOICE_ID}/lines/from-expense`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ expense_id: EXPENSE_ID, amount: 347.5 }),
    })

    expect(res.status).toBe(201)
    expect(mockAddExpenseToInvoiceLine).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        description: 'Supersol Deal - 2026-05-28',
      }),
    )
  })

  it('returns 409 when the expense is already billed', async () => {
    mockGetExpenseForInvoicing.mockResolvedValueOnce({
      id: EXPENSE_ID,
      tenantId: TENANT_ID,
      projectId: '00000000-0000-4000-8000-000000000011',
      customerId: CUSTOMER_ID,
      description: 'Office supplies',
      amount: '347.50',
      expenseDate: '2026-05-28',
      vendorName: 'Supersol Deal',
      billedAt: '2026-05-30T00:00:00.000Z',
      invoiceId: INVOICE_ID,
      status: 'COMPLETED',
    })

    const res = await invoiceApp().request(`/api/invoices/${INVOICE_ID}/lines/from-expense`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ expense_id: EXPENSE_ID }),
    })

    expect(res.status).toBe(409)
    expect(mockAddExpenseToInvoiceLine).not.toHaveBeenCalled()
  })

  it('returns 403 without expenses:read', async () => {
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['invoices:write'],
      tier: 'business',
    }

    const res = await invoiceApp().request(`/api/invoices/${INVOICE_ID}/lines/from-expense`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ expense_id: EXPENSE_ID }),
    })

    expect(res.status).toBe(403)
  })
})
