import { describe, it, expect, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useEditorTabSwap } from './useEditorTabSwap'

function makeTab(path: string, title: string, body: string) {
  return {
    entry: { path, title, filename: `${title}.md`, type: 'Note', status: 'Active', aliases: [], isA: '' } as never,
    content: `---\ntitle: ${title}\n---\n\n# ${title}\n\n${body}`,
  }
}

function makeContentTab(path: string, content: string) {
  return {
    entry: { path, title: path.replace(/\.md$/, ''), filename: path, type: 'Note', status: 'Active', aliases: [], isA: '' } as never,
    content,
  }
}

function makeMockEditor(currentMarkdown: string) {
  const markdownRef = { current: currentMarkdown }
  const docRef = {
    current: [
      {
        type: 'heading',
        props: { level: 1 },
        content: [{ type: 'text', text: 'Fresh Title', styles: {} }],
      },
      {
        type: 'paragraph',
        content: [{ type: 'text', text: 'Body typed live', styles: {} }],
      },
    ] as unknown[],
  }

  const editor = {
    get document() { return docRef.current },
    get prosemirrorView() { return {} },
    onMount: (cb: () => void) => { cb(); return () => {} },
    replaceBlocks: vi.fn(),
    insertBlocks: vi.fn(),
    blocksToMarkdownLossy: vi.fn(() => markdownRef.current),
    blocksToHTMLLossy: vi.fn(() => ''),
    tryParseMarkdownToBlocks: vi.fn(() => []),
    _tiptapEditor: { commands: { setContent: vi.fn() } },
    setMarkdown: (markdown: string) => {
      markdownRef.current = markdown
    },
  }

  return editor
}

function setupMountedEditorMocks() {
  vi.spyOn(document, 'querySelector').mockReturnValue({ scrollTop: 0 } as unknown as Element)
  vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { cb(0); return 0 })
}

function renderRenameHarness(options?: { onContentChange?: ReturnType<typeof vi.fn> }) {
  const editor = makeMockEditor('# Fresh Title\n\nBody typed live')
  const onContentChange = options?.onContentChange ?? vi.fn()
  const untitledTab = makeTab('untitled-note-123.md', 'Untitled Note 123', 'Body')
  const renamedTab = makeTab('fresh-title.md', 'Fresh Title', 'Body')

  const hook = renderHook(
    ({ tabs, activeTabPath }) => useEditorTabSwap({
      tabs,
      activeTabPath,
      editor: editor as never,
      onContentChange,
    }),
    { initialProps: { tabs: [untitledTab], activeTabPath: untitledTab.entry.path } },
  )

  return {
    editor,
    onContentChange,
    untitledTab,
    renamedTab,
    ...hook,
  }
}

async function settleRenameHarness(editor: ReturnType<typeof makeMockEditor>) {
  await act(() => new Promise(r => setTimeout(r, 0)))
  editor.replaceBlocks.mockClear()
  editor.tryParseMarkdownToBlocks.mockClear()
}

