import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'
import { renderHook } from '@testing-library/react'
import { useAutoSave } from '../useAutoSave'
import { SendConfirmationDialog } from '../send-confirmation-dialog'
import { ComposeHeader } from '../compose-header'
import { SaveStatusIndicator } from '../save-status-indicator'
import { SubjectInput } from '../subject-input'
import { ComposeLoading } from '../compose-loading'

// Mock Radix portal to render inline for all dialog tests
jest.mock('@radix-ui/react-dialog', () => {
  const actual = jest.requireActual('@radix-ui/react-dialog')
  return {
    ...actual,
    Portal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  }
})

// ---------------------------------------------------------------------------
// useAutoSave
// ---------------------------------------------------------------------------

describe('useAutoSave', () => {
  beforeEach(() => {
    jest.useFakeTimers()
  })

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

  it('initializes with no unsaved changes when content matches snapshot', () => {
    const onSave = jest.fn().mockResolvedValue(undefined)
    const { result } = renderHook(() =>
      useAutoSave({ serializedContent: '', onSave, delay: 1000 })
    )
    // lastSavedContentRef starts as '' and serializedContent is '' — no changes
    expect(result.current.hasUnsavedChanges).toBe(false)
  })

  it('detects unsaved changes when serializedContent differs from snapshot', () => {
    const onSave = jest.fn().mockResolvedValue(undefined)
    const { result } = renderHook(() =>
      useAutoSave({ serializedContent: 'hello world', onSave, delay: 1000 })
    )
    // lastSavedContentRef.current is '' but serializedContent is 'hello world'
    expect(result.current.hasUnsavedChanges).toBe(true)
  })

  it('fires onSave after the debounce delay when content has changed', async () => {
    const onSave = jest.fn().mockResolvedValue(undefined)
    renderHook(() =>
      useAutoSave({ serializedContent: 'changed', onSave, delay: 1000 })
    )
    expect(onSave).not.toHaveBeenCalled()
    act(() => {
      jest.advanceTimersByTime(1000)
    })
    expect(onSave).toHaveBeenCalledTimes(1)
    expect(onSave).toHaveBeenCalledWith(false)
  })

  it('does not fire onSave when content matches the snapshot', () => {
    const onSave = jest.fn().mockResolvedValue(undefined)
    const { result } = renderHook(() =>
      useAutoSave({ serializedContent: '', onSave, delay: 1000 })
    )
    // Take a snapshot of the initial content so they match
    act(() => {
      result.current.snapshotContent('')
    })
    act(() => {
      jest.advanceTimersByTime(2000)
    })
    expect(onSave).not.toHaveBeenCalled()
  })

  it('tracks that snapshotContent updates the saved-content reference', () => {
    // snapshotContent stores the "known saved" baseline in a ref.
    // The hasUnsavedChanges flag is derived by comparing serializedContent to
    // that ref inside a useEffect. This test verifies the snapshot mechanism:
    // start with no changes, take a snapshot of new content, then verify the
    // hook re-evaluates when content changes relative to that snapshot.
    const onSave = jest.fn().mockResolvedValue(undefined)
    const { result, rerender } = renderHook(
      ({ content }: { content: string }) =>
        useAutoSave({ serializedContent: content, onSave, delay: 1000 }),
      { initialProps: { content: '' } }
    )
    // Empty string matches initial ref — no unsaved changes
    expect(result.current.hasUnsavedChanges).toBe(false)

    // Snapshot '' as the saved state, then change content to 'edited'
    act(() => {
      result.current.snapshotContent('')
      rerender({ content: 'edited' })
    })
    // 'edited' !== '' (snapshotted) — changes should now be detected
    expect(result.current.hasUnsavedChanges).toBe(true)
  })

  it('debounces: only fires once when content changes rapidly', () => {
    const onSave = jest.fn().mockResolvedValue(undefined)
    const { rerender } = renderHook(
      ({ content }: { content: string }) =>
        useAutoSave({ serializedContent: content, onSave, delay: 1000 }),
      { initialProps: { content: 'v1' } }
    )
    rerender({ content: 'v2' })
    rerender({ content: 'v3' })
    act(() => {
      jest.advanceTimersByTime(1000)
    })
    expect(onSave).toHaveBeenCalledTimes(1)
  })

  it('uses default delay of 30000ms when no delay is provided', () => {
    const onSave = jest.fn().mockResolvedValue(undefined)
    renderHook(() =>
      useAutoSave({ serializedContent: 'some content', onSave })
    )
    act(() => {
      jest.advanceTimersByTime(29999)
    })
    expect(onSave).not.toHaveBeenCalled()
    act(() => {
      jest.advanceTimersByTime(1)
    })
    expect(onSave).toHaveBeenCalledTimes(1)
  })

  it('clears the timer on unmount to prevent calling save after cleanup', () => {
    const onSave = jest.fn().mockResolvedValue(undefined)
    const { unmount } = renderHook(() =>
      useAutoSave({ serializedContent: 'will unmount', onSave, delay: 1000 })
    )
    unmount()
    act(() => {
      jest.advanceTimersByTime(2000)
    })
    expect(onSave).not.toHaveBeenCalled()
  })

  describe('setSaving / setLastSaved', () => {
    it('exposes setSaving to update saving state', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { result } = renderHook(() =>
        useAutoSave({ serializedContent: '', onSave, delay: 1000 })
      )
      expect(result.current.saving).toBe(false)
      act(() => {
        result.current.setSaving(true)
      })
      expect(result.current.saving).toBe(true)
    })

    it('exposes setLastSaved to update lastSaved date', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { result } = renderHook(() =>
        useAutoSave({ serializedContent: '', onSave, delay: 1000 })
      )
      expect(result.current.lastSaved).toBeNull()
      const now = new Date()
      act(() => {
        result.current.setLastSaved(now)
      })
      expect(result.current.lastSaved).toBe(now)
    })
  })

  describe('formatLastSaved', () => {
    it('returns null when lastSaved is null', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { result } = renderHook(() =>
        useAutoSave({ serializedContent: '', onSave })
      )
      expect(result.current.formatLastSaved()).toBeNull()
    })

    it('returns "just now" when saved less than 60 seconds ago', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { result } = renderHook(() =>
        useAutoSave({ serializedContent: '', onSave })
      )
      act(() => {
        result.current.setLastSaved(new Date(Date.now() - 30000))
      })
      expect(result.current.formatLastSaved()).toBe('just now')
    })

    it('returns minutes ago when saved between 1 and 60 minutes ago', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { result } = renderHook(() =>
        useAutoSave({ serializedContent: '', onSave })
      )
      act(() => {
        result.current.setLastSaved(new Date(Date.now() - 5 * 60000))
      })
      expect(result.current.formatLastSaved()).toBe('5m ago')
    })

    it('returns locale time string when saved more than 60 minutes ago', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { result } = renderHook(() =>
        useAutoSave({ serializedContent: '', onSave })
      )
      const savedDate = new Date(Date.now() - 2 * 3600000)
      act(() => {
        result.current.setLastSaved(savedDate)
      })
      expect(result.current.formatLastSaved()).toBe(savedDate.toLocaleTimeString())
    })

    it('returns null for an invalid Date', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { result } = renderHook(() =>
        useAutoSave({ serializedContent: '', onSave })
      )
      act(() => {
        result.current.setLastSaved(new Date('invalid'))
      })
      expect(result.current.formatLastSaved()).toBeNull()
    })
  })

  describe('beforeunload listener', () => {
    it('adds a beforeunload listener when there are unsaved changes', () => {
      const addSpy = jest.spyOn(window, 'addEventListener')
      const onSave = jest.fn().mockResolvedValue(undefined)
      renderHook(() =>
        useAutoSave({ serializedContent: 'unsaved', onSave, delay: 1000 })
      )
      const calls = addSpy.mock.calls.filter(([event]) => event === 'beforeunload')
      expect(calls.length).toBeGreaterThan(0)
      addSpy.mockRestore()
    })

    it('prevents navigation when there are unsaved changes', () => {
      const onSave = jest.fn().mockResolvedValue(undefined)
      renderHook(() =>
        useAutoSave({ serializedContent: 'unsaved', onSave, delay: 1000 })
      )
      const event = new Event('beforeunload') as BeforeUnloadEvent
      Object.defineProperty(event, 'returnValue', { writable: true, value: '' })
      const preventDefaultSpy = jest.spyOn(event, 'preventDefault')
      window.dispatchEvent(event)
      expect(preventDefaultSpy).toHaveBeenCalled()
    })

    it('removes the beforeunload listener on unmount', () => {
      const removeSpy = jest.spyOn(window, 'removeEventListener')
      const onSave = jest.fn().mockResolvedValue(undefined)
      const { unmount } = renderHook(() =>
        useAutoSave({ serializedContent: 'unsaved', onSave, delay: 1000 })
      )
      unmount()
      const calls = removeSpy.mock.calls.filter(([event]) => event === 'beforeunload')
      expect(calls.length).toBeGreaterThan(0)
      removeSpy.mockRestore()
    })
  })
})

