/**
 * Invoice approval API routes — invoice-approval-workflow (iaw-012).
 */
import { describe, it, expect, vi, beforeEach } 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 mockListPendingApprovals = vi.fn()
const mockCountPendingApprovals = vi.fn()
const mockBulkApproveInvoices = vi.fn()
const mockApproveInvoiceEnhanced = vi.fn()
const mockRejectInvoiceEnhanced = vi.fn()
const mockGetInvoiceWithLines = vi.fn()
const mockResolveCustomerPrimaryEmail = vi.fn()
const mockLoadInvoiceRenderIdentity = vi.fn()
const mockSendEmail = vi.fn()
const mockInsertEmailEvent = vi.fn()

let mockSession: Record<string, unknown> = {
  type: 'user',
  sub: USER_ID,
  tid: TENANT_ID,
  email: 'staff@example.com',
  name: 'Staff User',
  permissions: ['invoices:write'],
  tier: 'business',
}

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', {})
    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,
    listPendingApprovals: (...args: unknown[]) => mockListPendingApprovals(...args),
    countPendingApprovals: (...args: unknown[]) => mockCountPendingApprovals(...args),
    bulkApproveInvoices: (...args: unknown[]) => mockBulkApproveInvoices(...args),
    approveInvoiceEnhanced: (...args: unknown[]) => mockApproveInvoiceEnhanced(...args),
    rejectInvoiceEnhanced: (...args: unknown[]) => mockRejectInvoiceEnhanced(...args),
    getInvoiceWithLines: (...args: unknown[]) => mockGetInvoiceWithLines(...args),
    insertEmailEvent: (...args: unknown[]) => mockInsertEmailEvent(...args),
  }
})

vi.mock('../src/services/payment-link', () => ({
  resolveCustomerPrimaryEmail: (...args: unknown[]) => mockResolveCustomerPrimaryEmail(...args),
}))

vi.mock('../src/lib/invoice-snapshot', () => ({
  loadInvoiceRenderIdentity: (...args: unknown[]) => mockLoadInvoiceRenderIdentity(...args),
}))

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

import { invoiceApprovalRoutes } from '../src/routes/invoices/approvals'
import { invoiceRoutes } from '../src/routes/invoices/index'

const fullInvoice = {
  id: INVOICE_ID,
  tenantId: TENANT_ID,
  customerId: '00000000-0000-4000-8000-000000000010',
  projectId: null,
  invoiceNumber: null,
  proformaNumber: 'INV-PRO-001',
  status: 'APPROVED',
  currency: 'ILS',
  issueDate: '2026-06-01',
  taxIssueDate: null,
  dueDate: null,
  vatRate: '18',
  subtotal: '1000.00',
  vatAmount: '180.00',
  total: '1180.00',
  amountPaid: '0',
  notes: null,
  source: 'manual',
  sentAt: '2026-06-01T08:00:00.000Z',
  approvedAt: '2026-06-01T09:00:00.000Z',
  approvedBy: USER_ID,
  approvalNote: '[Staff approval]',
  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-01T07:00:00.000Z',
  updatedAt: '2026-06-01T09:00:00.000Z',
  customerName: 'Acme Corp',
  lines: [],
}

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

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

describe('GET /api/invoices/approvals', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['invoices:write'],
      tier: 'business',
    }
    mockListPendingApprovals.mockResolvedValue({
      items: [{ id: INVOICE_ID, proformaNumber: 'INV-PRO-001', customerName: 'Acme', total: '100', currency: 'ILS', sentAt: null }],
      total: 1,
      page: 1,
      perPage: 50,
    })
  })

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

    const res = await appWithApprovalRoutes().request('/api/invoices/approvals', {
      method: 'GET',
      headers: { Origin: 'https://app.zync.is' },
    })

    expect(res.status).toBe(403)
    expect(mockListPendingApprovals).not.toHaveBeenCalled()
  })

  it('returns pending approvals for authorized callers', async () => {
    const res = await appWithApprovalRoutes().request('/api/invoices/approvals', {
      method: 'GET',
      headers: { Origin: 'https://app.zync.is' },
    })

    expect(res.status).toBe(200)
    expect(mockListPendingApprovals).toHaveBeenCalledOnce()
  })
})

