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

const mockSession = {
  type: 'user' as const,
  sub: 'user-1',
  tid: 'tenant-1',
  role: 'OWNER',
  permissions: ['customers:read', 'customers:write', 'users:invite'],
}

const mockGetContactById = vi.fn()
const mockGetRoleByName = vi.fn()
const mockCreateInvitation = vi.fn()
const mockAppendCustomerCommunication = vi.fn()
const mockListCommunications = vi.fn()
const mockSendPortalInvitationEmail = vi.fn()
const mockSendCustomerEmail = vi.fn()
const mockListMergeSuggestions = vi.fn()
const mockDismissSuggestion = vi.fn()
const mockMergeCustomers = vi.fn()
const mockScanForDuplicates = vi.fn()

const { requirePermissionMock, requireTierMock, registeredPermissions, registeredTiers } = vi.hoisted(() => {
  const registeredPermissions: string[] = []
  const registeredTiers: string[] = []
  return {
    requirePermissionMock: vi.fn((arg: unknown) => {
      registeredPermissions.push(arg as string)
      return async (_c: unknown, next: () => Promise<void>) => next()
    }),
    requireTierMock: vi.fn((arg: unknown) => {
      registeredTiers.push(arg as string)
      return async (_c: unknown, next: () => Promise<void>) => next()
    }),
    registeredPermissions,
    registeredTiers,
  }
})

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

vi.mock('@zync/auth', () => ({
  generateOpaqueToken: vi.fn(() => 'plain-token'),
  hashToken: vi.fn(async () => 'token-hash'),
  revokeAllPortalSessions: vi.fn(async () => undefined),
}))

vi.mock('@zync/db/queries', () => ({
  appendCustomerCommunication: (...args: unknown[]) => mockAppendCustomerCommunication(...args),
  createInvitation: (...args: unknown[]) => mockCreateInvitation(...args),
  getContactById: (...args: unknown[]) => mockGetContactById(...args),
  getRoleByName: (...args: unknown[]) => mockGetRoleByName(...args),
  listCommunications: (...args: unknown[]) => mockListCommunications(...args),
  listPortalUsers: vi.fn(),
  setPortalUserStatus: vi.fn(),
  listMergeSuggestions: (...args: unknown[]) => mockListMergeSuggestions(...args),
  dismissSuggestion: (...args: unknown[]) => mockDismissSuggestion(...args),
  mergeCustomers: (...args: unknown[]) => mockMergeCustomers(...args),
  scanForDuplicates: (...args: unknown[]) => mockScanForDuplicates(...args),
}))

vi.mock('../src/adapters/email-customers', () => ({
  sendPortalInvitationEmail: (...args: unknown[]) => mockSendPortalInvitationEmail(...args),
  sendCustomerEmail: (...args: unknown[]) => mockSendCustomerEmail(...args),
}))

import { portalRoute } from '../src/routes/customers/portal'
import { communicationsRoute } from '../src/routes/customers/communications'
import { customerDedupRoutes } from '../src/routes/customers/dedup'

const env = {
  DB: { connectionString: 'postgresql://test:test@localhost/test' },
  JWT_SECRET: 'secret',
} as AppEnv['Bindings']

function portalApp() {
  const app = new Hono<AppEnv>()
  app.use('*', async (c, next) => {
    c.set('session', mockSession)
    c.set('db', {})
    await next()
  })
  app.route('/api/customers', portalRoute)
  return app
}

function communicationsApp() {
  const app = new Hono<AppEnv>()
  app.use('*', async (c, next) => {
    c.set('session', mockSession)
    c.set('db', {})
    await next()
  })
  app.route('/api/customers', communicationsRoute)
  return app
}

function dedupApp(sessionOverrides: Partial<typeof mockSession> = {}) {
  const app = new Hono<AppEnv>()
  app.use('*', async (c, next) => {
    c.set('session', { ...mockSession, ...sessionOverrides })
    c.set('db', {})
    await next()
  })
  app.route('/api/customers', customerDedupRoutes)
  return app
}

