import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ScheduleDialog } from '../schedule-dialog'
import { TemplatePicker, EmailTemplate } from '../template-picker'
import {
  MergeFieldsMenu,
  MergeFieldPreview,
  replaceMergeFields,
} from '../merge-fields'
import { PreviewDialog } from '../preview-dialog'

// ---------------------------------------------------------------------------
// Shared mocks
// ---------------------------------------------------------------------------

// Render Radix portals inline so dialog content is queryable
jest.mock('@radix-ui/react-dialog', () => {
  const actual = jest.requireActual('@radix-ui/react-dialog')
  return {
    ...actual,
    Portal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  }
})

// Render Radix dropdown portal inline
jest.mock('@radix-ui/react-dropdown-menu', () => {
  const actual = jest.requireActual('@radix-ui/react-dropdown-menu')
  return {
    ...actual,
    Portal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  }
})

// Render Radix select portal inline
jest.mock('@radix-ui/react-select', () => {
  const actual = jest.requireActual('@radix-ui/react-select')
  return {
    ...actual,
    Portal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  }
})

// Render Radix popover portal inline
jest.mock('@radix-ui/react-popover', () => {
  const actual = jest.requireActual('@radix-ui/react-popover')
  return {
    ...actual,
    Portal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  }
})

// Mock useToast so we can assert toast calls without real toast state
const mockToast = jest.fn()
jest.mock('../../toast', () => ({
  useToast: () => ({ toast: mockToast }),
}))

// ---------------------------------------------------------------------------
// ScheduleDialog
// ---------------------------------------------------------------------------

