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-000000000002'
const EXPENSE_ID = '00000000-0000-4000-8000-000000000003'

const mockApproveExpense = vi.fn()
const mockRejectExpense = vi.fn()
const mockIsExpenseApprover = vi.fn()
const mockApproveOcrExpense = vi.fn()
const mockGetExpenseById = vi.fn()
const mockPushOverWebSocket = vi.fn().mockResolvedValue(undefined)
const mockPostExpenseReceipts = vi.fn().mockResolvedValue(0)

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

vi.mock('../src/middleware/auth', () => ({
  authMiddleware: async (
    c: { set: (key: string, value: unknown) => void },
    next: () => Promise<void>,
  ) => {
    c.set('db', {})
    c.set('session', mockSession)
    await next()
  },
}))

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/require-module-enabled', () => ({
  requireModuleEnabled: () => async (_c: unknown, next: () => Promise<void>) => next(),
}))

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

vi.mock('@zync/db/queries', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@zync/db/queries')>()
  return {
    ...actual,
    approveExpense: (...args: unknown[]) => mockApproveExpense(...args),
    rejectExpense: (...args: unknown[]) => mockRejectExpense(...args),
    isExpenseApprover: (...args: unknown[]) => mockIsExpenseApprover(...args),
    approveOcrExpense: (...args: unknown[]) => mockApproveOcrExpense(...args),
  }
})

vi.mock('@zync/expenses', () => ({
  listExpenses: vi.fn(),
  getExpense: vi.fn(),
  createExpense: vi.fn(),
  updateExpense: vi.fn(),
  softDeleteExpense: vi.fn(),
  getExpenseById: (...args: unknown[]) => mockGetExpenseById(...args),
  setExpenseStatus: vi.fn(),
  serializeExpense: vi.fn(),
  createPerDiemExpense: vi.fn(),
  evaluateDeductibility: vi.fn(),
  getExpenseSettings: vi.fn(),
  emitExpenseWebhook: vi.fn(),
  listExpensesByIds: vi.fn(),
  listNeedsReviewExpenseIds: vi.fn(),
  buildExpenseReportXlsx: vi.fn(),
}))

vi.mock('@zync/notifications', () => ({
  pushOverWebSocket: (...args: unknown[]) => mockPushOverWebSocket(...args),
}))

vi.mock('../src/integrations/platform/inventory', () => ({
  postExpenseReceipts: (...args: unknown[]) => mockPostExpenseReceipts(...args),
}))

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

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

describe('expense approval routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['expenses:approve', 'expenses:write'],
    }
    mockIsExpenseApprover.mockResolvedValue(true)
  })

  it('approves pending approval expenses through POST /api/expenses/:id/approve', async () => {
    mockGetExpenseById.mockResolvedValue({
      id: EXPENSE_ID,
      status: 'COMPLETED',
      approvalStatus: 'pending',
      tenantId: TENANT_ID,
      amount: '120.00',
      vatAmount: '20.00',
      processedAt: new Date('2026-07-05T10:00:00.000Z'),
      updatedAt: new Date('2026-07-05T10:00:00.000Z'),
      createdAt: new Date('2026-07-05T09:00:00.000Z'),
      sourceMetadata: {
        stockLines: [{ stockItemId: 'item-1', locationId: 'loc-1', qty: 1, netAmount: 100 }],
      },
    })
    mockApproveExpense.mockResolvedValue({ id: EXPENSE_ID, approvalStatus: 'approved' })

    const res = await appWithRoutes().request(`/api/expenses/${EXPENSE_ID}/approve`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ note: 'Looks good' }),
    })

    expect(res.status).toBe(200)
    expect(mockApproveExpense).toHaveBeenCalledWith({
      tenantId: TENANT_ID,
      expenseId: EXPENSE_ID,
      approverUserId: USER_ID,
      note: 'Looks good',
      db: {},
    })
    expect(mockPostExpenseReceipts).toHaveBeenCalledWith({}, expect.objectContaining({
      id: EXPENSE_ID,
      tenantId: TENANT_ID,
    }))
    expect(mockApproveOcrExpense).not.toHaveBeenCalled()
  })

  it('keeps OCR approve behavior on POST /api/expenses/:id/approve for NEEDS_REVIEW expenses', async () => {
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['expenses:write'],
    }
    mockGetExpenseById.mockResolvedValue({
      id: EXPENSE_ID,
      status: 'NEEDS_REVIEW',
      approvalStatus: 'not_required',
    })
    mockApproveOcrExpense.mockResolvedValue({
      id: EXPENSE_ID,
      tenantId: TENANT_ID,
      status: 'COMPLETED',
      amount: '59.00',
      vatAmount: '9.00',
      sourceMetadata: {
        stockLines: [{ stockItemId: 'item-2', locationId: 'loc-2', qty: 2, netAmount: 50 }],
      },
    })

    const res = await appWithRoutes().request(`/api/expenses/${EXPENSE_ID}/approve`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ corrections: { vendorName: 'Office Depot' } }),
    })

    expect(res.status).toBe(200)
    expect(mockApproveOcrExpense).toHaveBeenCalledWith({}, {
      tenantId: TENANT_ID,
      expenseId: EXPENSE_ID,
      userId: USER_ID,
      corrections: { vendorName: 'Office Depot' },
    })
    expect(mockPostExpenseReceipts).toHaveBeenCalledWith({}, expect.objectContaining({
      id: EXPENSE_ID,
      tenantId: TENANT_ID,
      status: 'COMPLETED',
    }))
    expect(mockPostExpenseReceipts).toHaveBeenCalledTimes(1)
    expect(mockApproveExpense).not.toHaveBeenCalled()
  })

  it('rejects a pending approval through POST /api/expenses/:id/reject', async () => {
    mockRejectExpense.mockResolvedValue({ id: EXPENSE_ID, approvalStatus: 'rejected' })

    const res = await appWithRoutes().request(`/api/expenses/${EXPENSE_ID}/reject`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ reason: 'Missing receipt details' }),
    })

    expect(res.status).toBe(200)
    expect(mockRejectExpense).toHaveBeenCalledWith({
      tenantId: TENANT_ID,
      expenseId: EXPENSE_ID,
      approverUserId: USER_ID,
      reason: 'Missing receipt details',
      db: {},
    })
  })
})
