import { expect, test } from '@playwright/test';
import {
  collapseEditorSelection,
  collapseEditorSelectionAfterText,
  createBrowserWorkspaceAndNote,
  getEditorLocator,
  readStoredMarkdown,
  selectEditorText,
  waitForEditorFocus,
  writeStoredMarkdown,
} from './common';

test('tight and loose lists survive editing, task toggling, and reload', async ({
  page,
}) => {
  const workspaceName = 'list-markdown-fidelity';
  const noteName = 'lists';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });

  const source = [
    '# List fidelity',
    '',
    '- tight alpha',
    '- tight beta',
    '',
    'Loose list:',
    '',
    '- loose alpha',
    '',
    '- loose beta',
    '',
    '1. [ ] ordered task',
  ].join('\n');
  await writeStoredMarkdown(page, workspaceName, noteName, source);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await expect(editor.getByText('tight alpha', { exact: true })).toBeVisible();
  await expect(editor.getByText('loose beta', { exact: true })).toBeVisible();
  const checkbox = editor.getByRole('checkbox');
  await expect(checkbox).not.toBeChecked();

  await editor.getByText('tight alpha', { exact: true }).click();
  await waitForEditorFocus(page, {});
  await page.keyboard.press('End');
  await page.keyboard.insertText(' edited');

  await editor.getByText('loose beta', { exact: true }).click();
  await page.keyboard.press('End');
  await page.keyboard.insertText(' edited');
  await checkbox.click();
  await expect(checkbox).toBeChecked();

  const expected = source
    .replace('tight alpha', 'tight alpha edited')
    .replace('loose beta', 'loose beta edited')
    .replace('1. [ ] ordered task', '1. [x] ordered task');
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(
    editor.getByText('tight alpha edited', { exact: true }),
  ).toBeVisible();
  await expect(
    editor.getByText('loose beta edited', { exact: true }),
  ).toBeVisible();
  await expect(editor.getByRole('checkbox')).toBeChecked();
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);
});

test('copy and paste preserves a loose ordered task list', async ({
  context,
  page,
}) => {
  await context.grantPermissions(['clipboard-read', 'clipboard-write']);
  const workspaceName = 'list-clipboard-fidelity';
  const noteName = 'ordered-tasks';
  const source = '1. [x] copied task\n\n1. [ ] copied pending';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });
  await writeStoredMarkdown(page, workspaceName, noteName, source);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await editor.click();
  await page.keyboard.press('ControlOrMeta+a');
  await page.keyboard.press('ControlOrMeta+c');
  await page.keyboard.press('Backspace');
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe('');
  await page.keyboard.press('ControlOrMeta+v');

  await expect(editor.getByText('copied task', { exact: true })).toBeVisible();
  await expect(
    editor.getByText('copied pending', { exact: true }),
  ).toBeVisible();
  await expect(editor.getByRole('checkbox').first()).toBeChecked();
  await expect(editor.getByRole('checkbox').last()).not.toBeChecked();
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(source);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(editor.getByRole('checkbox').first()).toBeChecked();
  await expect(editor.getByRole('checkbox').last()).not.toBeChecked();
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    source,
  );
});

test('Enter at the start of a loose ordered task preserves the list on reload', async ({
  page,
}) => {
  const workspaceName = 'list-enter-fidelity';
  const noteName = 'ordered-task-split';
  const source = '1. [x] ordered task\n\n1. [ ] sibling task';
  const expected = '1. [ ] \n\n1. [x] ordered task\n\n1. [ ] sibling task';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });
  await writeStoredMarkdown(page, workspaceName, noteName, source);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await expect(editor.getByText('ordered task', { exact: true })).toBeVisible();
  await collapseEditorSelection(page, 0);
  await page.keyboard.press('Enter');

  await expect(editor.getByRole('checkbox')).toHaveCount(3);
  await expect(editor.getByRole('checkbox').nth(0)).not.toBeChecked();
  await expect(editor.getByRole('checkbox').nth(1)).toBeChecked();
  await expect(editor.getByRole('checkbox').nth(2)).not.toBeChecked();
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(editor.getByRole('checkbox')).toHaveCount(3);
  await expect(editor.getByRole('checkbox').nth(1)).toBeChecked();
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    expected,
  );
});

