import { test, expect, type Locator, type Page } from '@playwright/test'
import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from '../helpers/fixtureVault'
import { executeCommand, openCommandPalette } from './helpers'

const MIXED_RICH_TEXT_CONTENT = `---
title: Note B
type: Note
status: Active
---

# Note B

> 인용문

## 섹션

- 첫 번째 항목
- 두 번째 항목

Plain paragraph after the list.
`

const NUMBERED_REENTRY_CONTENT = `---
title: Note B
type: Note
status: Active
---

# Note B

Intro paragraph before the list.

1. First numbered item
2. Second numbered item

Plain paragraph after the numbered list.
`

const PROCEDURE_NOTE_CONTENT = `---
title: Procedure
type: Procedure
status: Active
---

# Procedure

Procedures are long-running processes tied to a [[responsibility|Responsibility]].
- Status: Active
- Owner: The person responsible

Plain follow-up paragraph after the procedure checklist.
`

const NESTED_PROCEDURE_NOTE_CONTENT = `---
title: Note B
type: Procedure
status: Active
---

# Note B

Procedures are long-running processes tied to a responsibility.

- Prepare release
  1. Validate nested numbered content
  2. Notify the owner
- Record outcome

Plain follow-up paragraph after the nested procedure checklist.
`

let tempVaultDir: string

test.beforeEach(async ({ page }, testInfo) => {
  testInfo.setTimeout(90_000)
  tempVaultDir = createFixtureVaultCopy()
  await openFixtureVault(page, tempVaultDir)
})

test.afterEach(async () => {
  removeFixtureVaultCopy(tempVaultDir)
})

async function openNote(page: Page, title: string) {
  await page.locator('[data-testid="note-list-container"]').getByText(title, { exact: true }).click()
  await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
}

async function toggleRawMode(page: Page, visibleSelector: string) {
  await openCommandPalette(page)
  await executeCommand(page, 'Toggle Raw')
  await expect(page.locator(visibleSelector)).toBeVisible({ timeout: 5_000 })
}

async function getRawEditorContent(page: Page): Promise<string> {
  return page.evaluate(() => {
    type CodeMirrorHost = Element & {
      cmTile?: {
        view?: {
          state: {
            doc: {
              toString(): string
            }
          }
        }
      }
    }

    const el = document.querySelector('.cm-content')
    const view = (el as CodeMirrorHost | null)?.cmTile?.view
    return view?.state.doc.toString() ?? el?.textContent ?? ''
  })
}

async function setRawEditorContent(page: Page, content: string) {
  await page.evaluate((nextContent) => {
    type CodeMirrorHost = Element & {
      cmTile?: {
        view?: {
          state: {
            doc: {
              length: number
            }
          }
          dispatch(update: { changes: { from: number; to: number; insert: string } }): void
        }
      }
    }

    const el = document.querySelector('.cm-content')
    const view = (el as CodeMirrorHost | null)?.cmTile?.view
    if (!view) throw new Error('CodeMirror view not found')
    view.dispatch({
      changes: { from: 0, to: view.state.doc.length, insert: nextContent },
    })
  }, content)
}

function collectEditorCrashSignals(page: Page) {
  const messages: string[] = []

  page.on('pageerror', (error) => {
    messages.push(error.message)
  })

  page.on('console', (message) => {
    if (message.type() === 'error') {
      messages.push(message.text())
    }
  })

  return messages
}

function expectNoBlockContainerCrash(messages: string[]) {
  expect(messages.filter((message) => (
    message.includes('Invalid content for node blockContainer') ||
    message.includes('RangeError')
  ))).toEqual([])
}

async function loadRawContentInRichEditor(page: Page, content: string) {
  await openNote(page, 'Note B')
  await toggleRawMode(page, '.cm-content')
  await setRawEditorContent(page, content)
  await page.waitForTimeout(700)
  await toggleRawMode(page, '.bn-editor')
}

async function appendMarkerToBlock(page: Page, block: Locator, marker: string) {
  await expect(block).toBeVisible({ timeout: 5_000 })
  await block.click()
  await page.keyboard.press('End')
  await page.keyboard.insertText(` ${marker}`)
  await page.waitForTimeout(900)
}

async function reopenNoteB(page: Page) {
  await openNote(page, 'Note C')
  await openNote(page, 'Note B')
}

async function expectListContentEditableAfterReopen(page: Page, options: {
  content: string
  markerPrefix: string
  primary: {
    selector: string
    text: string
  }
  secondary: {
    selector: string
    text: string
  }
}) {
  const crashSignals = collectEditorCrashSignals(page)
  const markerId = Date.now()
  const primaryMarker = `${options.markerPrefix}-primary-${markerId}`
  const secondaryMarker = `${options.markerPrefix}-secondary-${markerId}`
  const reentryMarker = `${options.markerPrefix}-reentry-${markerId}`

  await loadRawContentInRichEditor(page, options.content)

  const primaryBlock = page.locator(options.primary.selector, {
    hasText: options.primary.text,
  }).first()
  await appendMarkerToBlock(page, primaryBlock, primaryMarker)

  const secondaryBlock = page.locator(options.secondary.selector, {
    hasText: options.secondary.text,
  }).first()
  await appendMarkerToBlock(page, secondaryBlock, secondaryMarker)

  await reopenNoteB(page)
  await appendMarkerToBlock(page, primaryBlock, reentryMarker)

  expectNoBlockContainerCrash(crashSignals)

  await toggleRawMode(page, '.cm-content')
  const raw = await getRawEditorContent(page)
  expect(raw).toContain(primaryMarker)
  expect(raw).toContain(secondaryMarker)
  expect(raw).toContain(reentryMarker)
  expectNoBlockContainerCrash(crashSignals)
}