describe('ScheduleDialog', () => {
  const defaultProps = {
    open: true,
    onOpenChange: jest.fn(),
    onSchedule: jest.fn().mockResolvedValue(undefined),
    summaryItems: [
      { label: 'Subject', value: 'Q1 Update' },
      { label: 'Recipients', value: '150 contacts' },
    ],
  }

  beforeEach(() => {
    jest.clearAllMocks()
    jest.useFakeTimers()
    jest.setSystemTime(new Date('2026-01-01T08:00:00.000Z'))
  })

  afterEach(() => {
    jest.runOnlyPendingTimers()
    jest.useRealTimers()
  })

  it('renders the default title', () => {
    render(<ScheduleDialog {...defaultProps} />)
    expect(screen.getByRole('heading', { name: 'Schedule Send' })).toBeInTheDocument()
  })

  it('renders the default description', () => {
    render(<ScheduleDialog {...defaultProps} />)
    expect(screen.getByText('Choose when to send this email')).toBeInTheDocument()
  })

  it('renders a custom title and description', () => {
    render(
      <ScheduleDialog
        {...defaultProps}
        title="Schedule Campaign"
        description="Pick a delivery time"
      />
    )
    expect(screen.getByRole('heading', { name: 'Schedule Campaign' })).toBeInTheDocument()
    expect(screen.getByText('Pick a delivery time')).toBeInTheDocument()
  })

  it('renders summary item labels and values', () => {
    render(<ScheduleDialog {...defaultProps} />)
    expect(screen.getByText('Subject:')).toBeInTheDocument()
    expect(screen.getByText('Q1 Update')).toBeInTheDocument()
    expect(screen.getByText('Recipients:')).toBeInTheDocument()
    expect(screen.getByText('150 contacts')).toBeInTheDocument()
  })

  it('renders all four quick schedule option buttons', () => {
    render(<ScheduleDialog {...defaultProps} />)
    expect(screen.getByRole('button', { name: 'Tomorrow 9 AM' })).toBeInTheDocument()
    expect(screen.getByRole('button', { name: 'Tomorrow 2 PM' })).toBeInTheDocument()
    expect(screen.getByRole('button', { name: 'Monday 9 AM' })).toBeInTheDocument()
    expect(screen.getByRole('button', { name: 'Next week' })).toBeInTheDocument()
  })

  it('renders Date and Time labels', () => {
    render(<ScheduleDialog {...defaultProps} />)
    expect(screen.getByText('Date')).toBeInTheDocument()
    expect(screen.getByText('Time')).toBeInTheDocument()
  })

  it('renders Cancel and Schedule Send buttons', () => {
    render(<ScheduleDialog {...defaultProps} />)
    expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
    expect(screen.getByRole('button', { name: /schedule send/i })).toBeInTheDocument()
  })

  it('calls onOpenChange(false) when Cancel is clicked', () => {
    render(<ScheduleDialog {...defaultProps} />)
    fireEvent.click(screen.getByRole('button', { name: /cancel/i }))
    expect(defaultProps.onOpenChange).toHaveBeenCalledWith(false)
  })

  it('Schedule Send button is enabled when default date is in the future', () => {
    // System time = 2026-01-01T08:00:00Z. Default date = addDays(now, 1) at 09:00 — valid.
    render(<ScheduleDialog {...defaultProps} />)
    expect(screen.getByRole('button', { name: /schedule send/i })).not.toBeDisabled()
  })

  it('calls onSchedule with an ISO date string when Schedule Send is clicked', async () => {
    render(<ScheduleDialog {...defaultProps} />)
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /schedule send/i }))
    })
    await waitFor(() => {
      expect(defaultProps.onSchedule).toHaveBeenCalledTimes(1)
    })
    const calledWith: string = defaultProps.onSchedule.mock.calls[0][0]
    expect(typeof calledWith).toBe('string')
    expect(isNaN(Date.parse(calledWith))).toBe(false)
  })

  it('calls onSaveDraft before onSchedule when hasUnsavedChanges is true', async () => {
    const onSaveDraft = jest.fn().mockResolvedValue(undefined)
    const onSchedule = jest.fn().mockResolvedValue(undefined)
    render(
      <ScheduleDialog
        {...defaultProps}
        onSchedule={onSchedule}
        onSaveDraft={onSaveDraft}
        hasUnsavedChanges
      />
    )
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /schedule send/i }))
    })
    await waitFor(() => {
      expect(onSaveDraft).toHaveBeenCalled()
      expect(onSchedule).toHaveBeenCalled()
    })
    expect(onSaveDraft.mock.invocationCallOrder[0]).toBeLessThan(
      onSchedule.mock.invocationCallOrder[0]
    )
  })

  it('does not call onSaveDraft when hasUnsavedChanges is false', async () => {
    const onSaveDraft = jest.fn().mockResolvedValue(undefined)
    const onSchedule = jest.fn().mockResolvedValue(undefined)
    render(
      <ScheduleDialog
        {...defaultProps}
        onSchedule={onSchedule}
        onSaveDraft={onSaveDraft}
        hasUnsavedChanges={false}
      />
    )
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /schedule send/i }))
    })
    await waitFor(() => expect(onSchedule).toHaveBeenCalled())
    expect(onSaveDraft).not.toHaveBeenCalled()
  })

  it('calls onPrepareRecipients before onSchedule when provided', async () => {
    const onPrepareRecipients = jest.fn().mockResolvedValue(undefined)
    const onSchedule = jest.fn().mockResolvedValue(undefined)
    render(
      <ScheduleDialog
        {...defaultProps}
        onSchedule={onSchedule}
        onPrepareRecipients={onPrepareRecipients}
      />
    )
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /schedule send/i }))
    })
    await waitFor(() => {
      expect(onPrepareRecipients).toHaveBeenCalled()
      expect(onSchedule).toHaveBeenCalled()
    })
    expect(onPrepareRecipients.mock.invocationCallOrder[0]).toBeLessThan(
      onSchedule.mock.invocationCallOrder[0]
    )
  })

  it('shows a success toast and closes dialog after successful scheduling', async () => {
    const onOpenChange = jest.fn()
    render(<ScheduleDialog {...defaultProps} onOpenChange={onOpenChange} />)
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /schedule send/i }))
    })
    await waitFor(() => {
      expect(mockToast).toHaveBeenCalledWith(
        expect.objectContaining({ description: expect.stringContaining('Scheduled for') })
      )
      expect(onOpenChange).toHaveBeenCalledWith(false)
    })
  })

  it('shows a destructive toast when onSchedule throws', async () => {
    const onSchedule = jest.fn().mockRejectedValue(new Error('Network error'))
    render(<ScheduleDialog {...defaultProps} onSchedule={onSchedule} />)
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /schedule send/i }))
    })
    await waitFor(() => {
      expect(mockToast).toHaveBeenCalledWith(
        expect.objectContaining({
          description: 'Network error',
          variant: 'destructive',
        })
      )
    })
  })

  it('shows scheduled preview after clicking a quick option', async () => {
    render(<ScheduleDialog {...defaultProps} />)
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: 'Next week' }))
    })
    expect(screen.getByText(/Scheduled for:/)).toBeInTheDocument()
  })

  it('renders custom scheduledInfoMessage in the preview box', async () => {
    render(
      <ScheduleDialog
        {...defaultProps}
        scheduledInfoMessage="Remember to review before send."
      />
    )
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: 'Next week' }))
    })
    expect(screen.getByText('Remember to review before send.')).toBeInTheDocument()
  })

  it('does not render when open is false', () => {
    render(<ScheduleDialog {...defaultProps} open={false} />)
    expect(screen.queryByRole('heading', { name: 'Schedule Send' })).not.toBeInTheDocument()
  })
})