// ---------------------------------------------------------------------------
// SendConfirmationDialog
// ---------------------------------------------------------------------------

describe('SendConfirmationDialog', () => {
  const defaultProps = {
    open: true,
    onOpenChange: jest.fn(),
    onConfirm: jest.fn(),
    sending: false,
    summaryItems: [
      { label: 'Recipients', value: '42 contacts' },
      { label: 'Subject', value: 'Q1 Update' },
    ],
  }

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

  it('renders the default title', () => {
    render(<SendConfirmationDialog {...defaultProps} />)
    expect(screen.getByText('Send Update')).toBeInTheDocument()
  })

  it('renders a custom title', () => {
    render(<SendConfirmationDialog {...defaultProps} title="Send Campaign" />)
    expect(screen.getByText('Send Campaign')).toBeInTheDocument()
  })

  it('renders the default description', () => {
    render(<SendConfirmationDialog {...defaultProps} />)
    expect(screen.getByText('Are you ready to send this?')).toBeInTheDocument()
  })

  it('renders a custom description', () => {
    render(
      <SendConfirmationDialog {...defaultProps} description="Please confirm the send." />
    )
    expect(screen.getByText('Please confirm the send.')).toBeInTheDocument()
  })

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

  it('renders the default warning message', () => {
    render(<SendConfirmationDialog {...defaultProps} />)
    expect(
      screen.getByText(/Emails will be sent immediately/)
    ).toBeInTheDocument()
  })

  it('renders a custom warning message', () => {
    render(
      <SendConfirmationDialog {...defaultProps} warningMessage="This action cannot be undone." />
    )
    expect(screen.getByText('This action cannot be undone.')).toBeInTheDocument()
  })

  it('does not render a warning block when warningMessage is empty string', () => {
    const { container } = render(
      <SendConfirmationDialog {...defaultProps} warningMessage="" />
    )
    expect(container.querySelector('.bg-amber-50')).not.toBeInTheDocument()
  })

  it('calls onConfirm when "Send Now" is clicked', () => {
    render(<SendConfirmationDialog {...defaultProps} />)
    fireEvent.click(screen.getByText('Send Now'))
    expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1)
  })

  it('calls onOpenChange(false) when Cancel is clicked', () => {
    render(<SendConfirmationDialog {...defaultProps} />)
    fireEvent.click(screen.getByText('Cancel'))
    expect(defaultProps.onOpenChange).toHaveBeenCalledWith(false)
  })

  it('disables both buttons while sending', () => {
    render(<SendConfirmationDialog {...defaultProps} sending />)
    const cancelBtn = screen.getByText('Cancel').closest('button')
    const sendingBtn = screen.getByText('Sending...').closest('button')
    expect(cancelBtn).toBeDisabled()
    expect(sendingBtn).toBeDisabled()
  })

  it('shows "Sending..." with a spinner while sending', () => {
    const { container } = render(<SendConfirmationDialog {...defaultProps} sending />)
    expect(screen.getByText('Sending...')).toBeInTheDocument()
    expect(container.querySelector('.animate-spin')).toBeInTheDocument()
  })

  it('shows "Send Now" when not sending', () => {
    render(<SendConfirmationDialog {...defaultProps} />)
    expect(screen.getByText('Send Now')).toBeInTheDocument()
    expect(screen.queryByText('Sending...')).not.toBeInTheDocument()
  })

  it('renders multiple summary items correctly', () => {
    const items = [
      { label: 'To', value: '100 contacts' },
      { label: 'From', value: 'hello@example.com' },
      { label: 'Schedule', value: 'Immediately' },
    ]
    render(<SendConfirmationDialog {...defaultProps} summaryItems={items} />)
    expect(screen.getByText('To:')).toBeInTheDocument()
    expect(screen.getByText('100 contacts')).toBeInTheDocument()
    expect(screen.getByText('From:')).toBeInTheDocument()
    expect(screen.getByText('hello@example.com')).toBeInTheDocument()
    expect(screen.getByText('Schedule:')).toBeInTheDocument()
    expect(screen.getByText('Immediately')).toBeInTheDocument()
  })

  it('does not render when open is false', () => {
    render(<SendConfirmationDialog {...defaultProps} open={false} />)
    expect(screen.queryByText('Send Update')).not.toBeInTheDocument()
  })
})

