import type { ComponentProps } from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render as rtlRender, screen, fireEvent, act, within } from '@testing-library/react'
import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel, InstancesPanel } from './InspectorPanels'
import { TooltipProvider } from './ui/tooltip'
import type { ReferencedByItem } from './InspectorPanels'
import type { VaultEntry, GitCommit } from '../types'

// jsdom doesn't implement scrollIntoView
Element.prototype.scrollIntoView = vi.fn()

const render = (ui: Parameters<typeof rtlRender>[0]) => rtlRender(ui, { wrapper: TooltipProvider })

const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
  path: '/vault/note/test.md',
  filename: 'test.md',
  title: 'Test Note',
  isA: 'Note',
  aliases: [],
  belongsTo: [],
  relatedTo: [],
  status: 'Active',
  owner: null,
  cadence: null,
  archived: false,
  modifiedAt: 1700000000,
  createdAt: 1700000000,
  fileSize: 100,
  snippet: '',
  wordCount: 0,
  relationships: {},
  icon: null,
  color: null,
  order: null,
  template: null, sort: null,
  outgoingLinks: [],
  ...overrides,
})

const makeWorkspace = (
  alias: string,
  path: string,
  label = alias,
): NonNullable<VaultEntry['workspace']> => ({
  id: alias,
  label,
  alias,
  path,
  shortLabel: label.slice(0, 2).toUpperCase(),
  color: null,
  icon: null,
  mounted: true,
  available: true,
  defaultForNewNotes: false,
})