// ---------------------------------------------------------------------------
// TemplatePicker
// ---------------------------------------------------------------------------

const SAMPLE_TEMPLATES: EmailTemplate[] = [
  {
    id: '1',
    name: 'Welcome Email',
    description: 'Onboarding welcome message',
    subject: 'Welcome to our platform!',
    blocks: null,
    category: 'onboarding',
    isDefault: true,
    useCount: 10,
  },
  {
    id: '2',
    name: 'Follow Up',
    description: 'Post-demo follow up',
    subject: 'Great talking with you',
    blocks: null,
    category: 'sales',
    isDefault: false,
    useCount: 5,
  },
  {
    id: '3',
    name: 'Newsletter',
    description: null,
    subject: 'Monthly updates',
    blocks: null,
    category: 'marketing',
    isDefault: false,
    useCount: 0,
  },
]

describe('TemplatePicker', () => {
  const defaultProps = {
    open: true,
    onOpenChange: jest.fn(),
    onSelectTemplate: jest.fn(),
    onLoadTemplates: jest.fn().mockResolvedValue(SAMPLE_TEMPLATES),
  }

  beforeEach(() => {
    jest.clearAllMocks()
    defaultProps.onLoadTemplates.mockResolvedValue(SAMPLE_TEMPLATES)
  })

  it('shows a loading state while templates are being fetched', async () => {
    let resolve!: (v: EmailTemplate[]) => void
    const onLoadTemplates = jest.fn(
      () => new Promise<EmailTemplate[]>((res) => { resolve = res })
    )
    render(<TemplatePicker {...defaultProps} onLoadTemplates={onLoadTemplates} />)
    expect(screen.getByRole('status', { name: /loading/i })).toBeInTheDocument()
    await act(async () => { resolve([]) })
  })

  it('renders all templates after loading', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => {
      expect(screen.getByText('Welcome Email')).toBeInTheDocument()
      expect(screen.getByText('Follow Up')).toBeInTheDocument()
      expect(screen.getByText('Newsletter')).toBeInTheDocument()
    })
  })

  it('renders template descriptions when present', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => {
      expect(screen.getByText('Onboarding welcome message')).toBeInTheDocument()
      expect(screen.getByText('Post-demo follow up')).toBeInTheDocument()
    })
  })

  it('renders template subjects', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => {
      expect(screen.getByText(/Welcome to our platform!/)).toBeInTheDocument()
    })
  })

  it('renders a "Default" badge for default templates', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => {
      expect(screen.getByText('Default')).toBeInTheDocument()
    })
  })

  it('shows available template count', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => {
      expect(screen.getByText('3 templates available')).toBeInTheDocument()
    })
  })

  it('renders the search input', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => {
      expect(screen.getByPlaceholderText('Search templates...')).toBeInTheDocument()
    })
  })

  it('filters templates by name when searching', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => screen.getByPlaceholderText('Search templates...'))

    fireEvent.change(screen.getByPlaceholderText('Search templates...'), {
      target: { value: 'follow' },
    })

    expect(screen.getByText('Follow Up')).toBeInTheDocument()
    expect(screen.queryByText('Welcome Email')).not.toBeInTheDocument()
    expect(screen.queryByText('Newsletter')).not.toBeInTheDocument()
  })

  it('filters templates by subject when searching', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => screen.getByPlaceholderText('Search templates...'))

    fireEvent.change(screen.getByPlaceholderText('Search templates...'), {
      target: { value: 'monthly' },
    })

    expect(screen.getByText('Newsletter')).toBeInTheDocument()
    expect(screen.queryByText('Welcome Email')).not.toBeInTheDocument()
  })

  it('filters templates by description when searching', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => screen.getByPlaceholderText('Search templates...'))

    fireEvent.change(screen.getByPlaceholderText('Search templates...'), {
      target: { value: 'onboarding' },
    })

    expect(screen.getByText('Welcome Email')).toBeInTheDocument()
    expect(screen.queryByText('Follow Up')).not.toBeInTheDocument()
  })

  it('shows "No templates match your search" when no results', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => screen.getByPlaceholderText('Search templates...'))

    fireEvent.change(screen.getByPlaceholderText('Search templates...'), {
      target: { value: 'xyzzy-no-match' },
    })

    expect(screen.getByText('No templates match your search')).toBeInTheDocument()
  })

  it('updates count as search filters results', async () => {
    render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => screen.getByPlaceholderText('Search templates...'))

    fireEvent.change(screen.getByPlaceholderText('Search templates...'), {
      target: { value: 'follow' },
    })

    expect(screen.getByText('1 template available')).toBeInTheDocument()
  })

  it('calls onSelectTemplate with the clicked template and closes the dialog', async () => {
    const onSelectTemplate = jest.fn()
    const onOpenChange = jest.fn()
    render(
      <TemplatePicker
        {...defaultProps}
        onSelectTemplate={onSelectTemplate}
        onOpenChange={onOpenChange}
      />
    )
    await waitFor(() => screen.getByText('Follow Up'))

    fireEvent.click(screen.getByRole('option', { name: /select follow up template/i }))

    expect(onSelectTemplate).toHaveBeenCalledTimes(1)
    expect(onSelectTemplate).toHaveBeenCalledWith(
      expect.objectContaining({ id: '2', name: 'Follow Up' })
    )
    expect(onOpenChange).toHaveBeenCalledWith(false)
  })

  it('filters by categoryFilter prop', async () => {
    render(<TemplatePicker {...defaultProps} categoryFilter="sales" />)
    await waitFor(() => {
      expect(screen.getByText('Follow Up')).toBeInTheDocument()
      expect(screen.queryByText('Welcome Email')).not.toBeInTheDocument()
      expect(screen.queryByText('Newsletter')).not.toBeInTheDocument()
    })
  })

  it('shows error state when onLoadTemplates rejects', async () => {
    const onLoadTemplates = jest.fn().mockRejectedValue(new Error('API down'))
    render(<TemplatePicker {...defaultProps} onLoadTemplates={onLoadTemplates} />)
    await waitFor(() => {
      expect(screen.getByText('Failed to load templates')).toBeInTheDocument()
    })
  })

  it('shows Retry button on error and retries on click', async () => {
    const onLoadTemplates = jest.fn()
      .mockRejectedValueOnce(new Error('API down'))
      .mockResolvedValueOnce(SAMPLE_TEMPLATES)

    render(<TemplatePicker {...defaultProps} onLoadTemplates={onLoadTemplates} />)
    await waitFor(() => screen.getByText('Retry'))

    fireEvent.click(screen.getByText('Retry'))

    await waitFor(() => {
      expect(screen.getByText('Welcome Email')).toBeInTheDocument()
    })
  })

  it('shows empty state when no templates are returned', async () => {
    const onLoadTemplates = jest.fn().mockResolvedValue([])
    render(<TemplatePicker {...defaultProps} onLoadTemplates={onLoadTemplates} />)
    await waitFor(() => {
      expect(screen.getByText('No templates available')).toBeInTheDocument()
    })
  })

  it('shows "Create Default Templates" button in empty state when onSeedTemplates is provided', async () => {
    const onLoadTemplates = jest.fn().mockResolvedValue([])
    const onSeedTemplates = jest.fn().mockResolvedValue(undefined)
    render(
      <TemplatePicker
        {...defaultProps}
        onLoadTemplates={onLoadTemplates}
        onSeedTemplates={onSeedTemplates}
      />
    )
    await waitFor(() => {
      expect(screen.getByText('Create Default Templates')).toBeInTheDocument()
    })
  })

  it('normalizes templates using subjectTemplate when subject is empty', async () => {
    const rawTemplates = [
      {
        id: '99',
        name: 'Alt Template',
        description: null,
        subject: '',
        blocks: null,
        category: null,
        isDefault: false,
        useCount: 0,
        subjectTemplate: 'Fallback Subject',
      },
    ]
    const onLoadTemplates = jest.fn().mockResolvedValue(rawTemplates)
    render(<TemplatePicker {...defaultProps} onLoadTemplates={onLoadTemplates} />)
    await waitFor(() => {
      expect(screen.getByText(/Fallback Subject/)).toBeInTheDocument()
    })
  })

  it('calls onOpenChange(false) when Cancel is clicked', async () => {
    const onOpenChange = jest.fn()
    render(<TemplatePicker {...defaultProps} onOpenChange={onOpenChange} />)
    await waitFor(() => screen.getByText('Cancel'))
    fireEvent.click(screen.getByText('Cancel'))
    expect(onOpenChange).toHaveBeenCalledWith(false)
  })

  it('does not render content when open is false', () => {
    render(<TemplatePicker {...defaultProps} open={false} />)
    expect(screen.queryByText('Choose a Template')).not.toBeInTheDocument()
  })

  it('resets search when dialog is reopened', async () => {
    const { rerender } = render(<TemplatePicker {...defaultProps} />)
    await waitFor(() => screen.getByPlaceholderText('Search templates...'))

    fireEvent.change(screen.getByPlaceholderText('Search templates...'), {
      target: { value: 'follow' },
    })
    expect(screen.queryByText('Welcome Email')).not.toBeInTheDocument()

    rerender(<TemplatePicker {...defaultProps} open={false} />)
    rerender(<TemplatePicker {...defaultProps} open={true} />)

    await waitFor(() => {
      expect(screen.getByText('Welcome Email')).toBeInTheDocument()
    })
  })
})