// ---------------------------------------------------------------------------
// ComposeHeader
// ---------------------------------------------------------------------------

describe('ComposeHeader', () => {
  const defaultProps = {
    title: 'Monthly Newsletter',
    saving: false,
    lastSavedLabel: null,
    hasUnsavedChanges: false,
    canSend: true,
    onBack: jest.fn(),
    onSave: jest.fn(),
    onPreview: jest.fn(),
    onSend: jest.fn(),
  }

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

  it('renders the title', () => {
    render(<ComposeHeader {...defaultProps} />)
    expect(screen.getByText('Monthly Newsletter')).toBeInTheDocument()
  })

  it('calls onBack when the back button is clicked', () => {
    render(<ComposeHeader {...defaultProps} />)
    // ArrowLeft icon button — find by its container role since it has no label text
    const buttons = screen.getAllByRole('button')
    // First button is the back button (ghost icon button)
    fireEvent.click(buttons[0])
    expect(defaultProps.onBack).toHaveBeenCalledTimes(1)
  })

  it('calls onSave when Save is clicked', () => {
    render(<ComposeHeader {...defaultProps} />)
    fireEvent.click(screen.getByText('Save'))
    expect(defaultProps.onSave).toHaveBeenCalledTimes(1)
  })

  it('calls onPreview when Preview is clicked', () => {
    render(<ComposeHeader {...defaultProps} />)
    fireEvent.click(screen.getByText('Preview'))
    expect(defaultProps.onPreview).toHaveBeenCalledTimes(1)
  })

  it('calls onSend when Send is clicked', () => {
    render(<ComposeHeader {...defaultProps} />)
    fireEvent.click(screen.getByText('Send'))
    expect(defaultProps.onSend).toHaveBeenCalledTimes(1)
  })

  it('disables Save button while saving', () => {
    render(<ComposeHeader {...defaultProps} saving />)
    const saveBtn = screen.getByText('Save').closest('button')
    expect(saveBtn).toBeDisabled()
  })

  it('disables Send button when canSend is false', () => {
    render(<ComposeHeader {...defaultProps} canSend={false} />)
    const sendBtn = screen.getByText('Send').closest('button')
    expect(sendBtn).toBeDisabled()
  })

  it('enables Send button when canSend is true', () => {
    render(<ComposeHeader {...defaultProps} canSend />)
    const sendBtn = screen.getByText('Send').closest('button')
    expect(sendBtn).not.toBeDisabled()
  })

  it('renders extraActions when provided', () => {
    render(
      <ComposeHeader
        {...defaultProps}
        extraActions={<button data-testid="extra-action">Schedule</button>}
      />
    )
    expect(screen.getByTestId('extra-action')).toBeInTheDocument()
  })

  it('renders SaveStatusIndicator with the correct props (saving state)', () => {
    render(<ComposeHeader {...defaultProps} saving />)
    expect(screen.getByText('Saving...')).toBeInTheDocument()
  })

  it('renders SaveStatusIndicator with lastSavedLabel', () => {
    render(<ComposeHeader {...defaultProps} lastSavedLabel="just now" />)
    expect(screen.getByText('Saved just now')).toBeInTheDocument()
  })

  it('renders SaveStatusIndicator showing unsaved changes', () => {
    render(<ComposeHeader {...defaultProps} hasUnsavedChanges />)
    expect(screen.getByText('Unsaved changes')).toBeInTheDocument()
  })
})