describe('DynamicRelationshipsPanel', () => {
  const onNavigate = vi.fn()
  const onAddProperty = vi.fn()
  const personTypeEntry = makeEntry({
    path: '/vault/person.md', filename: 'person.md', title: 'Person',
    isA: 'Type', color: 'yellow', icon: 'user',
  })
  const entries = [
    makeEntry({ path: '/vault/project/my-project.md', filename: 'my-project.md', title: 'My Project', isA: 'Project' }),
    makeEntry({ path: '/vault/topic/ai.md', filename: 'ai.md', title: 'AI', isA: 'Topic' }),
    personTypeEntry,
  ]
  const typeEntryMap: Record<string, VaultEntry> = { Person: personTypeEntry }

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

  const renderRelationshipsPanel = (
    overrides: Partial<ComponentProps<typeof DynamicRelationshipsPanel>> = {},
  ) => render(
    <DynamicRelationshipsPanel
      typeEntryMap={{}}
      frontmatter={{}}
      entries={entries}
      onNavigate={onNavigate}
      {...overrides}
    />,
  )

  it.each([
    {
      name: 'applies type color via typeEntryMap',
      entry: { path: '/vault/people/luca.md', filename: 'luca.md', title: 'Luca', isA: 'Person' as const },
      ref: '[[Luca]]', key: 'Owner', tMap: typeEntryMap, expectedColor: 'var(--accent-yellow)',
    },
    {
      name: 'resolves by title when filename differs',
      entry: { path: '/vault/people/john-doe.md', filename: 'john-doe.md', title: 'John Doe', isA: 'Person' as const },
      ref: '[[John Doe]]', key: 'Owner', tMap: typeEntryMap, expectedColor: 'var(--accent-yellow)',
    },
    {
      name: 'shows neutral color for notes with no type',
      entry: { path: '/vault/misc/random.md', filename: 'random.md', title: 'Random', isA: null },
      ref: '[[Random]]', key: 'Related to', tMap: {} as Record<string, VaultEntry>, expectedColor: 'var(--muted-foreground)',
    },
  ])('$name', ({ entry: overrides, ref, key, tMap, expectedColor }) => {
    const { container } = render(
      <DynamicRelationshipsPanel
        typeEntryMap={tMap}
        frontmatter={{ [key]: [ref] }}
        entries={[...entries, makeEntry(overrides)]}
        onNavigate={onNavigate}
      />
    )
    const chip = container.querySelector('.group\\/link')
    expect(chip).toBeTruthy()
    expect(chip!.style.color).toBe(expectedColor)
  })

  it('hides empty state label when frontmatter has no relations', () => {
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{ Status: 'Active', title: 'Test' }}
        entries={entries}
        onNavigate={onNavigate}
      />
    )
    expect(screen.queryByText('No relationships')).not.toBeInTheDocument()
  })

  it('renders relationship groups with wikilinks', () => {
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{
          'Belongs to': ['[[project/my-project]]'],
          'Related to': ['[[topic/ai]]'],
        }}
        entries={entries}
        onNavigate={onNavigate}
      />
    )
    expect(screen.getByText('Belongs to')).toBeInTheDocument()
    expect(screen.getByText('Related to')).toBeInTheDocument()
  })

  it('shows missing type-defined relationships as editable placeholders', () => {
    const bookType = makeEntry({
      path: '/vault/book.md',
      filename: 'book.md',
      title: 'Book',
      isA: 'Type',
      relationships: {
        Mentor: ['[[person/alice]]'],
      },
    })

    renderRelationshipsPanel({
      entry: makeEntry({ title: 'Dune', isA: 'Book' }),
      entries: [...entries, bookType],
      frontmatter: {},
      onAddProperty,
    })

    const placeholder = screen.getByTestId('type-derived-relationship')
    expect(within(placeholder).getByText('Mentor')).toHaveClass('text-muted-foreground/40')
    fireEvent.click(within(placeholder).getByTestId('add-relation-ref'))
    fireEvent.change(within(placeholder).getByTestId('add-relation-ref-input'), {
      target: { value: 'My Project' },
    })
    fireEvent.keyDown(within(placeholder).getByTestId('add-relation-ref-input'), { key: 'Enter' })

    expect(onAddProperty).toHaveBeenCalledWith('Mentor', '[[project/my-project]]')
  })

  it('navigates when clicking a relationship link', () => {
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{ 'Belongs to': ['[[project/my-project]]'] }}
        entries={entries}
        onNavigate={onNavigate}
      />
    )
    // Click the rendered link
    const link = screen.getByText('My Project')
    fireEvent.click(link)
    expect(onNavigate).toHaveBeenCalledWith('project/my-project')
  })

  it('renders single string wikilink value', () => {
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{ Owner: '[[person/luca]]' }}
        entries={[makeEntry({ path: '/vault/person/luca.md', filename: 'luca.md', title: 'Luca', isA: 'Person' })]  }
        onNavigate={onNavigate}
      />
    )
    expect(screen.getByText('Owner')).toBeInTheDocument()
    expect(screen.getByText('Luca')).toBeInTheDocument()
  })

  it('renders + Add relationship button when onAddProperty provided', () => {
    renderRelationshipsPanel({ onAddProperty })
    expect(screen.getByText('+ Add relationship')).toBeInTheDocument()
  })

  it('opens add relationship form when button clicked', () => {
    renderRelationshipsPanel({ onAddProperty })
    fireEvent.click(screen.getByText('+ Add relationship'))
    expect(screen.getByPlaceholderText('Relationship name')).toBeInTheDocument()
    expect(screen.getByPlaceholderText('Note title')).toBeInTheDocument()
  })

  it('adds relationship via form', () => {
    renderRelationshipsPanel({ onAddProperty })
    fireEvent.click(screen.getByText('+ Add relationship'))
    fireEvent.change(screen.getByPlaceholderText('Relationship name'), { target: { value: 'Related to' } })
    fireEvent.change(screen.getByPlaceholderText('Note title'), { target: { value: 'AI' } })
    fireEvent.click(screen.getByTestId('submit-add-relationship'))
    expect(onAddProperty).toHaveBeenCalledWith('Related to', '[[topic/ai]]')
  })

  it('prefixes selected cross-workspace relationship targets from the add form', () => {
    const personal = makeWorkspace('personal', '/personal', 'Personal')
    const team = makeWorkspace('team', '/team', 'Team')
    const source = makeEntry({
      path: '/personal/source.md',
      filename: 'source.md',
      title: 'Source',
      workspace: personal,
    })
    const remote = makeEntry({
      path: '/team/topic/remote.md',
      filename: 'remote.md',
      title: 'Remote',
      workspace: team,
    })

    renderRelationshipsPanel({
      entry: source,
      entries: [source, remote, ...entries],
      vaultPath: '/personal',
      onAddProperty,
    })
    fireEvent.click(screen.getByText('+ Add relationship'))
    fireEvent.change(screen.getByPlaceholderText('Relationship name'), { target: { value: 'Mentions' } })
    const noteInput = screen.getByPlaceholderText('Note title')
    fireEvent.focus(noteInput)
    fireEvent.change(noteInput, { target: { value: 'Remote' } })
    expect(screen.getByTestId('note-search-workspace-badge')).toHaveTextContent('TE')
    fireEvent.click(screen.getByText('Remote'))
    fireEvent.click(screen.getByTestId('submit-add-relationship'))

    expect(onAddProperty).toHaveBeenCalledWith('Mentions', '[[team/topic/remote]]')
  })

  it('cancels add relationship form', () => {
    renderRelationshipsPanel({ onAddProperty })
    fireEvent.click(screen.getByText('+ Add relationship'))
    fireEvent.click(screen.getByText('Cancel'))
    expect(screen.getByText('+ Add relationship')).toBeInTheDocument()
  })

  describe('suggested relationship slots', () => {
    it('shows Belongs to/Related to/Has slots when no relationships exist', () => {
      renderRelationshipsPanel({ onAddProperty })
      const slots = screen.getAllByTestId('suggested-relationship')
      expect(slots.length).toBe(3)
      expect(screen.getByText('Belongs to')).toBeInTheDocument()
      expect(screen.getByText('Related to')).toBeInTheDocument()
      expect(screen.getByText('Has')).toBeInTheDocument()
    })

    it('hides slot when canonical snake_case relationship already exists', () => {
      renderRelationshipsPanel({
        frontmatter: { belongs_to: '[[Project Alpha]]' },
        onAddProperty,
      })
      const slots = screen.getAllByTestId('suggested-relationship')
      expect(slots.length).toBe(2)
      expect(screen.queryAllByText('Belongs to').every(el => !el.closest('[data-testid="suggested-relationship"]'))).toBe(true)
    })

    it('does not show slots when onAddProperty not provided', () => {
      renderRelationshipsPanel()
      expect(screen.queryByTestId('suggested-relationship')).not.toBeInTheDocument()
    })
  })

  it('dims archived entries', () => {
    const archivedEntry = makeEntry({
      path: '/vault/project/old.md', filename: 'old.md', title: 'Old Project', isA: 'Project', archived: true,
    })
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{ 'Belongs to': ['[[project/old]]'] }}
        entries={[archivedEntry]}
        onNavigate={onNavigate}
      />
    )
    // The button title should indicate "Archived" status
    expect(screen.getByTitle('Archived')).toBeInTheDocument()
  })

  it('handles aliased wikilinks [[path|Display]]', () => {
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{ 'Belongs to': ['[[project/my-project|My Cool Project]]'] }}
        entries={entries}
        onNavigate={onNavigate}
      />
    )
    expect(screen.getByText('My Cool Project')).toBeInTheDocument()
  })

  it('shows emoji icon before label when entry has an emoji', () => {
    const emojiEntry = makeEntry({
      path: '/vault/note/rocket.md', filename: 'rocket.md', title: 'Rocket Note', isA: 'Note', icon: '🚀',
    })
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{ 'Belongs to': ['[[Rocket Note]]'] }}
        entries={[emojiEntry]}
        onNavigate={onNavigate}
      />
    )
    expect(screen.getByText('🚀')).toBeInTheDocument()
    expect(screen.getByText('Rocket Note')).toBeInTheDocument()
  })

  it('does not show emoji when entry has no icon', () => {
    render(
      <DynamicRelationshipsPanel
        typeEntryMap={{}}
        frontmatter={{ 'Belongs to': ['[[project/my-project]]'] }}
        entries={entries}
        onNavigate={onNavigate}
      />
    )
    const link = screen.getByText('My Project')
    const container = link.closest('.group\\/link')
    expect(container?.textContent).not.toMatch(/^[\p{Emoji_Presentation}]/u)
  })

  describe('relation editing', () => {
    const onUpdateProperty = vi.fn()
    const onDeleteProperty = vi.fn()
    const renderEditableRelationships = (
      overrides: Partial<ComponentProps<typeof DynamicRelationshipsPanel>> = {},
    ) => renderRelationshipsPanel({
      frontmatter: { 'Belongs to': ['[[project/my-project]]'] },
      onUpdateProperty,
      onDeleteProperty,
      ...overrides,
    })
    const openInlineAdd = (value?: string) => {
      fireEvent.click(screen.getByTestId('add-relation-ref'))
      const input = screen.getByTestId('add-relation-ref-input')
      if (value !== undefined) {
        fireEvent.change(input, { target: { value } })
      }
      return input
    }

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

    it('shows remove buttons on relation refs when editing is enabled', () => {
      renderEditableRelationships()
      expect(screen.getByTestId('remove-relation-ref')).toBeInTheDocument()
    })

    it('does not show remove buttons when editing is disabled', () => {
      renderRelationshipsPanel({ frontmatter: { 'Belongs to': ['[[project/my-project]]'] } })
      expect(screen.queryByTestId('remove-relation-ref')).not.toBeInTheDocument()
    })

    it('calls onDeleteProperty when removing the last ref in a group', () => {
      renderEditableRelationships()
      fireEvent.click(screen.getByTestId('remove-relation-ref'))
      expect(onDeleteProperty).toHaveBeenCalledWith('Belongs to')
    })

    it.each([
      {
        name: 'calls onUpdateProperty with remaining refs when removing one of many',
        frontmatter: { 'Related to': ['[[project/my-project]]', '[[topic/ai]]'] },
        expectedKey: 'Related to',
        expectedValue: '[[topic/ai]]',
      },
      {
        name: 'calls onUpdateProperty with string when two refs become one',
        frontmatter: { Has: ['[[project/my-project]]', '[[topic/ai]]'] },
        expectedKey: 'Has',
        expectedValue: '[[topic/ai]]',
      },
    ])('$name', ({ frontmatter, expectedKey, expectedValue }) => {
      renderEditableRelationships({ frontmatter })
      const removeButtons = screen.getAllByTestId('remove-relation-ref')
      fireEvent.click(removeButtons[0])
      expect(onUpdateProperty).toHaveBeenCalledWith(expectedKey, expectedValue)
    })

    it('shows inline add button for each relationship group when editing is enabled', () => {
      renderEditableRelationships()
      expect(screen.getByTestId('add-relation-ref')).toBeInTheDocument()
    })

    it('opens inline add input when add button clicked', () => {
      renderEditableRelationships()
      openInlineAdd()
      expect(screen.getByTestId('add-relation-ref-input')).toBeInTheDocument()
    })

    it('adds a note to an existing relationship via inline add', () => {
      renderEditableRelationships()
      const input = openInlineAdd('AI')
      fireEvent.keyDown(input, { key: 'Enter' })
      expect(onUpdateProperty).toHaveBeenCalledWith('Belongs to', ['[[project/my-project]]', '[[topic/ai]]'])
    })

    it('clicks a search result to add the relationship and close the inline editor', () => {
      renderEditableRelationships()
      openInlineAdd('AI')
      fireEvent.click(screen.getByText('AI'))

      expect(onUpdateProperty).toHaveBeenCalledWith('Belongs to', ['[[project/my-project]]', '[[topic/ai]]'])
      expect(screen.queryByTestId('add-relation-ref-input')).not.toBeInTheDocument()
    })

    it('prefixes inline cross-workspace relationship targets with the target workspace alias', () => {
      const personal = makeWorkspace('personal', '/personal', 'Personal')
      const team = makeWorkspace('team', '/team', 'Team')
      const source = makeEntry({
        path: '/personal/source.md',
        filename: 'source.md',
        title: 'Source',
        workspace: personal,
      })
      const remote = makeEntry({
        path: '/team/topic/remote.md',
        filename: 'remote.md',
        title: 'Remote',
        workspace: team,
      })

      renderEditableRelationships({
        entry: source,
        entries: [source, remote, ...entries],
        vaultPath: '/personal',
      })
      const input = openInlineAdd('Remote')
      fireEvent.keyDown(input, { key: 'Enter' })

      expect(onUpdateProperty).toHaveBeenCalledWith('Belongs to', ['[[project/my-project]]', '[[team/topic/remote]]'])
    })

    it('does not add duplicate refs', () => {
      renderEditableRelationships({ frontmatter: { 'Belongs to': ['[[topic/ai]]'] } })
      const input = openInlineAdd('AI')
      fireEvent.keyDown(input, { key: 'Enter' })
      expect(onUpdateProperty).not.toHaveBeenCalled()
    })

    it('ignores empty inline-add Enter presses', () => {
      renderEditableRelationships()
      const input = openInlineAdd()
      fireEvent.keyDown(input, { key: 'Enter' })

      expect(onUpdateProperty).not.toHaveBeenCalled()
      expect(screen.queryByTestId('add-relation-ref-input')).not.toBeInTheDocument()
    })

    it('closes inline add on Escape', () => {
      renderEditableRelationships()
      const input = openInlineAdd()
      fireEvent.keyDown(input, { key: 'Escape' })
      expect(screen.queryByTestId('add-relation-ref-input')).not.toBeInTheDocument()
      expect(screen.getByTestId('add-relation-ref')).toBeInTheDocument()
    })
  })

  describe('create & open from inline add', () => {
    const onUpdateProperty = vi.fn()
    const onDeleteProperty = vi.fn()
    const onCreateAndOpenNote = vi.fn<(title: string) => Promise<boolean>>()
    const renderInlineCreatePanel = (
      overrides: Partial<ComponentProps<typeof DynamicRelationshipsPanel>> = {},
    ) => renderRelationshipsPanel({
      frontmatter: { 'Belongs to': ['[[project/my-project]]'] },
      onUpdateProperty,
      onDeleteProperty,
      onCreateAndOpenNote,
      ...overrides,
    })
    const openInlineCreate = (value: string) => {
      fireEvent.click(screen.getByTestId('add-relation-ref'))
      const input = screen.getByTestId('add-relation-ref-input')
      fireEvent.change(input, { target: { value } })
      return input
    }

    beforeEach(() => {
      vi.clearAllMocks()
      onCreateAndOpenNote.mockResolvedValue(true)
    })

    it('shows "Create & open" option when typed title does not match any note', () => {
      renderInlineCreatePanel()
      openInlineCreate('Brand New Note')
      expect(screen.getByTestId('create-and-open-option')).toBeInTheDocument()
      expect(screen.getByText(/Create & open/)).toBeInTheDocument()
      expect(screen.getByText(/Brand New Note/)).toBeInTheDocument()
    })

    it('does not show "Create & open" when typed title matches an existing note', () => {
      renderInlineCreatePanel()
      openInlineCreate('AI')
      expect(screen.queryByTestId('create-and-open-option')).not.toBeInTheDocument()
    })

    it('calls onCreateAndOpenNote and adds wikilink when "Create & open" clicked', async () => {
      renderInlineCreatePanel()
      openInlineCreate('Brand New Note')
      fireEvent.click(screen.getByTestId('create-and-open-option'))
      expect(onCreateAndOpenNote).toHaveBeenCalledWith('Brand New Note')
      await vi.waitFor(() => {
        expect(onUpdateProperty).toHaveBeenCalledWith('Belongs to', ['[[project/my-project]]', '[[brand-new-note]]'])
      })
    })

    it('does not add wikilink when note creation fails', async () => {
      onCreateAndOpenNote.mockResolvedValue(false)
      renderInlineCreatePanel()
      openInlineCreate('Failing Note')
      fireEvent.click(screen.getByTestId('create-and-open-option'))
      expect(onCreateAndOpenNote).toHaveBeenCalledWith('Failing Note')
      // Give async handler time to resolve
      await vi.waitFor(() => {
        expect(onCreateAndOpenNote).toHaveBeenCalled()
      })
      // Wikilink is deferred to next tick and only added on success
      expect(onUpdateProperty).not.toHaveBeenCalled()
    })

    it('shows both existing matches and "Create & open" for partial matches', () => {
      renderInlineCreatePanel()
      // "My" partially matches "My Project" but is not an exact match.
      openInlineCreate('My')
      // Should show search results AND create option
      expect(screen.getByTestId('create-and-open-option')).toBeInTheDocument()
    })

    it('does not show "Create & open" when onCreateAndOpenNote is not provided', () => {
      renderInlineCreatePanel({ onCreateAndOpenNote: undefined })
      openInlineCreate('Brand New Note')
      expect(screen.queryByTestId('create-and-open-option')).not.toBeInTheDocument()
    })
  })

  describe('create & open from AddRelationshipForm', () => {
    const onCreateAndOpenNote = vi.fn<(title: string) => Promise<boolean>>()

    beforeEach(() => {
      vi.clearAllMocks()
      onCreateAndOpenNote.mockResolvedValue(true)
    })

    it('shows "Create & open" option in target input when title does not exist', () => {
      renderRelationshipsPanel({ onAddProperty, onCreateAndOpenNote })
      fireEvent.click(screen.getByText('+ Add relationship'))
      fireEvent.change(screen.getByPlaceholderText('Relationship name'), { target: { value: 'Mentions' } })
      const noteInput = screen.getByPlaceholderText('Note title')
      fireEvent.focus(noteInput)
      fireEvent.change(noteInput, { target: { value: 'New Person' } })
      expect(screen.getByTestId('create-and-open-option')).toBeInTheDocument()
    })

    it('submits from the relationship-name input on Enter and cancels the form from the note input on Escape', () => {
      renderRelationshipsPanel({ onAddProperty, onCreateAndOpenNote })
      fireEvent.click(screen.getByText('+ Add relationship'))
      fireEvent.change(screen.getByPlaceholderText('Relationship name'), { target: { value: 'Mentions' } })
      fireEvent.change(screen.getByPlaceholderText('Note title'), { target: { value: 'AI' } })
      fireEvent.keyDown(screen.getByPlaceholderText('Relationship name'), { key: 'Enter' })

      expect(onAddProperty).toHaveBeenCalledWith('Mentions', '[[topic/ai]]')

      fireEvent.click(screen.getByText('+ Add relationship'))
      fireEvent.keyDown(screen.getByPlaceholderText('Note title'), { key: 'Escape' })
      expect(screen.queryByPlaceholderText('Relationship name')).not.toBeInTheDocument()
    })

    it('hides the target dropdown after blur once the grace timeout expires', () => {
      vi.useFakeTimers()
      renderRelationshipsPanel({ onAddProperty, onCreateAndOpenNote })
      fireEvent.click(screen.getByText('+ Add relationship'))
      fireEvent.change(screen.getByPlaceholderText('Relationship name'), { target: { value: 'Mentions' } })

      const noteInput = screen.getByPlaceholderText('Note title')
      fireEvent.focus(noteInput)
      fireEvent.change(noteInput, { target: { value: 'New Person' } })
      expect(screen.getByTestId('create-and-open-option')).toBeInTheDocument()

      act(() => {
        fireEvent.blur(noteInput)
        vi.advanceTimersByTime(150)
      })

      expect(screen.queryByTestId('create-and-open-option')).not.toBeInTheDocument()
      vi.useRealTimers()
    })

    it('creates note and adds relationship via form', async () => {
      renderRelationshipsPanel({ onAddProperty, onCreateAndOpenNote })
      fireEvent.click(screen.getByText('+ Add relationship'))
      fireEvent.change(screen.getByPlaceholderText('Relationship name'), { target: { value: 'Mentions' } })
      const noteInput = screen.getByPlaceholderText('Note title')
      fireEvent.focus(noteInput)
      fireEvent.change(noteInput, { target: { value: 'New Person' } })
      fireEvent.click(screen.getByTestId('create-and-open-option'))
      expect(onCreateAndOpenNote).toHaveBeenCalledWith('New Person')
      await vi.waitFor(() => {
        expect(onAddProperty).toHaveBeenCalledWith('Mentions', '[[new-person]]')
      })
    })
  })
})