// ---------------------------------------------------------------------------
// MergeFieldsMenu
// ---------------------------------------------------------------------------

describe('MergeFieldsMenu', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('renders the "Insert Merge Field" trigger button by default', () => {
    render(<MergeFieldsMenu onInsert={jest.fn()} />)
    expect(screen.getByRole('button', { name: /insert merge field/i })).toBeInTheDocument()
  })

  it('renders a compact trigger button when variant is "compact"', () => {
    render(<MergeFieldsMenu onInsert={jest.fn()} variant="compact" />)
    expect(screen.getByText('Merge')).toBeInTheDocument()
  })

  it('opens the dropdown menu when trigger is clicked', async () => {
    const user = userEvent.setup()
    render(<MergeFieldsMenu onInsert={jest.fn()} />)
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => {
      expect(screen.getByText('Recipient')).toBeInTheDocument()
    })
  })

  it('renders all default category labels after opening', async () => {
    const user = userEvent.setup()
    render(<MergeFieldsMenu onInsert={jest.fn()} />)
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => {
      expect(screen.getByText('Recipient')).toBeInTheDocument()
      // 'Organization' appears as both a category label and a field label —
      // assert at least one instance is present
      expect(screen.getAllByText('Organization').length).toBeGreaterThanOrEqual(1)
      expect(screen.getByText('Sender')).toBeInTheDocument()
      expect(screen.getByText('Date')).toBeInTheDocument()
    })
  })

  it('renders field labels from default fields after opening', async () => {
    const user = userEvent.setup()
    render(<MergeFieldsMenu onInsert={jest.fn()} />)
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => {
      expect(screen.getByText('First Name')).toBeInTheDocument()
      expect(screen.getByText('Last Name')).toBeInTheDocument()
    })
  })

  it('renders example values for each field', async () => {
    const user = userEvent.setup()
    render(<MergeFieldsMenu onInsert={jest.fn()} />)
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => {
      expect(screen.getByText('John')).toBeInTheDocument()
      expect(screen.getByText('Acme Corp')).toBeInTheDocument()
    })
  })

  it('calls onInsert with the field key when a merge field item is clicked', async () => {
    const user = userEvent.setup()
    const onInsert = jest.fn()
    render(<MergeFieldsMenu onInsert={onInsert} />)
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => screen.getByText('First Name'))

    await user.click(screen.getByText('First Name'))

    expect(onInsert).toHaveBeenCalledTimes(1)
    expect(onInsert).toHaveBeenCalledWith('{{recipient.firstName}}')
  })

  it('calls onInsert with sender.name key when "Your Company" is clicked', async () => {
    const user = userEvent.setup()
    const onInsert = jest.fn()
    render(<MergeFieldsMenu onInsert={onInsert} />)
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => screen.getByText('Your Company'))

    await user.click(screen.getByText('Your Company'))

    expect(onInsert).toHaveBeenCalledWith('{{sender.name}}')
  })

  it('renders custom category labels when provided', async () => {
    const user = userEvent.setup()
    const customFields = [
      { key: '{{custom.field}}', label: 'Custom Field', example: 'Value', category: 'custom' },
    ]
    const customCategories = [{ name: 'custom', label: 'Custom' }]

    render(
      <MergeFieldsMenu
        onInsert={jest.fn()}
        fields={customFields}
        categories={customCategories}
      />
    )
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => {
      expect(screen.getByText('Custom')).toBeInTheDocument()
      expect(screen.getByText('Custom Field')).toBeInTheDocument()
    })
  })

  it('calls onInsert with the custom field key', async () => {
    const user = userEvent.setup()
    const onInsert = jest.fn()
    const customFields = [
      { key: '{{custom.field}}', label: 'My Field', example: 'ex', category: 'custom' },
    ]
    const customCategories = [{ name: 'custom', label: 'Custom' }]

    render(
      <MergeFieldsMenu
        onInsert={onInsert}
        fields={customFields}
        categories={customCategories}
      />
    )
    await user.click(screen.getByRole('button', { name: /insert merge field/i }))
    await waitFor(() => screen.getByText('My Field'))

    await user.click(screen.getByText('My Field'))

    expect(onInsert).toHaveBeenCalledWith('{{custom.field}}')
  })
})