// ---------------------------------------------------------------------------
// SaveStatusIndicator
// ---------------------------------------------------------------------------

describe('SaveStatusIndicator', () => {
  it('shows "Saving..." when saving is true', () => {
    render(
      <SaveStatusIndicator saving lastSavedLabel={null} hasUnsavedChanges={false} />
    )
    expect(screen.getByText('Saving...')).toBeInTheDocument()
  })

  it('shows a spinning loader icon when saving', () => {
    const { container } = render(
      <SaveStatusIndicator saving lastSavedLabel={null} hasUnsavedChanges={false} />
    )
    expect(container.querySelector('.animate-spin')).toBeInTheDocument()
  })

  it('shows "Saved <label>" when lastSavedLabel is provided and not saving', () => {
    render(
      <SaveStatusIndicator saving={false} lastSavedLabel="just now" hasUnsavedChanges={false} />
    )
    expect(screen.getByText('Saved just now')).toBeInTheDocument()
  })

  it('shows "Unsaved changes" when hasUnsavedChanges is true and no lastSavedLabel', () => {
    render(
      <SaveStatusIndicator saving={false} lastSavedLabel={null} hasUnsavedChanges />
    )
    expect(screen.getByText('Unsaved changes')).toBeInTheDocument()
  })

  it('shows "New draft" when not saving, no lastSavedLabel, no unsaved changes', () => {
    render(
      <SaveStatusIndicator saving={false} lastSavedLabel={null} hasUnsavedChanges={false} />
    )
    expect(screen.getByText('New draft')).toBeInTheDocument()
  })

  it('prioritizes "Saving..." over other states when saving=true', () => {
    render(
      <SaveStatusIndicator saving lastSavedLabel="just now" hasUnsavedChanges />
    )
    expect(screen.getByText('Saving...')).toBeInTheDocument()
    expect(screen.queryByText('Saved just now')).not.toBeInTheDocument()
    expect(screen.queryByText('Unsaved changes')).not.toBeInTheDocument()
  })

  it('prioritizes lastSavedLabel over unsaved changes when not saving', () => {
    render(
      <SaveStatusIndicator saving={false} lastSavedLabel="5m ago" hasUnsavedChanges />
    )
    expect(screen.getByText('Saved 5m ago')).toBeInTheDocument()
    expect(screen.queryByText('Unsaved changes')).not.toBeInTheDocument()
  })

  it('shows amber dot for unsaved changes', () => {
    const { container } = render(
      <SaveStatusIndicator saving={false} lastSavedLabel={null} hasUnsavedChanges />
    )
    expect(container.querySelector('.bg-amber-400')).toBeInTheDocument()
  })

  it('shows green dot when lastSavedLabel is set', () => {
    const { container } = render(
      <SaveStatusIndicator saving={false} lastSavedLabel="just now" hasUnsavedChanges={false} />
    )
    expect(container.querySelector('.bg-green-500')).toBeInTheDocument()
  })

  it('shows a bordered dot (new draft state) when no changes and no lastSavedLabel', () => {
    const { container } = render(
      <SaveStatusIndicator saving={false} lastSavedLabel={null} hasUnsavedChanges={false} />
    )
    const dot = container.querySelector('.border.border-muted-foreground')
    expect(dot).toBeInTheDocument()
  })
})

