import { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Hono } from 'hono'
import type { AppEnv } from '../src/types'
import {
  applyFieldPermissions,
  applyFieldPermissionsToPage,
  attachReadOnlyMeta,
} from '../src/routes/field-permissions/enforcement'

const mockGetTenantFieldRules = vi.fn()
const mockGetCustomerWithStats = vi.fn()
const mockGetProjectWithStats = vi.fn()
const mockGetContractor = vi.fn()
const mockGetLeadDetail = vi.fn()
const mockListLeadActivities = vi.fn()
const CUSTOMER_ID = '00000000-0000-4000-8000-000000000001'

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

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

vi.mock('../src/middleware/require-module-enabled', () => ({
  requireModuleEnabled: () => async (_c: unknown, next: () => Promise<void>) => next(),
}))

vi.mock('@zync/db/queries', async () => {
  const actual = await vi.importActual<Record<string, unknown>>('@zync/db/queries')
  return {
    ...actual,
    getTenantFieldRules: (...args: unknown[]) => mockGetTenantFieldRules(...args),
    getCustomerWithStats: (...args: unknown[]) => mockGetCustomerWithStats(...args),
    getProjectWithStats: (...args: unknown[]) => mockGetProjectWithStats(...args),
    getContractor: (...args: unknown[]) => mockGetContractor(...args),
    getLeadDetail: (...args: unknown[]) => mockGetLeadDetail(...args),
    listLeadActivities: (...args: unknown[]) => mockListLeadActivities(...args),
  }
})

import { customersIndexRoute } from '../src/routes/customers/index'
import { projectCrudRoute } from '../src/routes/projects/crud'
import { contractorRoutes } from '../src/routes/contractors/router'
import { leadsRoute } from '../src/routes/marketing/leads'

function appFor(routeBase: string, route: Hono<AppEnv>) {
  const app = new Hono<AppEnv>()
  app.use('*', async (c, next) => {
    c.set('session', {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      role: 'CONTRACTOR',
      permissions: ['customers:read', 'projects:read', 'payouts:read', 'marketing:read'],
    })
    c.set('db', {})
    await next()
  })
  app.route(routeBase, route)
  return app
}

describe('field permission enforcement on entity reads', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGetTenantFieldRules.mockResolvedValue([
      { entityType: 'customer', fieldName: 'phone', role: 'CONTRACTOR', permission: 'hidden' },
      { entityType: 'project', fieldName: 'billing_config', role: 'CONTRACTOR', permission: 'read_only' },
      { entityType: 'contractor', fieldName: 'hourly_rate', role: 'CONTRACTOR', permission: 'hidden' },
      { entityType: 'lead', fieldName: 'score', role: 'CONTRACTOR', permission: 'hidden' },
      { entityType: 'lead', fieldName: 'notes', role: 'CONTRACTOR', permission: 'read_only' },
    ])
    mockGetCustomerWithStats.mockResolvedValue({
      id: CUSTOMER_ID,
      name: 'Acme',
      phone: '555-0100',
      email: 'ops@acme.test',
    })
    mockGetProjectWithStats.mockResolvedValue({
      id: 'project-1',
      name: 'Alpha',
      billing_config: { rate_per_hour: 125 },
    })
    mockGetContractor.mockResolvedValue({
      id: 'contractor-1',
      full_name: 'Dana',
      hourly_rate: '150.00',
    })
    mockGetLeadDetail.mockResolvedValue({
      id: 'lead-1',
      name: 'Rivka',
      score: 88,
      notes: 'High intent',
    })
    mockListLeadActivities.mockResolvedValue({ items: [] })
  })

  it('strips customer hidden fields and attaches readOnly metadata when present', async () => {
    const res = await appFor('/api/customers', customersIndexRoute).request(
      `/api/customers/${CUSTOMER_ID}`,
      undefined,
      {} as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    await expect(res.json()).resolves.toEqual({
      id: CUSTOMER_ID,
      name: 'Acme',
      email: 'ops@acme.test',
      _meta: { readOnly: [] },
    })
  })

  it('keeps read-only project billing_config but marks it in metadata', async () => {
    const res = await appFor('/api/projects', projectCrudRoute).request(
      '/api/projects/project-1',
      undefined,
      {} as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    await expect(res.json()).resolves.toEqual({
      id: 'project-1',
      name: 'Alpha',
      billing_config: { rate_per_hour: 125 },
      _meta: { readOnly: ['billing_config'] },
    })
  })

  it('strips contractor hourly_rate from detail responses', async () => {
    const res = await appFor('/api/contractors', contractorRoutes).request(
      '/api/contractors/contractor-1',
      undefined,
      {} as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    await expect(res.json()).resolves.toEqual({
      id: 'contractor-1',
      full_name: 'Dana',
      _meta: { readOnly: [] },
    })
  })

  it('applies hidden and read-only lead rules to lead detail payloads', async () => {
    const res = await appFor('/api/marketing/leads', leadsRoute).request(
      '/api/marketing/leads/lead-1',
      undefined,
      {} as AppEnv['Bindings'],
    )

    expect(res.status).toBe(200)
    await expect(res.json()).resolves.toEqual({
      lead: {
        id: 'lead-1',
        name: 'Rivka',
        notes: 'High intent',
      },
      activities: [],
      _meta: { readOnly: ['notes'] },
    })
  })
})

describe('field permission helper typing', () => {
  it('preserves concrete entity and page shapes for typed route payloads', () => {
    type Lead = {
      id: string
      tenantId: string
      name: string
      email: string
      score?: number
      notes?: string
      stage: string
      stagePosition: number
    }

    type LeadPage = {
      items: Lead[]
      nextCursor: string | null
      total: number
    }

    const lead: Lead = {
      id: 'lead-1',
      tenantId: 'tenant-1',
      name: 'Rivka',
      email: 'rivka@example.com',
      score: 88,
      notes: 'High intent',
      stage: 'new',
      stagePosition: 1,
    }

    const rules = [
      { entityType: 'lead', fieldName: 'score', role: 'CONTRACTOR', permission: 'hidden' },
      { entityType: 'lead', fieldName: 'notes', role: 'CONTRACTOR', permission: 'read_only' },
    ]

    const filteredLead = applyFieldPermissions(lead, 'lead', 'CONTRACTOR', rules)
    const filteredPage = applyFieldPermissionsToPage(
      { items: [lead], nextCursor: null, total: 1 } satisfies LeadPage,
      'lead',
      'CONTRACTOR',
      rules,
    )
    const leadWithMeta = attachReadOnlyMeta(filteredLead.data, filteredLead.readOnly)

    expect(filteredLead.readOnly).toEqual(['notes'])
    expect(filteredPage.items[0]).not.toHaveProperty('score')
    expectTypeOf(filteredLead.data).toEqualTypeOf<Lead>()
    expectTypeOf(filteredPage.items).toEqualTypeOf<LeadPage['items']>()
    expectTypeOf(leadWithMeta).toEqualTypeOf<Lead & { _meta: { readOnly: string[] } }>()
  })
})