// ---------------------------------------------------------------------------
// MergeFieldPreview (pure function)
// ---------------------------------------------------------------------------

describe('MergeFieldPreview', () => {
  it('replaces known merge field tokens with default sample data', () => {
    const result = MergeFieldPreview({ content: 'Hello {{recipient.firstName}}' })
    expect(result).toBe('Hello John')
  })

  it('replaces multiple tokens in a single string', () => {
    const result = MergeFieldPreview({
      content: '{{recipient.firstName}} from {{organization.name}}',
    })
    expect(result).toBe('John from Acme Corp')
  })

  it('overrides default sample data with provided sampleData', () => {
    const result = MergeFieldPreview({
      content: 'Hi {{recipient.firstName}}',
      sampleData: { '{{recipient.firstName}}': 'Alice' },
    })
    expect(result).toBe('Hi Alice')
  })

  it('returns content unchanged when no tokens match', () => {
    const result = MergeFieldPreview({ content: 'Hello world' })
    expect(result).toBe('Hello world')
  })

  it('handles empty string content', () => {
    const result = MergeFieldPreview({ content: '' })
    expect(result).toBe('')
  })

  it('replaces sender name token', () => {
    const result = MergeFieldPreview({
      content: 'From: {{sender.name}}',
      sampleData: { '{{sender.name}}': 'My Biz' },
    })
    expect(result).toBe('From: My Biz')
  })
})