// ---------------------------------------------------------------------------
// SubjectInput
// ---------------------------------------------------------------------------

describe('SubjectInput', () => {
  it('renders an input with the given value', () => {
    render(<SubjectInput value="Hello World" onChange={jest.fn()} />)
    const input = screen.getByRole('textbox')
    expect(input).toHaveValue('Hello World')
  })

  it('renders an empty input when value is empty string', () => {
    render(<SubjectInput value="" onChange={jest.fn()} />)
    expect(screen.getByRole('textbox')).toHaveValue('')
  })

  it('calls onChange with the new value when user types', () => {
    const onChange = jest.fn()
    render(<SubjectInput value="" onChange={onChange} />)
    fireEvent.change(screen.getByRole('textbox'), { target: { value: 'New subject' } })
    expect(onChange).toHaveBeenCalledWith('New subject')
    expect(onChange).toHaveBeenCalledTimes(1)
  })

  it('renders with the default placeholder', () => {
    render(<SubjectInput value="" onChange={jest.fn()} />)
    expect(screen.getByPlaceholderText('Subject line...')).toBeInTheDocument()
  })

  it('renders with a custom placeholder', () => {
    render(
      <SubjectInput value="" onChange={jest.fn()} placeholder="Enter subject..." />
    )
    expect(screen.getByPlaceholderText('Enter subject...')).toBeInTheDocument()
  })

  it('has id="subject" for label association', () => {
    render(<SubjectInput value="Test" onChange={jest.fn()} />)
    expect(screen.getByRole('textbox')).toHaveAttribute('id', 'subject')
  })

  it('does not call onChange when value prop changes without user interaction', () => {
    const onChange = jest.fn()
    const { rerender } = render(<SubjectInput value="v1" onChange={onChange} />)
    rerender(<SubjectInput value="v2" onChange={onChange} />)
    expect(onChange).not.toHaveBeenCalled()
  })
})