describe('customers portal routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGetContactById.mockResolvedValue({
      id: 'contact-1',
      customerId: 'customer-1',
      email: 'contact@example.com',
    })
    mockGetRoleByName.mockResolvedValue({ id: 'viewer-role' })
    mockCreateInvitation.mockResolvedValue({ id: 'invite-1' })
    mockAppendCustomerCommunication.mockResolvedValue({ id: 'comm-1' })
  })

  it('invites a portal contact using the stored contact email without requiring a body', async () => {
    const res = await portalApp().request(
      '/api/customers/customer-1/contacts/contact-1/invite-portal',
      { method: 'POST' },
      env,
    )

    expect(res.status).toBe(201)
    expect(mockGetContactById).toHaveBeenCalledWith({}, 'tenant-1', 'contact-1')
    expect(mockCreateInvitation).toHaveBeenCalledWith(
      {},
      expect.objectContaining({ email: 'contact@example.com', roleId: 'viewer-role' }),
    )
    expect(mockSendPortalInvitationEmail).toHaveBeenCalledWith(
      env,
      expect.objectContaining({ to: 'contact@example.com' }),
    )
  })

  it('returns 404 when the contact does not belong to the customer', async () => {
    mockGetContactById.mockResolvedValueOnce({
      id: 'contact-1',
      customerId: 'other-customer',
      email: 'contact@example.com',
    })

    const res = await portalApp().request(
      '/api/customers/customer-1/contacts/contact-1/invite-portal',
      { method: 'POST' },
      env,
    )

    expect(res.status).toBe(404)
    expect(mockCreateInvitation).not.toHaveBeenCalled()
  })
})

describe('customers communications routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockAppendCustomerCommunication.mockResolvedValue({ id: 'comm-1' })
  })

  it('accepts spec-shaped to_address for outbound email creation', async () => {
    const res = await communicationsApp().request(
      '/api/customers/customer-1/communications',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          direction: 'outbound',
          channel: 'email',
          subject: 'Invoice sent',
          body: 'Please review the attached invoice.',
          to_address: 'billing@example.com',
        }),
      },
      env,
    )

    expect(res.status).toBe(201)
    expect(mockSendCustomerEmail).toHaveBeenCalledWith(
      env,
      expect.objectContaining({ to: 'billing@example.com' }),
    )
    expect(mockAppendCustomerCommunication).toHaveBeenCalledWith(
      {},
      'tenant-1',
      'customer-1',
      expect.objectContaining({ toAddress: 'billing@example.com' }),
    )
  })
})

describe('customer dedup routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockListMergeSuggestions.mockResolvedValue({ suggestions: [], total: 0 })
    mockDismissSuggestion.mockResolvedValue(undefined)
    mockScanForDuplicates.mockResolvedValue(0)
    mockMergeCustomers.mockResolvedValue({
      keptCustomerId: 'customer-keep',
      archivedCustomerId: 'customer-delete',
      reassigned: {
        invoices: 1,
        projects: 2,
        contacts: 3,
        tickets: 4,
        portalUsers: 5,
        activities: 6,
      },
    })
  })

  it('lists duplicates from the spec endpoint with business tier + customers:read guard', async () => {
    const res = await dedupApp().request('/api/customers/duplicates?limit=20', { method: 'GET' }, env)

    expect(res.status).toBe(200)
    expect(registeredTiers).toContain(TenantTier.BUSINESS)
    expect(registeredPermissions).toContain('customers:read')
    expect(mockListMergeSuggestions).toHaveBeenCalledWith({}, 'tenant-1', { limit: 20 })
    expect(await res.json()).toEqual({ suggestions: [], total: 0 })
  })

  it('dismisses via /api/customers/duplicates/:id/dismiss and returns 204', async () => {
    const res = await dedupApp().request('/api/customers/duplicates/550e8400-e29b-41d4-a716-446655440000/dismiss', { method: 'POST' }, env)

    expect(res.status).toBe(204)
    expect(mockDismissSuggestion).toHaveBeenCalledWith(
      {},
      'tenant-1',
      '550e8400-e29b-41d4-a716-446655440000',
      'user-1',
    )
  })

  it('merges via /api/customers/merge using keepId/deleteId and maps typed errors', async () => {
    const res = await dedupApp().request('/api/customers/merge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        keepId: '550e8400-e29b-41d4-a716-446655440000',
        deleteId: '550e8400-e29b-41d4-a716-446655440001',
      }),
    }, env)

    expect(res.status).toBe(200)
    expect(registeredPermissions).toContain('customers:delete')
    expect(mockMergeCustomers).toHaveBeenCalledWith(
      {},
      'tenant-1',
      {
        userId: 'user-1',
        name: null,
        email: null,
      },
      {
        keepId: '550e8400-e29b-41d4-a716-446655440000',
        deleteId: '550e8400-e29b-41d4-a716-446655440001',
      },
    )
  })

  it('rejects keepId === deleteId as 400', async () => {
    const res = await dedupApp().request('/api/customers/merge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        keepId: '550e8400-e29b-41d4-a716-446655440000',
        deleteId: '550e8400-e29b-41d4-a716-446655440000',
      }),
    }, env)

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