test('converting an ordered list to tasks keeps its container and looseness', async ({
  page,
}) => {
  const workspaceName = 'list-command-fidelity';
  const noteName = 'ordered-to-task';
  const source = '1. ordered one\n\n1. ordered two';
  const expected = '1. [ ] ordered one\n\n1. [ ] ordered two';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });
  await writeStoredMarkdown(page, workspaceName, noteName, source);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await editor.getByText('ordered one', { exact: true }).click();
  await waitForEditorFocus(page, {});
  await page.keyboard.press('ControlOrMeta+a');
  await page.keyboard.press('ControlOrMeta+Shift+7');

  await expect(editor.getByRole('checkbox')).toHaveCount(2);
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(editor.getByRole('checkbox')).toHaveCount(2);
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    expected,
  );

  await editor.click();
  await page.keyboard.press('ControlOrMeta+a');
  await page.keyboard.press('Backspace');
  await page.keyboard.type('1. typed ordered');
  await page.keyboard.press('ControlOrMeta+Shift+7');
  const inputRuleExpected = '1. [ ] typed ordered';
  await expect(editor.getByRole('checkbox')).toHaveCount(1);
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(inputRuleExpected);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(editor.getByRole('checkbox')).toHaveCount(1);
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    inputRuleExpected,
  );
});

test('typing a bullet marker inside an ordered item persists as a bullet', async ({
  page,
}) => {
  const workspaceName = 'list-input-rule-fidelity';
  const noteName = 'ordered-to-bullet';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });

  const editor = getEditorLocator(page, {});
  await editor.click();
  await waitForEditorFocus(page, {});
  await page.keyboard.type('1. temporary');
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe('1. temporary');

  await selectEditorText(page, 'temporary');
  await page.keyboard.press('Backspace');
  await page.keyboard.type('- converted');

  await expect(editor.getByText('converted', { exact: true })).toBeVisible();
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe('- converted');

  await page.reload({ waitUntil: 'networkidle' });
  await expect(editor.getByText('converted', { exact: true })).toBeVisible();
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    '- converted',
  );
});

test('list type changes follow the same-level run', async ({ page }) => {
  const workspaceName = 'same-level-list-formatting';
  const noteName = 'list-runs';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });

  const bulletRun = '- alpha\n- beta\n- gamma';
  await writeStoredMarkdown(page, workspaceName, noteName, bulletRun);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await collapseEditorSelectionAfterText(page, 'beta');
  await page.keyboard.insertText(' /');
  const slashMenu = page.getByTestId('slash-command-menu');
  await expect(slashMenu).toBeVisible();
  await slashMenu.getByText('Numbered list', { exact: true }).click();
  await page.keyboard.press('Backspace');

  const orderedRun = '1. alpha\n1. beta\n1. gamma';
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(orderedRun);
  await expect(
    editor.locator(
      '.prosemirror-flat-list[data-list-container-kind="ordered"]',
    ),
  ).toHaveCount(3);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    orderedRun,
  );

  const nestedRun = '- parent\n  1. nested\n- sibling';
  await writeStoredMarkdown(page, workspaceName, noteName, nestedRun);
  await page.reload({ waitUntil: 'networkidle' });
  await collapseEditorSelectionAfterText(page, 'nested');
  await page.keyboard.press('Shift+Tab');

  const dedentedRun = '- parent\n- nested\n- sibling';
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(dedentedRun);
  await expect(
    editor.locator('.prosemirror-flat-list[data-list-container-kind="bullet"]'),
  ).toHaveCount(3);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    dedentedRun,
  );
});

test('replacing task text with a thematic break preserves both structures', async ({
  page,
}) => {
  const workspaceName = 'task-thematic-break-fidelity';
  const noteName = 'task-thematic-break';
  const source = '- [ ] replace me\n- [ ] second';
  const expected = '- [ ] \n\n  ---\n\n- [ ] second';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });
  await writeStoredMarkdown(page, workspaceName, noteName, source);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await selectEditorText(page, 'replace me');
  await page.keyboard.press('Backspace');
  await page.keyboard.type('---');

  await expect(editor.getByRole('checkbox')).toHaveCount(2);
  await expect(editor.locator('hr')).toHaveCount(1);
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(editor.getByRole('checkbox')).toHaveCount(2);
  await expect(editor.locator('hr')).toHaveCount(1);
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    expected,
  );
});

test('literal paren list marker text stays text after save and reload', async ({
  page,
}) => {
  const workspaceName = 'literal-list-marker-fidelity';
  const noteName = 'literal-list-marker';
  const source = '- replace me\n- before break';
  const expected =
    '- 2\\) literal ordered\n- before break\\\n  \\- literal bullet';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });
  await writeStoredMarkdown(page, workspaceName, noteName, source);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await selectEditorText(page, 'replace me');
  await page.keyboard.insertText('2) literal ordered');

  await collapseEditorSelectionAfterText(page, 'before break');
  await page.keyboard.press('Shift+Enter');
  await page.keyboard.insertText('- literal bullet');

  await expect(
    editor.getByText('2) literal ordered', { exact: true }),
  ).toBeVisible();
  const hardBreakParagraph = editor.locator('p', {
    hasText: 'before break',
  });
  await expect(hardBreakParagraph.locator('br')).toHaveCount(1);
  await expect(hardBreakParagraph).toContainText('- literal bullet');
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(
    editor.getByText('2) literal ordered', { exact: true }),
  ).toBeVisible();
  const reloadedHardBreakParagraph = editor.locator('p', {
    hasText: 'before break',
  });
  await expect(reloadedHardBreakParagraph.locator('br')).toHaveCount(1);
  await expect(reloadedHardBreakParagraph).toContainText('- literal bullet');
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    expected,
  );
});