async function expectRenameSessionContinues(options: { renamedTabArrivesLate: boolean }) {
  const {
    editor,
    onContentChange,
    renamedTab,
    result,
    rerender,
    untitledTab,
  } = renderRenameHarness()

  await settleRenameHarness(editor)

  if (options.renamedTabArrivesLate) {
    rerender({ tabs: [untitledTab], activeTabPath: renamedTab.entry.path })
    await act(() => new Promise(r => setTimeout(r, 0)))
  }

  rerender({ tabs: [renamedTab], activeTabPath: renamedTab.entry.path })
  await act(() => new Promise(r => setTimeout(r, 0)))

  expect(editor.replaceBlocks).not.toHaveBeenCalled()
  expect(editor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()

  act(() => {
    result.current.handleEditorChange()
  })
  act(() => {
    result.current.flushPendingEditorChange()
  })

  expect(onContentChange).toHaveBeenCalledWith(
    'fresh-title.md',
    expect.stringContaining('Body typed live'),
  )
}

describe('useEditorTabSwap untitled rename continuity', () => {
  it('keeps the live editor session when an untitled note auto-renames', async () => {
    setupMountedEditorMocks()
    await expectRenameSessionContinues({ renamedTabArrivesLate: false })
  })

  it('still swaps when the next note does not match the live untitled body', async () => {
    setupMountedEditorMocks()

    const editor = makeMockEditor('# Fresh Title\n\nBody typed live')
    const untitledTab = makeTab('untitled-note-123.md', 'Untitled Note 123', 'Body')
    const otherTab = makeTab('fresh-title.md', 'Fresh Title', 'Different body')

    const { rerender } = renderHook(
      ({ tabs, activeTabPath }) => useEditorTabSwap({
        tabs,
        activeTabPath,
        editor: editor as never,
      }),
      { initialProps: { tabs: [untitledTab], activeTabPath: untitledTab.entry.path } },
    )

    await act(() => new Promise(r => setTimeout(r, 0)))
    editor.replaceBlocks.mockClear()
    editor.tryParseMarkdownToBlocks.mockClear()

    rerender({ tabs: [otherTab], activeTabPath: otherTab.entry.path })
    await act(() => new Promise(r => setTimeout(r, 0)))

    expect(editor.tryParseMarkdownToBlocks).toHaveBeenCalled()
  })

  it('keeps the live editor session when the renamed tab arrives one render after the path switch', async () => {
    setupMountedEditorMocks()
    await expectRenameSessionContinues({ renamedTabArrivesLate: true })
  })

  it('does not re-swap the active note when app state catches up with live typing', async () => {
    setupMountedEditorMocks()

    const editor = makeMockEditor('# Fresh Title\n\nBody typed live')
    const onContentChange = vi.fn()
    const tab = makeTab('fresh-title.md', 'Fresh Title', 'Body')

    const { result, rerender } = renderHook(
      ({ tabs, activeTabPath }) => useEditorTabSwap({
        tabs,
        activeTabPath,
        editor: editor as never,
        onContentChange,
      }),
      { initialProps: { tabs: [tab], activeTabPath: tab.entry.path } },
    )

    await act(() => new Promise(r => setTimeout(r, 0)))

    act(() => {
      result.current.handleEditorChange()
    })
    act(() => {
      result.current.flushPendingEditorChange()
    })

    const syncedContent = onContentChange.mock.calls.at(-1)?.[1]
    expect(typeof syncedContent).toBe('string')

    editor.replaceBlocks.mockClear()
    editor.tryParseMarkdownToBlocks.mockClear()

    rerender({
      tabs: [{ ...tab, content: syncedContent }],
      activeTabPath: tab.entry.path,
    })
    await act(() => new Promise(r => setTimeout(r, 0)))

    expect(editor.replaceBlocks).not.toHaveBeenCalled()
    expect(editor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()
  })

  it('does not re-swap while local wikilink insertion is ahead of the latest tab props', async () => {
    setupMountedEditorMocks()

    const editor = makeMockEditor('# Fresh Title\n\nBody')
    const onContentChange = vi.fn()
    const tab = makeTab('fresh-title.md', 'Fresh Title', 'Body')

    const { result, rerender } = renderHook(
      ({ tabs, activeTabPath }) => useEditorTabSwap({
        tabs,
        activeTabPath,
        editor: editor as never,
        onContentChange,
      }),
      { initialProps: { tabs: [tab], activeTabPath: tab.entry.path } },
    )

    await act(() => new Promise(r => setTimeout(r, 0)))

    editor.setMarkdown('# Fresh Title\n\nBody\n\n[[Mana')
    act(() => {
      result.current.handleEditorChange()
    })
    act(() => {
      result.current.flushPendingEditorChange()
    })
    const queryContent = onContentChange.mock.calls.at(-1)?.[1]
    expect(typeof queryContent).toBe('string')

    editor.setMarkdown('# Fresh Title\n\nBody\n\n[[manage-sponsorships]] ')
    act(() => {
      result.current.handleEditorChange()
    })
    act(() => {
      result.current.flushPendingEditorChange()
    })
    const insertedContent = onContentChange.mock.calls.at(-1)?.[1]
    expect(typeof insertedContent).toBe('string')

    editor.replaceBlocks.mockClear()
    editor.tryParseMarkdownToBlocks.mockClear()

    rerender({
      tabs: [{ ...tab, content: queryContent }],
      activeTabPath: tab.entry.path,
    })
    await act(() => new Promise(r => setTimeout(r, 0)))

    expect(editor.replaceBlocks).not.toHaveBeenCalled()
    expect(editor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()

    rerender({
      tabs: [{ ...tab, content: insertedContent }],
      activeTabPath: tab.entry.path,
    })
    await act(() => new Promise(r => setTimeout(r, 0)))

    expect(editor.replaceBlocks).not.toHaveBeenCalled()
    expect(editor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()
  })

  it('does not re-swap equivalent empty-heading content after an untouched new note settles', async () => {
    setupMountedEditorMocks()

    const editor = makeMockEditor('#\n')
    const initialTab = makeContentTab('untitled-note-123.md', '---\ntype: Note\n---\n\n# \n\n')
    const settledTab = makeContentTab('untitled-note-123.md', '---\ntype: Note\n---\n\n#\n')

    const { rerender } = renderHook(
      ({ tabs, activeTabPath }) => useEditorTabSwap({
        tabs,
        activeTabPath,
        editor: editor as never,
      }),
      { initialProps: { tabs: [initialTab], activeTabPath: initialTab.entry.path } },
    )

    await act(() => new Promise(r => setTimeout(r, 0)))
    editor.replaceBlocks.mockClear()
    editor.tryParseMarkdownToBlocks.mockClear()

    rerender({ tabs: [settledTab], activeTabPath: settledTab.entry.path })
    await act(() => new Promise(r => setTimeout(r, 0)))

    expect(editor.replaceBlocks).not.toHaveBeenCalled()
    expect(editor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()
  })

  it('flushes pending first typing instead of re-swapping stale tab content', async () => {
    setupMountedEditorMocks()

    const editor = makeMockEditor('# First keystroke')
    const onContentChange = vi.fn()
    const tab = makeContentTab('untitled-note-123.md', '---\ntype: Note\n---\n\n# \n\n')

    const { result, rerender } = renderHook(
      ({ tabs, activeTabPath }) => useEditorTabSwap({
        tabs,
        activeTabPath,
        editor: editor as never,
        onContentChange,
      }),
      { initialProps: { tabs: [tab], activeTabPath: tab.entry.path } },
    )

    await act(() => new Promise(r => setTimeout(r, 0)))
    editor.replaceBlocks.mockClear()
    editor.tryParseMarkdownToBlocks.mockClear()

    act(() => {
      result.current.handleEditorChange()
    })
    rerender({ tabs: [tab], activeTabPath: tab.entry.path })
    await act(() => new Promise(r => setTimeout(r, 0)))

    expect(onContentChange).toHaveBeenCalledWith('untitled-note-123.md', expect.stringContaining('First keystroke'))
    expect(editor.replaceBlocks).not.toHaveBeenCalled()
    expect(editor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()
  })
})
