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

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

const mockGetExpenseSettings = vi.fn()
const mockUpdateExpenseSettings = vi.fn()
const mockRequirePermission = vi.fn()

let mockSession: Record<string, unknown>
let mockDb: {
  transaction: ReturnType<typeof vi.fn>
}
let txInsertValues: ReturnType<typeof vi.fn>

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>) => next(),
}))

vi.mock('../src/middleware/guards', () => ({
  requirePermission: (...args: unknown[]) => mockRequirePermission(...args),
}))

vi.mock('@zync/expenses', () => ({
  getExpenseSettings: (...args: unknown[]) => mockGetExpenseSettings(...args),
  updateExpenseSettings: (...args: unknown[]) => mockUpdateExpenseSettings(...args),
}))

async function appWithRoute() {
  const { expenseSettingsRoute } = await import('../src/routes/expenses/settings')
  const app = new Hono<AppEnv>()
  app.route('/api/settings/expenses', expenseSettingsRoute)
  return app
}

function buildSettings() {
  return {
    filing_cadence: 'monthly' as const,
    tax_basis: 'cash' as const,
    business_category: null,
    per_diem_rates: {
      domestic_full_day: 100,
      domestic_half_day: 50,
      international_full_day: 250,
      international_half_day: 125,
    },
    expense_default_category: 'exceptional',
    expense_auto_approve_threshold_ils: 500,
    expense_receipt_reminder_enabled: true,
    expense_receipt_reminder_days: 7,
    expense_approval_threshold_ils: 1000,
    expense_approver_role: USER_ID,
  }
}

function buildPatch() {
  const {
    filing_cadence,
    per_diem_rates,
    expense_default_category,
    expense_auto_approve_threshold_ils,
    expense_receipt_reminder_enabled,
    expense_receipt_reminder_days,
    expense_approval_threshold_ils,
    expense_approver_role,
  } = buildSettings()

  return {
    filing_cadence,
    per_diem_rates,
    expense_default_category,
    expense_auto_approve_threshold_ils,
    expense_receipt_reminder_enabled,
    expense_receipt_reminder_days,
    expense_approval_threshold_ils,
    expense_approver_role,
  }
}

describe('expense settings route', () => {
  beforeEach(() => {
    vi.resetModules()
    vi.clearAllMocks()
    txInsertValues = vi.fn().mockResolvedValue(undefined)
    mockDb = {
      transaction: vi.fn(async (cb: (tx: { insert: (table: unknown) => { values: typeof txInsertValues } }) => Promise<void>) =>
        cb({
          insert: () => ({ values: txInsertValues }),
        }),
      ),
    }
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      role: 'OWNER',
      tier: TenantTier.BUSINESS,
      permissions: ['expenses:read', 'settings:write'],
    }
    mockGetExpenseSettings.mockResolvedValue(buildSettings())
    mockUpdateExpenseSettings.mockResolvedValue(buildSettings())
    mockRequirePermission.mockImplementation(
      (permission: string) =>
        async (
          c: { get: (key: string) => { permissions?: string[] } | undefined; json: (body: unknown, status: number) => Response },
          next: () => Promise<void>,
        ) => {
          const session = c.get('session')
          if (!session?.permissions?.includes(permission)) {
            return c.json({ error: 'Forbidden' }, 403)
          }
          await next()
        },
    )
  })

  it('registers GET and PATCH with the spec permissions', async () => {
    await import('../src/routes/expenses/settings')

    expect(mockRequirePermission).toHaveBeenCalledWith('expenses:read')
    expect(mockRequirePermission).toHaveBeenCalledWith('settings:write')
  })

  it('writes the settings update and audit row in one transaction', async () => {
    const app = await appWithRoute()
    const res = await app.request('/api/settings/expenses', {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'CF-Connecting-IP': '203.0.113.8',
        'X-Request-Id': 'req-expense-settings',
      },
      body: JSON.stringify(buildPatch()),
    })

    expect(res.status).toBe(200)
    expect(mockDb.transaction).toHaveBeenCalledTimes(1)
    expect(mockUpdateExpenseSettings).toHaveBeenCalledWith(
      expect.objectContaining({ insert: expect.any(Function) }),
      TENANT_ID,
      expect.objectContaining({
        expense_approval_threshold_ils: 1000,
        expense_approver_role: USER_ID,
      }),
    )
    expect(txInsertValues).toHaveBeenCalledWith(
      expect.objectContaining({
        tenantId: TENANT_ID,
        actorId: USER_ID,
        actorType: 'user',
        entityType: 'expense_settings',
        entityId: TENANT_ID,
        action: 'settings.expenses.update',
        requestId: 'req-expense-settings',
        ip: '203.0.113.8',
      }),
    )
  })

  it('resets approval fields for non-Business tiers before persisting', async () => {
    mockSession.tier = TenantTier.FREELANCER
    const app = await appWithRoute()
    const res = await app.request('/api/settings/expenses', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(buildPatch()),
    })

    expect(res.status).toBe(200)
    expect(mockUpdateExpenseSettings).toHaveBeenCalledWith(
      expect.anything(),
      TENANT_ID,
      expect.objectContaining({
        expense_approval_threshold_ils: 0,
        expense_approver_role: 'any_admin',
      }),
    )
  })

  it('rejects PATCH when the session has no tenant id', async () => {
    mockSession.tid = null
    const app = await appWithRoute()
    const res = await app.request('/api/settings/expenses', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(buildPatch()),
    })

    expect(res.status).toBe(401)
    expect(mockDb.transaction).not.toHaveBeenCalled()
    expect(mockUpdateExpenseSettings).not.toHaveBeenCalled()
    expect(txInsertValues).not.toHaveBeenCalled()
  })
})