// ---------------------------------------------------------------------------
// replaceMergeFields (pure utility)
// ---------------------------------------------------------------------------

describe('replaceMergeFields', () => {
  it('replaces recipient firstName token', () => {
    const result = replaceMergeFields('Hi {{recipient.firstName}}', { firstName: 'Alice' })
    expect(result).toBe('Hi Alice')
  })

  it('replaces recipient fullName using the name field when firstName/lastName absent', () => {
    const result = replaceMergeFields('{{recipient.fullName}}', { name: 'Bob Jones' })
    expect(result).toBe('Bob Jones')
  })

  it('derives firstName from name when not explicitly provided', () => {
    const result = replaceMergeFields('{{recipient.firstName}}', { name: 'Bob Jones' })
    expect(result).toBe('Bob')
  })

  it('replaces organization name token', () => {
    const result = replaceMergeFields('{{organization.name}}', {}, { name: 'Acme Corp' })
    expect(result).toBe('Acme Corp')
  })

  it('replaces sender tokens', () => {
    const result = replaceMergeFields(
      '{{sender.name}} / {{sender.contactName}}',
      {},
      undefined,
      { name: 'My Biz', contactName: 'Jane' }
    )
    expect(result).toBe('My Biz / Jane')
  })

  it('replaces recipient email token', () => {
    const result = replaceMergeFields('{{recipient.email}}', { email: 'bob@example.com' })
    expect(result).toBe('bob@example.com')
  })

  it('replaces recipient title token', () => {
    const result = replaceMergeFields('{{recipient.title}}', { title: 'CTO' })
    expect(result).toBe('CTO')
  })

  it('replaces title with empty string when not provided', () => {
    const result = replaceMergeFields('Title: {{recipient.title}}', {})
    expect(result).toBe('Title: ')
  })

  it('returns content unchanged when there are no tokens', () => {
    const result = replaceMergeFields('No tokens here', { firstName: 'X' })
    expect(result).toBe('No tokens here')
  })

  it('replaces all occurrences of the same token', () => {
    const result = replaceMergeFields(
      '{{recipient.firstName}} and {{recipient.firstName}}',
      { firstName: 'Alice' }
    )
    expect(result).toBe('Alice and Alice')
  })

  it('replaces organization type token', () => {
    const result = replaceMergeFields('{{organization.type}}', {}, { type: 'Enterprise' })
    expect(result).toBe('Enterprise')
  })
})

