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

const mockListProjects = vi.fn()
const mockGetProjectHours = vi.fn()
const mockListRetainerMonths = vi.fn()
const mockGetProjectTasksWithActualHours = vi.fn()
const mockGetProjectEstimateSummary = vi.fn()
const mockGetCompletionSummary = vi.fn()
const mockCompleteProject = vi.fn()
const mockArchiveProjectLifecycle = vi.fn()
const mockReopenProject = vi.fn()

vi.mock('../src/middleware/guards', () => ({
  requirePermission: () => async (_c: unknown, next: () => Promise<void>) => next(),
}))

vi.mock('@zync/db/queries', () => ({
  listProjects: (...args: unknown[]) => mockListProjects(...args),
  getProjectHours: (...args: unknown[]) => mockGetProjectHours(...args),
  listRetainerMonths: (...args: unknown[]) => mockListRetainerMonths(...args),
  getProjectTasksWithActualHours: (...args: unknown[]) => mockGetProjectTasksWithActualHours(...args),
  getProjectEstimateSummary: (...args: unknown[]) => mockGetProjectEstimateSummary(...args),
  getCompletionSummary: (...args: unknown[]) => mockGetCompletionSummary(...args),
  completeProject: (...args: unknown[]) => mockCompleteProject(...args),
  archiveProjectLifecycle: (...args: unknown[]) => mockArchiveProjectLifecycle(...args),
  reopenProject: (...args: unknown[]) => mockReopenProject(...args),
  getProjectWithStats: vi.fn(),
  createProject: vi.fn(),
  updateProject: vi.fn(),
  archiveProject: vi.fn(),
  assertActiveTenantAssignee: vi.fn(),
  assertTenantOwnsCustomer: vi.fn(),
  invalidTenantReferenceBody: vi.fn(),
  getTenantFieldRules: vi.fn().mockResolvedValue([]),
}))

import { projectCrudRoute } from '../src/routes/projects/crud'
import { projectReportsRoute } from '../src/routes/projects/reports'
import { projectLifecycleRoutes } from '../src/routes/projects/lifecycle'

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', projectCrudRoute)
  app.route('/api/projects', projectReportsRoute)
  app.route('/api/projects', projectLifecycleRoutes)
  return app
}

