import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import type { VaultEntry } from '../types'
import { TableOfContentsPanel } from './TableOfContentsPanel'
import { buildTableOfContents, buildTableOfContentsFromMarkdown } from './tableOfContentsModel'

const entry = {
  title: 'The Compounding Software Factory',
  modifiedAt: 1700000000,
  createdAt: 1700000000,
  fileSize: 2048,
} as VaultEntry

const blocks = [
  { id: 'h1', type: 'heading', props: { level: 1 }, content: [{ type: 'text', text: 'The default path is degradation' }] },
  { id: 'h2', type: 'heading', props: { level: 2 }, content: [{ type: 'text', text: 'What causes teams to degrade' }] },
  { id: 'h3', type: 'heading', props: { level: 3 }, content: [{ type: 'text', text: 'Poor coding hygiene' }] },
  { id: 'ignored', type: 'paragraph', props: {}, content: [{ type: 'text', text: 'Body' }] },
]

describe('TableOfContentsPanel', () => {
  it('builds a title-rooted H1/H2/H3 hierarchy', () => {
    const toc = buildTableOfContents(entry.title, blocks)

    expect(toc.title).toBe('The Compounding Software Factory')
    expect(toc.children[0].title).toBe('The default path is degradation')
    expect(toc.children[0].children[0].title).toBe('What causes teams to degrade')
    expect(toc.children[0].children[0].children[0].title).toBe('Poor coding hygiene')
  })

  it('does not duplicate the note title when the first markdown H1 matches it', () => {
    const toc = buildTableOfContentsFromMarkdown(
      'Introducing Tolaria',
      '# Introducing Tolaria\n\n## Tolaria + Refactoring\n\n## Principles',
    )

    expect(toc.title).toBe('Introducing Tolaria')
    expect(toc.children.map((item) => item.title)).toEqual(['Tolaria + Refactoring', 'Principles'])
  })

  it('ignores markdown headings inside fenced and inline code areas', () => {
    const toc = buildTableOfContentsFromMarkdown(
      'Markdown Outline',
      [
        '# Markdown Outline',
        '',
        '## Real Setup',
        '',
        '```ts',
        '# Fenced code is not a heading',
        '## Nested fenced code is not a heading',
        '```',
        '',
        'This paragraph mentions `# Inline code is not a heading`.',
        '',
        '## Real Followup',
      ].join('\n'),
    )

    expect(toc.children.map((item) => item.title)).toEqual(['Real Setup', 'Real Followup'])
    expect(toc.children.flatMap((item) => item.children)).toEqual([])
  })

  it('keeps navigation ids after removing a duplicate markdown title H1', async () => {
    const setTextCursorPosition = vi.fn()
    render(
      <TableOfContentsPanel
        editor={{
          document: [
            { id: 'title-block', type: 'heading', props: { level: 1 }, content: [{ type: 'text', text: 'Introducing Tolaria' }] },
            { id: 'section-block', type: 'heading', props: { level: 2 }, content: [{ type: 'text', text: 'Tolaria + Refactoring' }] },
          ],
          setTextCursorPosition,
        }}
        entry={{ ...entry, title: 'Introducing Tolaria' } as VaultEntry}
        sourceContent={'# Introducing Tolaria\n\n## Tolaria + Refactoring'}
        onClose={vi.fn()}
      />,
    )

    fireEvent.click(await screen.findByRole('button', { name: /Introducing Tolaria/ }))
    expect(setTextCursorPosition).toHaveBeenCalledWith('title-block', 'start')

    fireEvent.click(await screen.findByRole('button', { name: /Tolaria \+ Refactoring/ }))
    expect(setTextCursorPosition).toHaveBeenCalledWith('section-block', 'start')
  })

  it('resolves navigation ids on click after the async TOC build starts without ids', async () => {
    const setTextCursorPosition = vi.fn()
    const editor = {
      document: [] as unknown[],
      setTextCursorPosition,
    }
    render(
      <TableOfContentsPanel
        editor={editor}
        entry={{ ...entry, title: 'New Note' } as VaultEntry}
        sourceContent={'# New Note\n\n## New Heading'}
        onClose={vi.fn()}
      />,
    )

    await screen.findByRole('button', { name: /New Heading/ })
    editor.document = [
      { id: 'title-block', type: 'heading', props: { level: 1 }, content: [{ type: 'text', text: 'New Note' }] },
      { id: 'new-heading-block', type: 'heading', props: { level: 2 }, content: [{ type: 'text', text: 'New Heading' }] },
    ]

    fireEvent.click(screen.getByRole('button', { name: /New Heading/ }))
    expect(setTextCursorPosition).toHaveBeenCalledWith('new-heading-block', 'start')
  })

  it('updates from source content even when the editor document is stale', async () => {
    const { rerender } = render(
      <TableOfContentsPanel
        editor={{ document: blocks, setTextCursorPosition: vi.fn() }}
        entry={{ ...entry, title: 'Old Note' } as VaultEntry}
        sourceContent={'# Old Note\n\n## Old Heading'}
        onClose={vi.fn()}
      />,
    )

    await screen.findByRole('button', { name: /Old Heading/ })
    rerender(
      <TableOfContentsPanel
        editor={{ document: blocks, setTextCursorPosition: vi.fn() }}
        entry={{ ...entry, title: 'New Note' } as VaultEntry}
        sourceContent={'# New Note\n\n## New Heading'}
        onClose={vi.fn()}
      />,
    )

    expect(screen.getByRole('button', { name: /New Note/ })).toBeInTheDocument()
    expect(screen.queryByRole('button', { name: /Old Heading/ })).not.toBeInTheDocument()
    expect(await screen.findByRole('button', { name: /New Heading/ })).toBeInTheDocument()
  })

  it('does not show stale editor headings while new note source content is loading', () => {
    render(
      <TableOfContentsPanel
        editor={{ document: blocks, setTextCursorPosition: vi.fn() }}
        entry={{ ...entry, title: 'New Note' } as VaultEntry}
        sourceContent={null}
        onClose={vi.fn()}
      />,
    )

    expect(screen.getByRole('button', { name: /New Note/ })).toBeInTheDocument()
    expect(screen.queryByRole('button', { name: /The default path is degradation/ })).not.toBeInTheDocument()
  })

  it('renders heading icons, nesting guides, and navigates to clicked headings', async () => {
    const setTextCursorPosition = vi.fn()
    render(
      <TableOfContentsPanel
        editor={{ document: blocks, setTextCursorPosition }}
        entry={entry}
        onClose={vi.fn()}
      />,
    )

    expect(screen.getByText('Table of Contents')).toBeInTheDocument()
    expect(await screen.findByTestId('toc-connector:toc-title')).toBeInTheDocument()
    expect(screen.getByTestId('toc-connector:h1')).toBeInTheDocument()

    fireEvent.click(screen.getByRole('button', { name: /What causes teams to degrade/ }))
    expect(setTextCursorPosition).toHaveBeenCalledWith('h2', 'start')
  })

  it('shows note info at the bottom of the table of contents', () => {
    render(
      <TableOfContentsPanel
        editor={{ document: blocks, setTextCursorPosition: vi.fn() }}
        entry={entry}
        sourceContent="One two three"
        onClose={vi.fn()}
      />,
    )

    expect(screen.getByText('Info')).toBeInTheDocument()
    expect(screen.getByText('Words')).toBeInTheDocument()
    expect(screen.getByText('3')).toBeInTheDocument()
    expect(screen.getByText('2.0 KB')).toBeInTheDocument()
  })
})