describe('GET /api/invoices/approvals/count', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockCountPendingApprovals.mockResolvedValue(3)
  })

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

    const res = await appWithApprovalRoutes().request('/api/invoices/approvals/count', {
      method: 'GET',
      headers: { Origin: 'https://app.zync.is' },
    })

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

describe('POST /api/invoices/bulk-approve', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['invoices:write'],
      tier: 'business',
    }
    mockBulkApproveInvoices.mockResolvedValue({ approved: 2, skipped: 0, errors: [] })
  })

  it('returns the bulk summary shape', async () => {
    const res = await appWithInvoiceRoutes().request('/api/invoices/bulk-approve', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ ids: [INVOICE_ID] }),
    })

    expect(res.status).toBe(200)
    const body = (await res.json()) as { approved: number; skipped: number; errors: unknown[] }
    expect(body).toEqual({ approved: 2, skipped: 0, errors: [] })
  })

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

    const res = await appWithInvoiceRoutes().request('/api/invoices/bulk-approve', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ ids: [INVOICE_ID] }),
    })

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

describe('POST /api/invoices/:id/reject', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['invoices:write'],
      tier: 'business',
    }
    mockRejectInvoiceEnhanced.mockResolvedValue({ id: INVOICE_ID, status: 'REJECTED' })
    mockGetInvoiceWithLines.mockResolvedValue({
      ...fullInvoice,
      status: 'REJECTED',
      rejectionReason: 'Needs updated line items',
      rejectionNotifyCustomer: true,
    })
    mockResolveCustomerPrimaryEmail.mockResolvedValue('contact@acme.com')
    mockLoadInvoiceRenderIdentity.mockResolvedValue({
      tenantName: 'Acme Studio',
      customerName: 'Acme Corp',
      locale: 'he-IL',
    })
    mockSendEmail.mockRejectedValue(new Error('SMTP unavailable'))
    mockInsertEmailEvent.mockResolvedValue(undefined)
  })

  it('returns 400 when the rejection reason is too short', async () => {
    const res = await appWithInvoiceRoutes().request(`/api/invoices/${INVOICE_ID}/reject`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ reason: 'nope' }),
    })

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

  it('returns 404 for cross-tenant invoices', async () => {
    mockRejectInvoiceEnhanced.mockRejectedValue(new Error('Invoice not found'))
    const res = await appWithInvoiceRoutes().request(`/api/invoices/${INVOICE_ID}/reject`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ reason: 'Needs updated line items', notifyCustomer: false }),
    })

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

  it('returns the full invoice object when email send fails', async () => {
    const res = await appWithInvoiceRoutes().request(`/api/invoices/${INVOICE_ID}/reject`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ reason: 'Needs updated line items', notifyCustomer: true }),
    })

    expect(res.status).toBe(200)
    const body = (await res.json()) as { id: string; status: string; lines: unknown[] }
    expect(body.id).toBe(INVOICE_ID)
    expect(body.status).toBe('REJECTED')
    expect(mockSendEmail).toHaveBeenCalledOnce()
  })
})

describe('POST /api/invoices/:id/approve', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: USER_ID,
      tid: TENANT_ID,
      permissions: ['invoices:write'],
      tier: 'business',
    }
    mockApproveInvoiceEnhanced.mockResolvedValue({
      id: INVOICE_ID,
      status: 'APPROVED',
      approvedAt: '2026-06-01T09:00:00.000Z',
    })
    mockGetInvoiceWithLines.mockResolvedValue(fullInvoice)
  })

  it('returns the full serialized invoice object', async () => {
    const res = await appWithInvoiceRoutes().request(`/api/invoices/${INVOICE_ID}/approve`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({ note: 'Confirmed by phone' }),
    })

    expect(res.status).toBe(200)
    const body = (await res.json()) as { id: string; approvedBy: string | null; lines: unknown[] }
    expect(body.id).toBe(INVOICE_ID)
    expect(body.approvedBy).toBe(USER_ID)
    expect(body.lines).toEqual([])
  })

  it('returns 404 for cross-tenant invoices', async () => {
    mockApproveInvoiceEnhanced.mockRejectedValue(new Error('Invoice not found'))
    const res = await appWithInvoiceRoutes().request(`/api/invoices/${INVOICE_ID}/approve`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({}),
    })

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