import { render, screen, fireEvent, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { NoteItem } from './NoteItem'
import { makeEntry } from '../test-utils/noteListTestUtils'

vi.mock('../utils/url', async () => {
  const actual = await vi.importActual('../utils/url') as typeof import('../utils/url')
  return { ...actual, openExternalUrl: vi.fn().mockResolvedValue(undefined) }
})

const { openExternalUrl } = await import('../utils/url') as typeof import('../utils/url') & {
  openExternalUrl: ReturnType<typeof vi.fn>
}

const NOW_SECONDS = 1_744_286_400

describe('NoteItem', () => {
  beforeEach(() => {
    openExternalUrl.mockClear()
  })

  it('renders unsupported binary files as non-clickable muted rows', () => {
    const binaryEntry = makeEntry({
      path: '/vault/archive.zip',
      filename: 'archive.zip',
      title: 'archive.zip',
      fileKind: 'binary',
    })
    const onClickNote = vi.fn()

    render(<NoteItem entry={binaryEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />)

    const item = screen.getByTestId('binary-file-item')
    expect(item.className).toContain('opacity-50')
    expect(item).toHaveAttribute('title', 'Cannot open this file type')

    fireEvent.click(item)
    expect(onClickNote).not.toHaveBeenCalled()
  })

  it('renders image files as clickable rows with an image file indicator', () => {
    const imageEntry = makeEntry({
      path: '/vault/photo.png',
      filename: 'photo.png',
      title: 'photo.png',
      fileKind: 'binary',
    })
    const onClickNote = vi.fn()

    render(<NoteItem entry={imageEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />)

    const item = screen.getByTestId('image-file-item')
    expect(item.className).not.toContain('opacity-50')
    expect(item).toHaveAttribute('title', 'Open image preview')

    fireEvent.click(item)
    expect(onClickNote).toHaveBeenCalledWith(imageEntry, expect.any(Object))
    expect(screen.getByTestId('type-icon')).toHaveAttribute('data-file-preview-kind', 'image')
  })

  it('renders PDF files as clickable rows with a PDF file indicator', () => {
    const pdfEntry = makeEntry({
      path: '/vault/reports/brief.pdf',
      filename: 'brief.pdf',
      title: 'brief.pdf',
      fileKind: 'binary',
    })
    const onClickNote = vi.fn()

    render(<NoteItem entry={pdfEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />)

    const item = screen.getByTestId('pdf-file-item')
    expect(item.className).not.toContain('opacity-50')
    expect(item).toHaveAttribute('title', 'Open PDF preview')

    fireEvent.click(item)
    expect(onClickNote).toHaveBeenCalledWith(pdfEntry, expect.any(Object))
    expect(screen.getByTestId('type-icon')).toHaveAttribute('data-file-preview-kind', 'pdf')
  })

  it('renders audio and video files as clickable media preview rows', () => {
    const audioEntry = makeEntry({
      path: '/vault/attachments/interview.mp3',
      filename: 'interview.mp3',
      title: 'interview.mp3',
      fileKind: 'binary',
    })
    const videoEntry = makeEntry({
      path: '/vault/attachments/demo.mp4',
      filename: 'demo.mp4',
      title: 'demo.mp4',
      fileKind: 'binary',
    })
    const onClickNote = vi.fn()

    render(
      <>
        <NoteItem entry={audioEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />
        <NoteItem entry={videoEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />
      </>,
    )

    expect(screen.getByTestId('audio-file-item')).toHaveAttribute('title', 'Open audio preview')
    expect(screen.getByTestId('video-file-item')).toHaveAttribute('title', 'Open video preview')

    fireEvent.click(screen.getByTestId('audio-file-item'))
    fireEvent.click(screen.getByTestId('video-file-item'))
    expect(onClickNote).toHaveBeenCalledWith(audioEntry, expect.any(Object))
    expect(onClickNote).toHaveBeenCalledWith(videoEntry, expect.any(Object))
  })

  it('renders text files as clickable rows', () => {
    const textEntry = makeEntry({
      path: '/vault/config.yml',
      filename: 'config.yml',
      title: 'config.yml',
      fileKind: 'text',
    })
    const onClickNote = vi.fn()

    render(<NoteItem entry={textEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />)

    const item = screen.getByText('config.yml').closest('div')!
    fireEvent.click(item)
    expect(onClickNote).toHaveBeenCalled()
  })

  it('uses CSS named colors from the Type document for note type indicators', () => {
    const ideaType = makeEntry({
      path: '/vault/type/idea.md',
      filename: 'idea.md',
      title: 'Idea',
      isA: 'Type',
      color: 'cyan',
    })
    const ideaEntry = makeEntry({
      path: '/vault/ideas/native-cyan-idea.md',
      filename: 'native-cyan-idea.md',
      title: 'Native Cyan Idea',
      isA: 'Idea',
    })

    render(
      <NoteItem
        entry={ideaEntry}
        isSelected={false}
        typeEntryMap={{ Idea: ideaType }}
        onClickNote={vi.fn()}
      />,
    )

    expect(screen.getByTestId('type-icon')).toHaveStyle({ color: 'rgb(0, 255, 255)' })
  })

  it('shows the title with filename metadata when a change status is present', () => {
    const entry = {
      ...makeEntry({ filename: 'my-note.md', title: 'My Note Title' }),
      __changeAddedLines: 42,
      __changeDeletedLines: 7,
    }

    render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} changeStatus="modified" />)

    expect(screen.getByText('My Note Title')).toBeInTheDocument()
    expect(screen.getByText('my-note.md')).toBeInTheDocument()
    expect(screen.getByTestId('change-note-filename')).toHaveClass('truncate', 'text-[12px]', 'leading-[1.5]', 'text-muted-foreground')
    expect(screen.getByTestId('change-stat-added')).toHaveTextContent('+42')
    expect(screen.getByTestId('change-stat-deleted')).toHaveTextContent('-7')
  })

  it('renders the correct symbol for modified files', () => {
    const entry = makeEntry({ filename: 'note.md' })

    render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} changeStatus="modified" />)

    expect(screen.getByTestId('change-status-icon').textContent).toBe('·')
  })

  it('renders the correct symbol for added files', () => {
    const entry = makeEntry({ filename: 'new-note.md' })

    render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} changeStatus="added" />)

    expect(screen.getByTestId('change-status-icon').textContent).toBe('+')
  })

  it('shows a neutral fallback when line stats are unavailable', () => {
    const entry = makeEntry({ filename: 'binary-note.md', title: 'Binary Note' })

    render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} changeStatus="modified" />)

    expect(screen.getByTestId('change-stat-fallback')).toHaveTextContent('Diff unavailable')
  })

  it('renders the regular title when no change status is set', () => {
    const entry = makeEntry({ filename: 'note.md', title: 'My Note' })

    render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} />)

    expect(screen.getByText('My Note')).toBeInTheDocument()
    expect(screen.queryByText('note.md')).not.toBeInTheDocument()
    expect(screen.queryByTestId('change-status-icon')).not.toBeInTheDocument()
  })

  it('adds more breathing room between note sections', () => {
    const entry = makeEntry({
      title: 'Spaced note',
      snippet: 'Body preview',
      createdAt: NOW_SECONDS - 86400 * 3,
      modifiedAt: NOW_SECONDS - 86400,
      properties: { Status: 'Active' },
    })

    render(
      <NoteItem
        entry={entry}
        isSelected={false}
        typeEntryMap={{}}
        displayPropsOverride={['Status']}
        onClickNote={vi.fn()}
      />,
    )

    expect(screen.getByTestId('note-content-stack').className).toContain('space-y-2')
  })

  it('shows created date on the right side of the date row when available', () => {
    vi.useFakeTimers()
    vi.setSystemTime(new Date(NOW_SECONDS * 1000))
    const entry = makeEntry({
      title: 'Dated note',
      createdAt: NOW_SECONDS - 86400 * 5,
      modifiedAt: NOW_SECONDS - 86400 * 2,
    })

    render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} />)

    const dateRow = screen.getByTestId('note-date-row')
    expect(dateRow.className).toContain('grid')
    expect(dateRow).toHaveTextContent('April 8, 2025')
    expect(dateRow).toHaveTextContent('Created April 5, 2025')
  })

  it('shows the workspace badge after the creation date as an outlined badge', () => {
    vi.useFakeTimers()
    vi.setSystemTime(new Date(NOW_SECONDS * 1000))
    const personalWorkspace = {
      id: 'personal',
      label: 'Personal',
      alias: 'personal',
      path: '/personal',
      shortLabel: 'PE',
      color: 'blue',
      icon: null,
      mounted: true,
      available: true,
      defaultForNewNotes: true,
    }
    const launchWorkspace = {
      id: 'launch',
      label: 'Launch',
      alias: 'launch',
      path: '/launch',
      shortLabel: 'LA',
      color: 'red',
      icon: null,
      mounted: true,
      available: true,
      defaultForNewNotes: false,
    }
    const entry = makeEntry({
      title: 'Campaigns',
      createdAt: NOW_SECONDS - 600,
      modifiedAt: NOW_SECONDS - 600,
      workspace: launchWorkspace,
    })
    const otherEntry = makeEntry({
      path: '/personal/other.md',
      filename: 'other.md',
      title: 'Other',
      workspace: personalWorkspace,
    })

    render(
      <NoteItem
        entry={entry}
        isSelected={false}
        typeEntryMap={{}}
        allEntries={[entry, otherEntry]}
        onClickNote={vi.fn()}
      />,
    )

    const dateRow = screen.getByTestId('note-date-row')
    const badge = within(dateRow).getByTestId('workspace-badge')
    expect(screen.getByTestId('note-title-row')).not.toContainElement(badge)
    expect(dateRow).toHaveTextContent('Created April 10, 2025')
    expect(badge).toHaveTextContent('LA')
    expect(badge).toHaveClass('-mr-1.5', 'border', 'bg-transparent', 'opacity-75')
    expect(badge.getAttribute('style')).toContain('border-color: var(--accent-red)')
    expect(badge.getAttribute('style')).toContain('color: var(--accent-red)')
  })

  it('leaves the right side empty when no creation date exists', () => {
    vi.useFakeTimers()
    vi.setSystemTime(new Date(NOW_SECONDS * 1000))
    const entry = makeEntry({
      title: 'Modified note',
      createdAt: null,
      modifiedAt: NOW_SECONDS - 3600,
    })

    render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} />)

    expect(screen.getByTestId('note-date-row')).toHaveTextContent('April 10, 2025')
    expect(screen.queryByText(/Created /)).not.toBeInTheDocument()
  })

  it('colors relationship chips by target type and opens the related note on Cmd+click only', () => {
    const linkedIdea = makeEntry({
      path: '/vault/ideas/build-app.md',
      filename: 'build-app.md',
      title: 'Build App',
      isA: 'Idea',
    })
    const ideaType = makeEntry({
      path: '/vault/type/idea.md',
      filename: 'idea.md',
      title: 'Idea',
      isA: 'Type',
      color: 'cyan',
      icon: 'wrench',
    })
    const sourceEntry = makeEntry({
      path: '/vault/note/source.md',
      filename: 'source.md',
      title: 'Source',
      isA: 'Note',
      relationships: { 'Belongs to': ['[[ideas/build-app]]'] },
    })
    const onClickNote = vi.fn()

    render(
      <NoteItem
        entry={sourceEntry}
        isSelected={false}
        typeEntryMap={{ Idea: ideaType }}
        allEntries={[sourceEntry, linkedIdea, ideaType]}
        displayPropsOverride={['Belongs to']}
        onClickNote={onClickNote}
      />,
    )

    const chip = screen.getByTestId('property-chip-belongs-to-0')
    const row = chip.closest('[data-note-path]')
    expect(chip).toHaveTextContent('Build App')
    expect(chip.className).toContain('cursor-pointer')
    expect(chip).toHaveStyle({ color: 'rgb(0, 255, 255)' })
    expect(chip.getAttribute('style')).toContain('background-color: color-mix(in srgb, cyan 14%, transparent)')
    expect(row?.tagName).toBe('DIV')
    expect(row).toHaveAttribute('role', 'option')

    fireEvent.click(chip)
    expect(onClickNote).not.toHaveBeenCalled()

    fireEvent.click(chip, { metaKey: true })
    expect(onClickNote).toHaveBeenCalledWith(linkedIdea, expect.objectContaining({ metaKey: true }))
  })

  it('falls back to the built-in type icon for relationship chips when the Type has no custom icon', () => {
    const linkedTopic = makeEntry({
      path: '/vault/topic/ai.md',
      filename: 'ai.md',
      title: 'AI',
      isA: 'topic',
    })
    const topicType = makeEntry({
      path: '/vault/type/topic.md',
      filename: 'topic.md',
      title: 'Topic',
      isA: 'Type',
      color: 'green',
      icon: null,
    })
    const sourceEntry = makeEntry({
      path: '/vault/note/source.md',
      filename: 'source.md',
      title: 'Source',
      isA: 'Note',
      relationships: { Topics: ['[[topic/ai]]'] },
    })

    render(
      <NoteItem
        entry={sourceEntry}
        isSelected={false}
        typeEntryMap={{ Topic: topicType, topic: topicType }}
        allEntries={[sourceEntry, linkedTopic, topicType]}
        displayPropsOverride={['Topics']}
        onClickNote={vi.fn()}
      />,
    )

    const chip = screen.getByTestId('property-chip-topics-0')
    expect(chip).toHaveTextContent('AI')
    expect(chip).toHaveStyle({ color: 'var(--accent-green)', backgroundColor: 'var(--accent-green-light)' })
    expect(chip.querySelector('svg')).not.toBeNull()
  })

  it('preserves exact linked note title formatting in relationship chips', () => {
    const linkedTopic = makeEntry({
      path: '/vault/topic/ai-ml.md',
      filename: 'ai-ml.md',
      title: 'AI / ML',
      isA: 'Topic',
    })
    const topicType = makeEntry({
      path: '/vault/type/topic.md',
      filename: 'topic.md',
      title: 'Topic',
      isA: 'Type',
      color: 'green',
    })
    const sourceEntry = makeEntry({
      path: '/vault/note/source.md',
      filename: 'source.md',
      title: 'Source',
      isA: 'Note',
      relationships: { Topics: ['[[topic/ai-ml]]'] },
    })

    render(
      <NoteItem
        entry={sourceEntry}
        isSelected={false}
        typeEntryMap={{ Topic: topicType }}
        allEntries={[sourceEntry, linkedTopic, topicType]}
        displayPropsOverride={['Topics']}
        onClickNote={vi.fn()}
      />,
    )

    expect(screen.getByTestId('property-chip-topics-0')).toHaveTextContent('AI / ML')
  })

  it('keeps explicit wikilink aliases in relationship chips', () => {
    const linkedProject = makeEntry({
      path: '/vault/project/my-project.md',
      filename: 'my-project.md',
      title: 'My Project',
      isA: 'Project',
    })
    const sourceEntry = makeEntry({
      path: '/vault/note/source.md',
      filename: 'source.md',
      title: 'Source',
      isA: 'Note',
      relationships: { 'Belongs to': ['[[project/my-project|My Cool Project]]'] },
    })

    render(
      <NoteItem
        entry={sourceEntry}
        isSelected={false}
        typeEntryMap={{}}
        allEntries={[sourceEntry, linkedProject]}
        displayPropsOverride={['Belongs to']}
        onClickNote={vi.fn()}
      />,
    )

    expect(screen.getByTestId('property-chip-belongs-to-0')).toHaveTextContent('My Cool Project')
  })

  it('opens URL chips on Cmd+click only and keeps regular clicks inert', () => {
    const entry = makeEntry({
      path: '/vault/note/source.md',
      filename: 'source.md',
      title: 'Source',
      properties: { URL: 'https://example.com/docs' },
    })
    const onClickNote = vi.fn()

    render(
      <NoteItem
        entry={entry}
        isSelected={false}
        typeEntryMap={{}}
        displayPropsOverride={['URL']}
        onClickNote={onClickNote}
      />,
    )

    const chip = screen.getByTestId('property-chip-url-0')
    expect(chip).toHaveTextContent('example.com')
    expect(chip.className).toContain('cursor-pointer')
    expect(chip).toHaveStyle({ color: 'var(--accent-blue)', backgroundColor: 'var(--accent-blue-light)' })

    fireEvent.click(chip)
    expect(openExternalUrl).not.toHaveBeenCalled()
    expect(onClickNote).not.toHaveBeenCalled()

    fireEvent.click(chip, { metaKey: true })
    expect(openExternalUrl).toHaveBeenCalledWith('https://example.com/docs')
    expect(onClickNote).not.toHaveBeenCalled()
  })

  it('renders broken relationship chips as neutral and non-interactive', () => {
    const entry = makeEntry({
      path: '/vault/note/source.md',
      filename: 'source.md',
      title: 'Source',
      relationships: { Related: ['[[missing/note]]'] },
    })
    const onClickNote = vi.fn()

    render(
      <NoteItem
        entry={entry}
        isSelected={false}
        typeEntryMap={{}}
        allEntries={[entry]}
        displayPropsOverride={['Related']}
        onClickNote={onClickNote}
      />,
    )

    const chip = screen.getByTestId('property-chip-related-0')
    expect(chip).toHaveTextContent('Note')
    expect(chip.className).not.toContain('cursor-pointer')

    fireEvent.click(chip, { metaKey: true })
    expect(onClickNote).not.toHaveBeenCalled()
    expect(openExternalUrl).not.toHaveBeenCalled()
  })
})
