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

const mocks = vi.hoisted(() => ({
  requirePermission: vi.fn(() => async (_c: unknown, next: () => Promise<void>) => {
    await next()
  }),
  getProjectById: vi.fn(),
  listMilestones: vi.fn(),
  getMilestoneSummary: vi.fn(),
  createMilestone: vi.fn(),
  getMilestone: vi.fn(),
  updateMilestone: vi.fn(),
  completeMilestone: vi.fn(),
  reopenMilestone: vi.fn(),
  deleteMilestone: vi.fn(),
  setMilestoneInvoice: vi.fn(),
  assertTenantOwnsInvoice: vi.fn(),
  getInvoice: vi.fn(),
  MilestoneInvoicedError: class MilestoneInvoicedError extends Error {
    readonly code = 'milestone_invoiced' as const
  },
}))

vi.mock('../src/middleware/guards', () => ({
  requirePermission: mocks.requirePermission,
}))

vi.mock('@zync/db/queries', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@zync/db/queries')>()
  return {
    ...actual,
    getProjectById: mocks.getProjectById,
    listMilestones: mocks.listMilestones,
    getMilestoneSummary: mocks.getMilestoneSummary,
    createMilestone: mocks.createMilestone,
    getMilestone: mocks.getMilestone,
    updateMilestone: mocks.updateMilestone,
    completeMilestone: mocks.completeMilestone,
    reopenMilestone: mocks.reopenMilestone,
    deleteMilestone: mocks.deleteMilestone,
    setMilestoneInvoice: mocks.setMilestoneInvoice,
    assertTenantOwnsInvoice: mocks.assertTenantOwnsInvoice,
    getInvoice: mocks.getInvoice,
    MilestoneInvoicedError: mocks.MilestoneInvoicedError,
  }
})

import { projectMilestoneRoutes } from '../src/routes/projects/milestones'

function appFor(session: Record<string, unknown>) {
  const app = new Hono<AppEnv>()
  app.use('*', async (c, next) => {
    c.set('session', session)
    c.set('db', {})
    await next()
  })
  app.route('/api/projects', projectMilestoneRoutes)
  return app
}

const project = {
  id: 'project-1',
  billing_config: { total_amount: 10000, deposit_pct: 30 },
  customer_id: 'customer-1',
}

const milestone = {
  id: 'milestone-1',
  projectId: 'project-1',
  name: 'Design approval',
  description: null,
  amount: '2000.00',
  dueDate: '2026-06-15',
  completedAt: null,
  status: 'pending',
  invoiceId: null,
  invoiceStatus: null,
  position: 1,
  createdAt: '2026-06-01T00:00:00.000Z',
  createdBy: 'user-1',
}

describe('project milestone routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mocks.getProjectById.mockResolvedValue(project)
    mocks.listMilestones.mockResolvedValue([milestone])
    mocks.getMilestoneSummary.mockResolvedValue({
      totalDefined: '2000.00',
      invoiced: '0.00',
      remaining: '2000.00',
      completeCount: 0,
      totalCount: 1,
      matchesBillingConfig: false,
    })
    mocks.createMilestone.mockResolvedValue(milestone)
    mocks.getMilestone.mockResolvedValue(milestone)
    mocks.updateMilestone.mockResolvedValue(milestone)
    mocks.completeMilestone.mockResolvedValue({ ...milestone, completedAt: '2026-06-02T00:00:00.000Z', status: 'complete' })
    mocks.reopenMilestone.mockResolvedValue(milestone)
    mocks.deleteMilestone.mockResolvedValue(undefined)
    mocks.setMilestoneInvoice.mockResolvedValue({ ...milestone, invoiceId: 'invoice-1', invoiceStatus: 'DRAFT' })
    mocks.assertTenantOwnsInvoice.mockResolvedValue(true)
    mocks.getInvoice.mockResolvedValue({ id: 'invoice-1', projectId: 'project-1' })
  })

  it('returns milestones with summary', async () => {
    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['projects:read'],
    }).request('/api/projects/project-1/milestones')

    expect(res.status).toBe(200)
    expect(await res.json()).toEqual({
      milestones: [milestone],
      summary: {
        totalDefined: '2000.00',
        invoiced: '0.00',
        remaining: '2000.00',
        completeCount: 0,
        totalCount: 1,
        matchesBillingConfig: false,
      },
    })
  })

  it('rejects user sessions without an active tenant', async () => {
    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: null,
      permissions: ['projects:read'],
    }).request('/api/projects/project-1/milestones')

    expect(res.status).toBe(401)
    expect(await res.json()).toEqual({ error: 'Unauthorized' })
    expect(mocks.getProjectById).not.toHaveBeenCalled()
  })

  it('uses dedicated complete and reopen endpoints', async () => {
    const app = appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['projects:write'],
    })

    const completeRes = await app.request('/api/projects/project-1/milestones/milestone-1/complete', {
      method: 'POST',
    })
    const reopenRes = await app.request('/api/projects/project-1/milestones/milestone-1/reopen', {
      method: 'POST',
    })

    expect(completeRes.status).toBe(200)
    expect(reopenRes.status).toBe(200)
    expect(mocks.completeMilestone).toHaveBeenCalledWith({}, 'tenant-1', 'milestone-1')
    expect(mocks.reopenMilestone).toHaveBeenCalledWith({}, 'tenant-1', 'milestone-1')
  })

  it('maps invoice-linked reopen failures to 422 milestone_invoiced', async () => {
    mocks.reopenMilestone.mockRejectedValue(
      new mocks.MilestoneInvoicedError(
        'Void or delete the linked invoice before reopening this milestone.',
      ),
    )

    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['projects:write'],
    }).request('/api/projects/project-1/milestones/milestone-1/reopen', {
      method: 'POST',
    })

    expect(res.status).toBe(422)
    expect(await res.json()).toEqual({
      code: 'milestone_invoiced',
      message: 'Void or delete the linked invoice before reopening this milestone.',
    })
  })

  it('rejects invoice linking when the invoice does not belong to the same tenant/project', async () => {
    mocks.assertTenantOwnsInvoice.mockResolvedValue(false)

    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['projects:write'],
    }).request('/api/projects/project-1/milestones/milestone-1', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ invoice_id: '11111111-1111-4111-8111-111111111111' }),
    })

    expect(res.status).toBe(404)
    expect(mocks.setMilestoneInvoice).not.toHaveBeenCalled()
  })

  it('returns 404 when the milestone does not belong to the project in the path', async () => {
    mocks.getMilestone.mockResolvedValue({ ...milestone, projectId: 'project-2' })

    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['projects:write'],
    }).request('/api/projects/project-1/milestones/milestone-1/complete', {
      method: 'POST',
    })

    expect(res.status).toBe(404)
    expect(mocks.completeMilestone).not.toHaveBeenCalled()
  })

  it('maps delete invoice guard failures to 422', async () => {
    mocks.deleteMilestone.mockRejectedValue(
      new mocks.MilestoneInvoicedError('Delete blocked for invoiced milestone.'),
    )

    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['projects:write'],
    }).request('/api/projects/project-1/milestones/milestone-1', {
      method: 'DELETE',
    })

    expect(res.status).toBe(422)
    expect(await res.json()).toEqual({
      code: 'milestone_invoiced',
      message: 'Delete blocked for invoiced milestone.',
    })
  })
})