describe('projects routes visibility', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockListProjects.mockResolvedValue({ items: [], nextCursor: null, total: 0 })
    mockGetProjectHours.mockResolvedValue({ this_month: 0, all_time: 0 })
    mockListRetainerMonths.mockResolvedValue([])
    mockGetProjectTasksWithActualHours.mockResolvedValue([])
    mockGetProjectEstimateSummary.mockResolvedValue({
      taskCount: 3,
      totalEstimated: 82,
      totalLogged: 54,
      remaining: 28,
      budgetConsumedPct: 66,
      paceStatus: 'on_track',
    })
    mockGetCompletionSummary.mockResolvedValue({
      open_tasks: 4,
      unbilled_hours: 2.5,
      unbilled_amount: 250,
      outstanding_invoice_amount: 4200,
    })
    mockCompleteProject.mockResolvedValue({
      ok: true,
      project: { id: 'project-1', status: 'completed' },
    })
    mockArchiveProjectLifecycle.mockResolvedValue({
      ok: true,
      project: { id: 'project-1', status: 'archived' },
    })
    mockReopenProject.mockResolvedValue({
      ok: true,
      project: { id: 'project-1', status: 'completed' },
    })
  })

  it('treats MEMBER role as member-scoped even though it has projects:read', async () => {
    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'MEMBER',
      permissions: ['projects:read'],
    }).request('/api/projects', undefined, {} as AppEnv['Bindings'])

    expect(res.status).toBe(200)
    expect(mockListProjects).toHaveBeenCalledWith(
      {},
      'tenant-1',
      expect.any(Object),
      { fullVisibility: false, userId: 'user-1' },
    )
  })

  it('passes the same scoped access guard to hours and retainer routes', async () => {
    const app = appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'MEMBER',
      permissions: ['projects:read'],
    })

    const hoursRes = await app.request('/api/projects/project-1/hours', undefined, {} as AppEnv['Bindings'])
    const monthsRes = await app.request('/api/projects/project-1/retainer-months', undefined, {} as AppEnv['Bindings'])

    expect(hoursRes.status).toBe(200)
    expect(monthsRes.status).toBe(200)
    expect(mockGetProjectHours).toHaveBeenCalledWith(
      {},
      'tenant-1',
      'project-1',
      { fullVisibility: false, userId: 'user-1' },
    )
    expect(mockListRetainerMonths).toHaveBeenCalledWith(
      {},
      'tenant-1',
      'project-1',
      { fullVisibility: false, userId: 'user-1' },
    )
  })

  it('returns project tasks with actual_hours from the project-scoped route', async () => {
    const task = {
      id: 'task-1',
      tenant_id: 'tenant-1',
      project_id: 'project-1',
      status_id: '11111111-1111-4111-8111-111111111111',
      title: 'Implement auth flow',
      description: null,
      priority: 'medium',
      assignee_id: null,
      reporter_id: 'user-1',
      due_date: null,
      estimated_hours: 6,
      actual_hours: 4.5,
      source: 'manual',
      external_id: null,
      position: 1,
      labels: [],
      created_at: '2026-07-01T00:00:00.000Z',
      updated_at: '2026-07-01T00:00:00.000Z',
    }
    mockGetProjectTasksWithActualHours.mockResolvedValue([task])

    const app = appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'MEMBER',
      permissions: ['projects:read'],
    })

    const res = await app.request('/api/projects/project-1/tasks', undefined, {} as AppEnv['Bindings'])
    const body = await res.json()

    expect(res.status).toBe(200)
    expect(body).toEqual({ data: [task] })
    expect(mockGetProjectTasksWithActualHours).toHaveBeenCalledWith(
      {},
      'tenant-1',
      'project-1',
      { fullVisibility: false, userId: 'user-1' },
    )
  })

  it('returns the estimate summary rollup from the project-scoped route', async () => {
    const app = appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'MEMBER',
      permissions: ['projects:read'],
    })

    const res = await app.request('/api/projects/project-1/summary', undefined, {} as AppEnv['Bindings'])
    const body = await res.json()

    expect(res.status).toBe(200)
    expect(body).toEqual({
      taskCount: 3,
      totalEstimated: 82,
      totalLogged: 54,
      remaining: 28,
      budgetConsumedPct: 66,
      paceStatus: 'on_track',
    })
    expect(mockGetProjectEstimateSummary).toHaveBeenCalledWith(
      {},
      'tenant-1',
      'project-1',
      { fullVisibility: false, userId: 'user-1' },
    )
  })

  it('defaults list requests to active + completed when status is omitted', async () => {
    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'OWNER',
      permissions: ['projects:read'],
    }).request('/api/projects', undefined, {} as AppEnv['Bindings'])

    expect(res.status).toBe(200)
    expect(mockListProjects).toHaveBeenCalledWith(
      {},
      'tenant-1',
      expect.objectContaining({ status: ['active', 'completed'] }),
      { fullVisibility: true, userId: 'user-1' },
    )
  })

  it('exposes the completion summary endpoint', async () => {
    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'MEMBER',
      permissions: ['projects:read'],
    }).request('/api/projects/project-1/completion-summary', undefined, {} as AppEnv['Bindings'])

    expect(res.status).toBe(200)
    await expect(res.json()).resolves.toEqual({
      open_tasks: 4,
      unbilled_hours: 2.5,
      unbilled_amount: 250,
      outstanding_invoice_amount: 4200,
    })
    expect(mockGetCompletionSummary).toHaveBeenCalledWith({}, 'tenant-1', 'project-1')
  })

  it('accepts close_open_tasks on complete', async () => {
    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'OWNER',
      permissions: ['projects:write'],
    }).request(
      '/api/projects/project-1/complete',
      {
        method: 'POST',
        body: JSON.stringify({ close_open_tasks: true }),
        headers: { 'Content-Type': 'application/json' },
      },
      {} as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    expect(mockCompleteProject).toHaveBeenCalledWith({}, 'tenant-1', 'project-1', 'user-1', {
      closeOpenTasks: true,
    })
  })

  it('maps unarchive to the lifecycle reopen helper', async () => {
    const res = await appFor({
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'OWNER',
      permissions: ['projects:write'],
    }).request('/api/projects/project-1/unarchive', { method: 'POST' }, {} as AppEnv['Bindings'])

    expect(res.status).toBe(200)
    expect(mockReopenProject).toHaveBeenCalledWith({}, 'tenant-1', 'project-1', 'user-1')
  })
})