// ---------------------------------------------------------------------------
// PreviewDialog
// ---------------------------------------------------------------------------

describe('PreviewDialog', () => {
  const defaultProps = {
    open: true,
    onOpenChange: jest.fn(),
    subject: 'Hello {{recipient.firstName}}',
    bodyHtml: '<p>Welcome to our platform!</p>',
  }

  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('renders the "Preview Email" heading', () => {
    render(<PreviewDialog {...defaultProps} />)
    expect(screen.getByRole('heading', { name: 'Preview Email' })).toBeInTheDocument()
  })

  it('renders the resolved subject line with merge fields substituted', () => {
    render(<PreviewDialog {...defaultProps} />)
    // Default sample recipient has firstName = 'John'
    expect(screen.getByText('Hello John')).toBeInTheDocument()
  })

  it('renders Desktop, Mobile, and HTML toggle tabs', () => {
    render(<PreviewDialog {...defaultProps} />)
    expect(screen.getByRole('tab', { name: /desktop/i })).toBeInTheDocument()
    expect(screen.getByRole('tab', { name: /mobile/i })).toBeInTheDocument()
    expect(screen.getByRole('tab', { name: /html/i })).toBeInTheDocument()
  })

  it('defaults to desktop view with an email iframe', () => {
    render(<PreviewDialog {...defaultProps} />)
    expect(screen.getByTitle('Email preview')).toBeInTheDocument()
  })

  it('switches to mobile view when Mobile tab is clicked', async () => {
    const user = userEvent.setup()
    const { container } = render(<PreviewDialog {...defaultProps} />)
    await user.click(screen.getByRole('tab', { name: /mobile/i }))
    const allDivs = Array.from(container.querySelectorAll('div'))
    const mobileContainer = allDivs.find(d => d.className.includes('max-w-[375px]'))
    expect(mobileContainer).toBeTruthy()
  })

  it('switches back to desktop view when Desktop tab is clicked after mobile', async () => {
    const user = userEvent.setup()
    const { container } = render(<PreviewDialog {...defaultProps} />)
    await user.click(screen.getByRole('tab', { name: /mobile/i }))
    await user.click(screen.getByRole('tab', { name: /desktop/i }))
    const allDivs = Array.from(container.querySelectorAll('div'))
    const mobileContainer = allDivs.find(d => d.className.includes('max-w-[375px]'))
    expect(mobileContainer).toBeFalsy()
    expect(screen.getByTitle('Email preview')).toBeInTheDocument()
  })

  it('switches to HTML source view when HTML tab is clicked', async () => {
    const user = userEvent.setup()
    const { container } = render(<PreviewDialog {...defaultProps} />)
    await user.click(screen.getByRole('tab', { name: /html/i }))
    const pre = container.querySelector('pre')
    expect(pre).toBeInTheDocument()
    expect(pre?.textContent).toContain('<!DOCTYPE html>')
  })

  it('renders the body HTML inside the iframe src document', async () => {
    const user = userEvent.setup()
    const { container } = render(<PreviewDialog {...defaultProps} />)
    await user.click(screen.getByRole('tab', { name: /html/i }))
    const pre = container.querySelector('pre')
    expect(pre?.textContent).toContain('Welcome to our platform!')
  })

  it('renders the unsubscribe text in HTML source by default', async () => {
    const user = userEvent.setup()
    const { container } = render(<PreviewDialog {...defaultProps} />)
    await user.click(screen.getByRole('tab', { name: /html/i }))
    const pre = container.querySelector('pre')
    expect(pre?.textContent).toContain('Unsubscribe from these updates')
  })

  it('omits unsubscribe block when unsubscribeText is null', async () => {
    const user = userEvent.setup()
    const { container } = render(<PreviewDialog {...defaultProps} unsubscribeText={null} />)
    await user.click(screen.getByRole('tab', { name: /html/i }))
    const pre = container.querySelector('pre')
    expect(pre?.textContent).not.toContain('Unsubscribe')
  })

  it('renders custom unsubscribe text in HTML source', async () => {
    const user = userEvent.setup()
    const { container } = render(<PreviewDialog {...defaultProps} unsubscribeText="Opt out" />)
    await user.click(screen.getByRole('tab', { name: /html/i }))
    const pre = container.querySelector('pre')
    expect(pre?.textContent).toContain('Opt out')
  })

  it('renders the select trigger with the default sample recipient name', () => {
    render(<PreviewDialog {...defaultProps} />)
    // The select trigger shows the currently selected recipient name
    expect(screen.getByText('John Smith')).toBeInTheDocument()
  })

  it('applies recipient merge data to the subject preview', () => {
    const recipients = [
      {
        id: 'r1',
        name: 'Alice',
        mergeData: { '{{recipient.firstName}}': 'Alice' },
      },
    ]
    render(<PreviewDialog {...defaultProps} recipients={recipients} />)
    expect(screen.getByText('Hello Alice')).toBeInTheDocument()
  })

  it('uses the first custom recipient label for merge field substitution', () => {
    const recipients = [
      {
        id: 'r1',
        name: 'Alice',
        label: 'Alice (alice@example.com)',
        mergeData: { '{{recipient.firstName}}': 'Alice' },
      },
      {
        id: 'r2',
        name: 'Bob',
        label: 'Bob (bob@example.com)',
        mergeData: { '{{recipient.firstName}}': 'Bob' },
      },
    ]
    render(<PreviewDialog {...defaultProps} recipients={recipients} />)
    // The first recipient is selected by default — subject shows Alice's firstName
    expect(screen.getByText('Hello Alice')).toBeInTheDocument()
  })

  it('calls onOpenChange(false) when the Close button is clicked', () => {
    const onOpenChange = jest.fn()
    render(<PreviewDialog {...defaultProps} onOpenChange={onOpenChange} />)
    // There are two "Close" text occurrences: the visible button and an sr-only span
    // on the dialog's X button. Click the one that is a visible button element.
    const closeButtons = screen.getAllByText('Close')
    const visibleButton = closeButtons.find(
      (el) => el.tagName === 'BUTTON' && !el.classList.contains('sr-only')
    )
    expect(visibleButton).toBeTruthy()
    fireEvent.click(visibleButton!)
    expect(onOpenChange).toHaveBeenCalledWith(false)
  })

  it('does not render when open is false', () => {
    render(<PreviewDialog {...defaultProps} open={false} />)
    expect(screen.queryByRole('heading', { name: 'Preview Email' })).not.toBeInTheDocument()
  })

  it('shows "Subject" label', () => {
    render(<PreviewDialog {...defaultProps} />)
    expect(screen.getByText('Subject')).toBeInTheDocument()
  })

  it('shows "Preview as" label', () => {
    render(<PreviewDialog {...defaultProps} />)
    expect(screen.getByText('Preview as')).toBeInTheDocument()
  })
})