describe('BacklinksPanel', () => {
  const onNavigate = vi.fn()

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

  it('renders nothing when empty', () => {
    const { container } = render(<BacklinksPanel backlinks={[]} onNavigate={onNavigate} />)
    expect(container.innerHTML).toBe('')
  })

  const twoBacklinks = [
    { entry: makeEntry({ title: 'Referencing Note', isA: 'Note' }), context: null },
    { entry: makeEntry({ title: 'Another Note', isA: 'Project', path: '/vault/project/another.md' }), context: null },
  ]

  it('renders header and all backlinks immediately (no collapse)', () => {
    render(<BacklinksPanel backlinks={twoBacklinks} onNavigate={onNavigate} />)
    expect(screen.getByText('Backlinks')).toBeInTheDocument()
    expect(screen.getByText('Referencing Note')).toBeInTheDocument()
    expect(screen.getByText('Another Note')).toBeInTheDocument()
  })

  it('navigates when clicking backlink', () => {
    const backlinks = [{ entry: makeEntry({ title: 'Reference' }), context: null }]
    render(<BacklinksPanel backlinks={backlinks} onNavigate={onNavigate} />)
    fireEvent.click(screen.getByText('Reference'))
    expect(onNavigate).toHaveBeenCalledWith('Reference')
  })

  it('shows paragraph context preview when available', () => {
    const backlinks = [
      { entry: makeEntry({ title: 'Referencing Note' }), context: 'This references [[My Note]] in context.' },
    ]
    render(<BacklinksPanel backlinks={backlinks} onNavigate={onNavigate} />)
    expect(screen.getByText('This references [[My Note]] in context.')).toBeInTheDocument()
  })

  it('shows emoji icon before backlink title when entry has an emoji', () => {
    const backlinks = [{
      entry: makeEntry({ title: 'Starred Note', icon: '⭐' }),
      context: null,
    }]
    render(<BacklinksPanel backlinks={backlinks} onNavigate={onNavigate} />)
    expect(screen.getByText('⭐')).toBeInTheDocument()
    expect(screen.getByText('Starred Note')).toBeInTheDocument()
  })

  it('does not show emoji when backlink entry has no icon', () => {
    const backlinks = [{ entry: makeEntry({ title: 'Plain Note' }), context: null }]
    render(<BacklinksPanel backlinks={backlinks} onNavigate={onNavigate} />)
    expect(screen.getByText('Plain Note')).toBeInTheDocument()
    const btn = screen.getByText('Plain Note').closest('button')
    const spans = btn?.querySelectorAll('span.shrink-0')
    const emojiSpans = Array.from(spans ?? []).filter(s => /[\p{Emoji_Presentation}]/u.test(s.textContent ?? ''))
    expect(emojiSpans).toHaveLength(0)
  })
})