test('deeply indenting a task does not create another task on reload', async ({
  page,
}) => {
  const workspaceName = 'deep-task-indent-fidelity';
  const noteName = 'deep-task-indent';
  const source = '- parent\n  - [ ] child';
  const expected = '- parent\n\n  - \n    - [ ] child';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });
  await writeStoredMarkdown(page, workspaceName, noteName, source);
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await expect(editor.getByRole('checkbox')).toHaveCount(1);
  await expect(editor.getByRole('checkbox')).not.toBeChecked();
  await collapseEditorSelectionAfterText(page, 'child');
  await page.keyboard.press('Tab');

  await expect(editor.getByRole('checkbox')).toHaveCount(1);
  await expect(editor.getByRole('checkbox')).not.toBeChecked();
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(editor.getByText('child', { exact: true })).toBeVisible();
  await expect(editor.getByRole('checkbox')).toHaveCount(1);
  await expect(editor.getByRole('checkbox')).not.toBeChecked();
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    expected,
  );
});

test('insert-item shortcuts add siblings that keep the list marker', async ({
  page,
}) => {
  const workspaceName = 'list-insert-shortcuts';
  const noteName = 'insert-items';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });

  await writeStoredMarkdown(
    page,
    workspaceName,
    noteName,
    '- alpha\n- beta\n- gamma',
  );
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await collapseEditorSelectionAfterText(page, 'beta');
  await waitForEditorFocus(page, {});
  await page.keyboard.press('ControlOrMeta+Enter');
  await page.keyboard.insertText('delta');

  const withBelow = '- alpha\n- beta\n- delta\n- gamma';
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(withBelow);
  await expect(
    editor.locator('.prosemirror-flat-list[data-list-container-kind="bullet"]'),
  ).toHaveCount(4);

  await collapseEditorSelectionAfterText(page, 'alpha');
  await page.keyboard.press('ControlOrMeta+Shift+Enter');
  await page.keyboard.insertText('start');

  const withAbove = '- start\n- alpha\n- beta\n- delta\n- gamma';
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(withAbove);

  await page.reload({ waitUntil: 'networkidle' });
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    withAbove,
  );
});

test('insert-item shortcut adds an unchecked task without toggling the source', async ({
  page,
}) => {
  const workspaceName = 'list-insert-task-shortcut';
  const noteName = 'insert-task';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });

  await writeStoredMarkdown(page, workspaceName, noteName, '- [x] done');
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await expect(editor.getByRole('checkbox')).toBeChecked();
  await collapseEditorSelectionAfterText(page, 'done');
  await waitForEditorFocus(page, {});
  await page.keyboard.press('ControlOrMeta+Enter');
  await page.keyboard.insertText('todo');

  const expected = '- [x] done\n- [ ] todo';
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(expected);
  await expect(editor.getByRole('checkbox')).toHaveCount(2);
  await expect(editor.getByRole('checkbox').first()).toBeChecked();
  await expect(editor.getByRole('checkbox').last()).not.toBeChecked();

  await page.reload({ waitUntil: 'networkidle' });
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    expected,
  );
});

test('the task checkbox shortcut toggles the item under the caret', async ({
  page,
}) => {
  const workspaceName = 'list-task-shortcut';
  const noteName = 'toggle-task';
  await createBrowserWorkspaceAndNote(page, { workspaceName, noteName });

  await writeStoredMarkdown(
    page,
    workspaceName,
    noteName,
    '- [ ] alpha\n- [ ] bravo',
  );
  await page.reload({ waitUntil: 'networkidle' });

  const editor = getEditorLocator(page, {});
  await collapseEditorSelectionAfterText(page, 'bravo');
  await waitForEditorFocus(page, {});
  await page.keyboard.press('Alt+Enter');

  const toggled = '- [ ] alpha\n- [x] bravo';
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe(toggled);
  await expect(editor.getByRole('checkbox').first()).not.toBeChecked();
  await expect(editor.getByRole('checkbox').last()).toBeChecked();

  // Toggling back proves the shortcut is a toggle, not a one-way set.
  await page.keyboard.press('Alt+Enter');
  await expect
    .poll(() => readStoredMarkdown(page, workspaceName, noteName))
    .toBe('- [ ] alpha\n- [ ] bravo');

  await page.reload({ waitUntil: 'networkidle' });
  await expect(readStoredMarkdown(page, workspaceName, noteName)).resolves.toBe(
    '- [ ] alpha\n- [ ] bravo',
  );
});