const listReopenScenarios = [
  {
    name: 'numbered list content stays editable after autosave and re-entry',
    content: NUMBERED_REENTRY_CONTENT,
    markerPrefix: 'numbered',
    primary: {
      selector: '.bn-block-content[data-content-type="numberedListItem"]',
      text: 'First numbered item',
    },
    secondary: {
      selector: '.bn-block-content',
      text: 'Plain paragraph after the numbered list.',
    },
  },
  {
    name: 'nested procedure list content stays editable after save and reopen',
    content: NESTED_PROCEDURE_NOTE_CONTENT,
    markerPrefix: 'nested',
    primary: {
      selector: '.bn-block-content[data-content-type="numberedListItem"]',
      text: 'Validate nested numbered content',
    },
    secondary: {
      selector: '.bn-block-content[data-content-type="bulletListItem"]',
      text: 'Record outcome',
    },
  },
] as const

test('mixed rich-text blocks with Korean list content stay editable after action clicks', async ({ page }) => {
  const crashSignals = collectEditorCrashSignals(page)

  await openNote(page, 'Note B')
  await toggleRawMode(page, '.cm-content')
  await setRawEditorContent(page, MIXED_RICH_TEXT_CONTENT)
  await page.waitForTimeout(700)
  await toggleRawMode(page, '.bn-editor')

  const bulletBlock = page.locator('.bn-block-content', { hasText: '첫 번째 항목' }).first()
  await expect(bulletBlock).toBeVisible({ timeout: 5_000 })

  await page.locator('.bn-block-content', { hasText: '인용문' }).first().click()
  await page.keyboard.press('End')
  await page.keyboard.insertText(' 추가')
  await bulletBlock.hover()
  await expect(page.locator('.bn-side-menu').first()).toBeVisible({ timeout: 5_000 })
  await page.locator('.bn-side-menu').first().click({ force: true })
  await page.keyboard.press('Escape')
  await bulletBlock.click()
  await page.keyboard.press('End')
  await page.keyboard.insertText(' 계속')
  await page.waitForTimeout(700)

  expectNoBlockContainerCrash(crashSignals)

  await toggleRawMode(page, '.cm-content')
  const raw = await getRawEditorContent(page)
  expect(raw).toContain('> 인용문 추가')
  expect(raw).toContain('- 첫 번째 항목 계속')
  expectNoBlockContainerCrash(crashSignals)
})

for (const scenario of listReopenScenarios) {
  test(scenario.name, async ({ page }) => {
    await expectListContentEditableAfterReopen(page, scenario)
  })
}

test('Procedure prose and adjacent bullet-list content stay editable through keydown edits', async ({ page }) => {
  const crashSignals = collectEditorCrashSignals(page)
  const markerId = Date.now()
  const paragraphMarker = `procedure-paragraph-${markerId}`
  const ownerMarker = `procedure-owner-${markerId}`
  const cadenceMarker = `procedure-cadence-${markerId}`

  await openNote(page, 'Note B')
  await toggleRawMode(page, '.cm-content')
  await setRawEditorContent(page, PROCEDURE_NOTE_CONTENT)
  await page.waitForTimeout(700)
  await toggleRawMode(page, '.bn-editor')

  const procedureParagraph = page.locator('.bn-block-content', {
    hasText: 'Procedures are long-running processes',
  }).first()
  await expect(procedureParagraph).toBeVisible({ timeout: 5_000 })
  await procedureParagraph.click()
  await page.keyboard.press('End')
  await page.keyboard.insertText(` ${paragraphMarker}`)
  await page.waitForTimeout(700)

  const ownerItem = page.locator('.bn-block-content[data-content-type="bulletListItem"]', {
    hasText: 'Owner: The person responsible',
  }).first()
  await expect(ownerItem).toBeVisible({ timeout: 5_000 })
  await ownerItem.click()
  await page.keyboard.press('End')
  await page.keyboard.insertText(` ${ownerMarker}`)
  await page.keyboard.press('Enter')
  await page.keyboard.insertText(`Cadence: Weekly ${cadenceMarker}`)
  await page.waitForTimeout(900)

  expectNoBlockContainerCrash(crashSignals)

  await toggleRawMode(page, '.cm-content')
  const raw = await getRawEditorContent(page)
  expect(raw).toContain(paragraphMarker)
  expect(raw).toContain(`- Owner: The person responsible ${ownerMarker}`)
  expect(raw).toContain(`- Cadence: Weekly ${cadenceMarker}`)
  expectNoBlockContainerCrash(crashSignals)
})