describe('ReferencedByPanel', () => {
  const onNavigate = vi.fn()

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

  it('renders nothing when empty', () => {
    const { container } = render(<ReferencedByPanel typeEntryMap={{}} items={[]} onNavigate={onNavigate} />)
    expect(container.innerHTML).toBe('')
  })

  it('renders referenced-by entries grouped by relationship key', () => {
    const items: ReferencedByItem[] = [
      { entry: makeEntry({ path: '/vault/proc/a.md', title: 'Write Essays', isA: 'Procedure' }), viaKey: 'Belongs to' },
      { entry: makeEntry({ path: '/vault/essay/b.md', title: 'On Writing Well', isA: 'Essay' }), viaKey: 'Belongs to' },
      { entry: makeEntry({ path: '/vault/exp/c.md', title: 'SEO Experiment', isA: 'Experiment' }), viaKey: 'Related to' },
    ]
    render(<ReferencedByPanel typeEntryMap={{}} items={items} onNavigate={onNavigate} />)

    expect(screen.getByTestId('derived-relationships-separator')).toBeInTheDocument()
    expect(screen.getByText('Write Essays')).toBeInTheDocument()
    expect(screen.getByText('On Writing Well')).toBeInTheDocument()
    expect(screen.getByText('SEO Experiment')).toBeInTheDocument()
    expect(screen.getByText('Children')).toBeInTheDocument()
    expect(screen.getByText('Referenced by')).toBeInTheDocument()
    expect(screen.queryByText('Derived relationships')).not.toBeInTheDocument()
    expect(screen.queryByText('Read-only groups sourced from other notes.')).not.toBeInTheDocument()
    expect(screen.queryByText('Events')).not.toBeInTheDocument()
  })

  it('navigates when clicking a referenced-by entry', () => {
    const items: ReferencedByItem[] = [
      { entry: makeEntry({ path: '/vault/a.md', title: 'My Note' }), viaKey: 'Belongs to' },
    ]
    render(<ReferencedByPanel typeEntryMap={{}} items={items} onNavigate={onNavigate} />)
    fireEvent.click(screen.getByText('My Note'))
    expect(onNavigate).toHaveBeenCalledWith('My Note')
  })

  it('dims archived entries in referenced-by', () => {
    const items: ReferencedByItem[] = [
      { entry: makeEntry({ path: '/vault/a.md', title: 'Old Note', archived: true }), viaKey: 'Has' },
    ]
    render(<ReferencedByPanel typeEntryMap={{}} items={items} onNavigate={onNavigate} />)
    expect(screen.getByTitle('Archived')).toBeInTheDocument()
  })

  it('adds an inverse-relationship tooltip to each derived relationship label', async () => {
    const items: ReferencedByItem[] = [
      { entry: makeEntry({ path: '/vault/a.md', title: 'Child Note' }), viaKey: 'Belongs to' },
    ]

    render(<ReferencedByPanel typeEntryMap={{}} items={items} onNavigate={onNavigate} />)

    fireEvent.focus(screen.getByText('Children'))
    expect(await screen.findByRole('tooltip')).toHaveTextContent('Derived inverse relationship, inverse of Belongs to.')
  })

})