// ---------------------------------------------------------------------------
// ComposeLoading
// ---------------------------------------------------------------------------

describe('ComposeLoading', () => {
  it('renders without crashing', () => {
    const { container } = render(<ComposeLoading />)
    expect(container.firstChild).toBeInTheDocument()
  })

  it('renders within a padded container', () => {
    const { container } = render(<ComposeLoading />)
    expect(container.querySelector('.p-6')).toBeInTheDocument()
  })

  it('renders an animate-pulse wrapper for the skeleton', () => {
    const { container } = render(<ComposeLoading />)
    expect(container.querySelector('.animate-pulse')).toBeInTheDocument()
  })

  it('renders three distinct skeleton blocks', () => {
    const { container } = render(<ComposeLoading />)
    // header skeleton, subject/toolbar skeleton, body skeleton
    const skeletonBlocks = container.querySelectorAll('.animate-pulse > div')
    expect(skeletonBlocks.length).toBe(3)
  })

  it('renders the header skeleton with a narrower width', () => {
    const { container } = render(<ComposeLoading />)
    const headerBlock = container.querySelector('.w-1\\/4')
    expect(headerBlock).toBeInTheDocument()
  })

  it('renders full-width skeleton blocks for content areas', () => {
    const { container } = render(<ComposeLoading />)
    const animatePulse = container.querySelector('.animate-pulse')
    const divs = animatePulse?.querySelectorAll(':scope > div')
    // Blocks 2 and 3 should not have a width constraint (full width)
    expect(divs?.[1]).not.toHaveClass('w-1/4')
    expect(divs?.[2]).not.toHaveClass('w-1/4')
  })
})