describe('GitHistoryPanel', () => {
  const onViewCommitDiff = vi.fn()

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

  it('renders nothing when empty', () => {
    const { container } = render(<GitHistoryPanel commits={[]} />)
    expect(container.innerHTML).toBe('')
  })

  it('renders commit entries with hash and message', () => {
    const commits: GitCommit[] = [
      { hash: 'abc1234567890', shortHash: 'abc1234', message: 'Initial commit', author: 'luca', date: Math.floor(Date.now() / 1000) - 3600 },
      { hash: 'def4567890123', shortHash: 'def4567', message: 'Fix bug', author: 'jane', date: Math.floor(Date.now() / 1000) - 86400 * 2 },
    ]
    render(<GitHistoryPanel commits={commits} onViewCommitDiff={onViewCommitDiff} />)
    expect(screen.getByText('abc1234')).toBeInTheDocument()
    expect(screen.getByText('def4567')).toBeInTheDocument()
  })

  it('calls onViewCommitDiff when clicking commit entry', () => {
    const commits: GitCommit[] = [
      { hash: 'abc1234567890', shortHash: 'abc1234', message: 'test', author: '', date: Math.floor(Date.now() / 1000) },
    ]
    render(<GitHistoryPanel commits={commits} onViewCommitDiff={onViewCommitDiff} />)
    fireEvent.click(screen.getByText('abc1234'))
    expect(onViewCommitDiff).toHaveBeenCalledWith('abc1234567890')
  })

  it('displays relative dates correctly', () => {
    const now = Math.floor(Date.now() / 1000)
    const commits: GitCommit[] = [
      { hash: 'a', shortHash: 'a1', message: 'm1', author: '', date: now }, // today
      { hash: 'b', shortHash: 'b1', message: 'm2', author: '', date: now - 86400 }, // yesterday
      { hash: 'c', shortHash: 'c1', message: 'm3', author: '', date: now - 86400 * 10 }, // 10d ago
      { hash: 'd', shortHash: 'd1', message: 'm4', author: '', date: now - 86400 * 45 }, // ~1.5mo ago
    ]
    render(<GitHistoryPanel commits={commits} />)
    expect(screen.getByText('today')).toBeInTheDocument()
    expect(screen.getByText('yesterday')).toBeInTheDocument()
    expect(screen.getByText('10d ago')).toBeInTheDocument()
  })
})

describe('InstancesPanel', () => {
  const onNavigate = vi.fn()
  const quarterType = makeEntry({
    path: '/vault/quarter.md', filename: 'quarter.md', title: 'Quarter',
    isA: 'Type', color: 'blue', icon: 'calendar',
  })
  const typeEntryMap: Record<string, VaultEntry> = { Quarter: quarterType }

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

  it('renders nothing when entry is not a Type', () => {
    const entry = makeEntry({ title: 'Random Note', isA: 'Note' })
    const { container } = render(
      <InstancesPanel entry={entry} entries={[]} typeEntryMap={{}} onNavigate={onNavigate} />
    )
    expect(container.innerHTML).toBe('')
  })

  it('renders nothing when Type has zero instances', () => {
    const { container } = render(
      <InstancesPanel entry={quarterType} entries={[]} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
    )
    expect(container.innerHTML).toBe('')
  })

  it('renders instances of a Type sorted by modifiedAt descending', () => {
    const instances = [
      makeEntry({ path: '/vault/quarter/q1.md', title: 'Q1 2026', isA: 'Quarter', modifiedAt: 1000 }),
      makeEntry({ path: '/vault/quarter/q2.md', title: 'Q2 2026', isA: 'Quarter', modifiedAt: 3000 }),
      makeEntry({ path: '/vault/quarter/q3.md', title: 'Q3 2026', isA: 'Quarter', modifiedAt: 2000 }),
    ]
    render(
      <InstancesPanel entry={quarterType} entries={instances} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
    )
    expect(screen.getByText('Instances (3)')).toBeInTheDocument()
    const buttons = screen.getAllByRole('button').filter(b => ['Q1 2026', 'Q2 2026', 'Q3 2026'].includes(b.textContent?.replace(/\s*\(.*\)/, '') ?? ''))
    // Q2 (3000) should come before Q3 (2000) before Q1 (1000)
    expect(buttons[0].textContent).toContain('Q2 2026')
    expect(buttons[1].textContent).toContain('Q3 2026')
    expect(buttons[2].textContent).toContain('Q1 2026')
  })

  it('dims archived instances', () => {
    const instances = [
      makeEntry({ path: '/vault/quarter/old.md', title: 'Q4 2024', isA: 'Quarter', archived: true, modifiedAt: 1000 }),
    ]
    render(
      <InstancesPanel entry={quarterType} entries={instances} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
    )
    expect(screen.getByTitle('Archived')).toBeInTheDocument()
  })

  it('navigates when clicking an instance', () => {
    const instances = [
      makeEntry({ path: '/vault/quarter/q1.md', title: 'Q1 2026', isA: 'Quarter', modifiedAt: 1000 }),
    ]
    render(
      <InstancesPanel entry={quarterType} entries={instances} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
    )
    fireEvent.click(screen.getByText('Q1 2026'))
    expect(onNavigate).toHaveBeenCalledWith('Q1 2026')
  })

  it('caps display at 50 instances and shows count badge', () => {
    const instances = Array.from({ length: 80 }, (_, i) =>
      makeEntry({
        path: `/vault/quarter/q${i}.md`,
        title: `Instance ${i}`,
        isA: 'Quarter',
        modifiedAt: 80 - i,
      })
    )
    render(
      <InstancesPanel entry={quarterType} entries={instances} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
    )
    expect(screen.getByText('Instances (80)')).toBeInTheDocument()
    // Only 50 link buttons rendered
    const allButtons = screen.getAllByRole('button')
    const instanceButtons = allButtons.filter(b => b.textContent?.startsWith('Instance'))
    expect(instanceButtons.length).toBe(50)
    expect(screen.getByText('showing 50 of 80')).toBeInTheDocument()
  })

  it('does not show Instances section for non-Type note even if title matches a type name', () => {
    const notAType = makeEntry({ title: 'Quarter', isA: 'Project' })
    const instances = [
      makeEntry({ path: '/vault/quarter/q1.md', title: 'Q1 2026', isA: 'Quarter', modifiedAt: 1000 }),
    ]
    const { container } = render(
      <InstancesPanel entry={notAType} entries={instances} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
    )
    expect(container.innerHTML).toBe('')
  })
})
