import { ApproxStructure, Assertions, FocusTools, Keyboard, Keys, Mouse, UiFinder, Waiter } from '@ephox/agar';
import { beforeEach, context, describe, it } from '@ephox/bedrock-client';
import { Arr, Fun } from '@ephox/katamari';
import { SugarBody, SugarDocument } from '@ephox/sugar';
import { TinyHooks, TinySelections, TinyState, TinyUiActions } from '@ephox/wrap-mcagar';
import { assert } from 'chai';

import Editor from 'tinymce/core/api/Editor';
import { EditorEvent } from 'tinymce/core/api/util/EventDispatcher';

import * as MenuUtils from '../../../module/MenuUtils';

describe('browser.tinymce.themes.silver.editor.bespoke.SilverBespokeButtonsTest', () => {
  const hook = TinyHooks.bddSetup<Editor>({
    toolbar: 'align fontfamily fontsize blocks styles',
    base_url: '/project/tinymce/js/tinymce',
    content_css: '/project/tinymce/src/themes/silver/test/css/content.css',
  }, []);

  const pAssertFocusOnItem = (itemText: string) => FocusTools.pTryOnSelector(
    `Focus should be on ${itemText}`,
    SugarDocument.getDocument(),
    `.tox-collection__item:contains("${itemText}")`
  );

  const pAssertFocusOnToolbarButton = (buttonText: string) => FocusTools.pTryOnSelector(
    `Focus should be on ${buttonText}`,
    SugarDocument.getDocument(),
    `.tox-toolbar__group button:contains("${buttonText}")`
  );

  const pAssertFocusOnAlignToolbarButton = () => FocusTools.pTryOnSelector(
    'Focus should be on Align',
    SugarDocument.getDocument(),
    '.tox-toolbar__group button[aria-label^="Align"].tox-tbtn--select'
  );

  const assertItemTicks = (label: string, expectedTicks: boolean[]) => {
    const group = UiFinder.findIn(SugarBody.body(), '.tox-selected-menu .tox-collection__group').getOrDie();
    Assertions.assertStructure(label, ApproxStructure.build((s, str, arr) => s.element('div', {
      classes: [ arr.has('tox-collection__group') ],
      children: Arr.map(expectedTicks, (expected) => s.element('div', {
        attrs: {
          'role': str.is('menuitemcheckbox'),
          'aria-checked': str.is(expected ? 'true' : 'false')
        }
      }))
    })), group);
  };

  const pCheckItemsAtLocationPlus = (pBeforeStep: () => Promise<void>, pAfterStep: () => Promise<void>, pOpen: (menu: MenuUtils.OpenMenu) => Promise<void>) =>
    async (label: string, editor: Editor, expectedTicks: boolean[], menu: MenuUtils.OpenMenu, path: number[], offset: number) => {
      TinySelections.setCursor(editor, path, offset);
      await pOpen(menu);
      await pBeforeStep();
      assertItemTicks(label, expectedTicks);
      await pAfterStep();
      TinyUiActions.keyup(editor, Keys.escape());
      UiFinder.notExists(SugarBody.body(), '[role="menu"]');
    };

  const pNoop = () => Promise.resolve();
  const pCheckItemsAtLocation = pCheckItemsAtLocationPlus(pNoop, pNoop, MenuUtils.pOpenMenu);
  const pCheckAlignItemsAtLocation = pCheckItemsAtLocationPlus(pNoop, pNoop, () => MenuUtils.pOpenAlignMenu(''));

  const pCheckSubItemsAtLocation = (expectedSubmenu: string) => pCheckItemsAtLocationPlus(
    async () => {
      Keyboard.activeKeydown(SugarDocument.getDocument(), Keys.right());
      await pAssertFocusOnItem(expectedSubmenu);
    },
    // Afterwards, escape the submenu
    () => {
      Keyboard.activeKeyup(SugarDocument.getDocument(), Keys.escape());
      return Promise.resolve();
    },
    MenuUtils.pOpenMenu
  );

  let eventCount = 0;
  let lastEventValue = '';
  const testWithEvents = (eventName: string, testFn: (editor: Editor) => Promise<void>) => async () => {
    const eventTester = (e: EditorEvent<{ value: string }>) => {
      eventCount++;
      lastEventValue = e.value;
    };

    eventCount = 0;
    lastEventValue = '';
    const editor = hook.editor();
    editor.on(eventName, eventTester);
    await testFn(editor);
    editor.off(eventName, eventTester);
  };

  const assertNoEvent = () => {
    assert.equal(eventCount, 0);
    assert.equal(lastEventValue, '');
  };

  const assertEvent = (count: number, value: string) => {
    // The event count is sometimes more if SelectionChange triggers a NodeChange. Since SelectionChange is dependent on browser thottling it may or may not execute in time for the assert
    assert.isAtLeast(eventCount, count);
    assert.equal(lastEventValue, value);
  };

  beforeEach(() => {
    // Adding undo levels triggers nodeChange so we need to reset that state between tests
    // or tests would depend on that state or break when you run the test in isolation.
    hook.editor().undoManager.reset();
  });

  it('TBA: Checking alignment ticks and updating',
    testWithEvents('AlignTextUpdate', async (editor) => {
      editor.setContent('<p>First paragraph</p><p>Second paragraph</p>');
      assertNoEvent();
      TinySelections.setCursor(editor, [ 0, 0 ], 'Fi'.length);
      assertEvent(1, 'left');
      await MenuUtils.pOpenAlignMenu('Align');
      assertEvent(2, 'left');
      await pAssertFocusOnItem('Left');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Center');
      assertEvent(2, 'left');
      TinyUiActions.keydown(editor, Keys.enter());
      assertEvent(8, 'center');
      UiFinder.notExists(SugarBody.body(), '[role="menu"]');

      await pCheckAlignItemsAtLocation(
        'First paragraph after "centering"',
        editor,
        [ false, true, false, false ],
        { name: 'Alignment', text: 'Center' },
        [ 0, 0 ], 'Fi'.length
      );

      await pCheckAlignItemsAtLocation(
        'Second paragraph with no set alignment',
        editor,
        [ false, false, false, false ],
        { name: 'Alignment', text: 'Align' },
        [ 1, 0 ], 'Se'.length
      );

      await pCheckAlignItemsAtLocation(
        'First paragraph with the alignment set to "center" previously',
        editor,
        [ false, true, false, false ],
        { name: 'Alignment', text: 'Center' },
        [ 0, 0 ], 'Fi'.length
      );
    }));

  it('TBA: Checking fontfamily ticks and updating',
    testWithEvents('FontFamilyTextUpdate', async (editor) => {
      editor.setContent('<p>First paragraph</p><p>Second paragraph</p>');
      assertNoEvent();
      TinySelections.setCursor(editor, [ 0, 0 ], 'Fi'.length);
      assertEvent(1, 'Verdana');
      await MenuUtils.pOpenMenu({ name: 'FontSelect', text: 'Verdana' });
      assertEvent(1, 'Verdana');
      await pAssertFocusOnItem('Andale Mono');
      assertEvent(1, 'Verdana');
      TinyUiActions.keydown(editor, Keys.enter());
      assertEvent(3, 'Andale Mono');
      UiFinder.notExists(SugarBody.body(), '[role="menu"]');

      await pCheckItemsAtLocation(
        'First paragraph after "Andale Mono"',
        editor,
        [ true ].concat(Arr.range(16, Fun.never)),
        { name: 'Font family', text: 'Andale Mono' },
        [ 0, 0, 0 ], 'Fi'.length
      );

      await pCheckItemsAtLocation(
        'Second paragraph with no set font',
        editor,
        Arr.range<boolean>(14, Fun.never).concat([ true ]).concat(Arr.range(2, Fun.never)),
        { name: 'Font family', text: 'Verdana' },
        [ 1, 0 ], 'Se'.length
      );

      await pCheckItemsAtLocation(
        'First paragraph with the font set to "Andale Mono" previously',
        editor,
        [ true ].concat(Arr.range(16, Fun.never)),
        { name: 'Font family', text: 'Andale Mono' },
        [ 0, 0, 0 ], 'Fi'.length
      );
    }));

  it('TBA: Checking fontsize ticks and updating',
    testWithEvents('FontSizeTextUpdate', async (editor) => {
      editor.setContent('<p>First paragraph</p><p>Second paragraph</p>');
      assertNoEvent();
      TinySelections.setCursor(editor, [ 0, 0 ], 'Fi'.length);
      assertEvent(1, '12pt');
      await MenuUtils.pOpenMenu({ name: 'FontSelect', text: '12pt' }); // This might be fragile.
      assertEvent(2, '12pt');
      await pAssertFocusOnItem('8pt');
      assertEvent(2, '12pt');
      TinyUiActions.keydown(editor, Keys.enter());
      assertEvent(4, '8pt');
      UiFinder.notExists(SugarBody.body(), '[role="menu"]');

      await pCheckItemsAtLocation(
        'First paragraph after "8pt',
        editor,
        [ true ].concat(Arr.range(6, Fun.never)),
        { name: 'FontSelect', text: '8pt' },
        [ 0, 0, 0 ], 'Fi'.length
      );

      await pCheckItemsAtLocation(
        'Second paragraph with no set font size',
        editor,
        Arr.range<boolean>(2, Fun.never).concat([ true ]).concat(Arr.range(4, Fun.never)),
        { name: 'FontSelect', text: '12pt' },
        [ 1, 0 ], 'Se'.length
      );

      await pCheckItemsAtLocation(
        'First paragraph with the font set to "8pt" previously',
        editor,
        [ true ].concat(Arr.range(6, Fun.never)),
        { name: 'FontSelect', text: '8pt' },
        [ 0, 0, 0 ], 'Fi'.length
      );
    }));

  it('TBA: Checking format ticks and updating',
    testWithEvents('BlocksTextUpdate', async (editor) => {
      editor.setContent('<p>First paragraph</p><p>Second paragraph</p>');
      assertNoEvent();
      TinySelections.setCursor(editor, [ 0, 0 ], 'Fi'.length);
      assertEvent(1, 'Paragraph');
      await MenuUtils.pOpenMenu({ name: 'Format', text: 'Paragraph' });
      assertEvent(1, 'Paragraph');
      await pAssertFocusOnItem('Paragraph');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Heading 1');
      assertEvent(2, 'Paragraph');
      TinyUiActions.keydown(editor, Keys.enter());
      assertEvent(5, 'Heading 1');
      UiFinder.notExists(SugarBody.body(), '[role="menu"]');

      await pCheckItemsAtLocation(
        'First block after "h1',
        editor,
        [ false, true ].concat(Arr.range(6, Fun.never)),
        { name: 'Format', text: 'Heading 1' },
        [ 0, 0 ], 'Fi'.length
      );

      await pCheckItemsAtLocation(
        'Second paragraph with no set format',
        editor,
        [ true ].concat(Arr.range(7, Fun.never)),
        { name: 'Format', text: 'Paragraph' },
        [ 1, 0 ], 'Se'.length
      );

      await pCheckItemsAtLocation(
        'First block with the "h1" set previously',
        editor,
        [ false, true ].concat(Arr.range(6, Fun.never)),
        { name: 'Format', text: 'Heading 1' },
        [ 0, 0 ], 'Fi'.length
      );

      // Check that the menus are working also
      Mouse.clickOn(SugarBody.body(), '[role="menubar"] [role="menuitem"]:contains("Format")');
      await pAssertFocusOnItem('Bold');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Italic');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Underline');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Strikethrough');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Superscript');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Subscript');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Code');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Format');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Block');
      TinyUiActions.keydown(editor, Keys.right());
      await pAssertFocusOnItem('Paragraph');
      assertItemTicks('Checking blocks in menu', [ false, true ].concat(Arr.range(6, Fun.never)));
      TinyUiActions.keyup(editor, Keys.escape());
      TinyUiActions.keyup(editor, Keys.escape());
    }));

  it('TBA: Checking style ticks and updating',
    testWithEvents('StylesTextUpdate', async (editor) => {
      editor.setContent('<p>First paragraph</p><p>Second paragraph</p>');
      assertNoEvent();
      TinySelections.setCursor(editor, [ 0, 0 ], 'Fi'.length);
      assertEvent(1, 'Paragraph');
      await MenuUtils.pOpenMenu({ name: 'Format', text: 'Paragraph', last: true });
      assertEvent(2, 'Paragraph');
      await pAssertFocusOnItem('Headings');
      TinyUiActions.keydown(editor, Keys.right());
      await pAssertFocusOnItem('Heading 1');
      assertEvent(2, 'Paragraph');
      TinyUiActions.keydown(editor, Keys.enter());
      assertEvent(4, 'Heading 1');
      UiFinder.notExists(SugarBody.body(), '[role="menu"]');

      await pCheckSubItemsAtLocation('Heading 1')(
        'First block after "h1',
        editor,
        [ true ].concat(Arr.range(5, Fun.never)),
        { name: 'Format', text: 'Heading 1', last: true },
        [ 0, 0 ], 'Fi'.length
      );

      await pCheckSubItemsAtLocation('Heading 1')(
        'Second paragraph with no set format',
        editor,
        Arr.range(6, Fun.never),
        { name: 'Format', text: 'Paragraph', last: true },
        [ 1, 0 ], 'Se'.length
      );

      await pCheckSubItemsAtLocation('Heading 1')(
        'First block with the "h1" set previously',
        editor,
        [ true ].concat(Arr.range(5, Fun.never)),
        { name: 'Format', text: 'Heading 1', last: true },
        [ 0, 0 ], 'Fi'.length
      );

      // Check that the menus are working also
      Mouse.clickOn(SugarBody.body(), '[role="menubar"] [role="menuitem"]:contains("Format")');
      await pAssertFocusOnItem('Bold');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Italic');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Underline');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Strikethrough');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Superscript');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Subscript');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Code');
      TinyUiActions.keydown(editor, Keys.down());
      await pAssertFocusOnItem('Format');
      TinyUiActions.keydown(editor, Keys.right());
      await pAssertFocusOnItem('Headings');
      TinyUiActions.keydown(editor, Keys.right());
      await pAssertFocusOnItem('Heading 1');
      assertItemTicks('Checking headings in menu', [ true ].concat(Arr.range(5, Fun.never)));
      TinyUiActions.keyup(editor, Keys.escape());
      TinyUiActions.keyup(editor, Keys.escape());
      TinyUiActions.keyup(editor, Keys.escape());
    }));

  context('Empty styles provided', () => {
    const hookStyleless = TinyHooks.bddSetup<Editor>({
      toolbar: 'align fontfamily fontsize blocks styles',
      base_url: '/project/tinymce/js/tinymce',
      content_css: '/project/tinymce/src/themes/silver/test/css/content.css',
      style_formats: [],
    }, []);

    it('TINY-12005: Empty styles should disable the menu button', async () => {
      const editor = hookStyleless.editor();
      editor.setContent('<p>Content</p>');
      TinySelections.setCursor(editor, [ 0, 0 ], 2);
      editor.focus();
      const menuItemSelector = 'div[aria-label^="Formats"][role^="menuitem"][aria-disabled="true"]';
      UiFinder.notExists(SugarBody.body(), menuItemSelector);
      Mouse.clickOn(SugarBody.body(), '.tox-edit-focus [role="menubar"] [role="menuitem"]:contains("Format")');
      await Waiter.pTryUntil('Format menu item should be disabled', () => UiFinder.exists(SugarBody.body(), menuItemSelector));
    });

    it('TINY-12005: Empty styles should disable thetoolbar button', async () => {
      const editor = hookStyleless.editor();
      editor.setContent('<p>Content</p>');
      TinySelections.setCursor(editor, [ 0, 0 ], 2);
      editor.focus();
      await Waiter.pTryUntil('Editor must have focus', () => assert.isTrue(editor.hasFocus()));
      UiFinder.exists(SugarBody.body(), 'button[aria-label^="Format"]:disabled');
    });
  });

  it('TBA: Checking toolbar keyboard navigation', async () => {
    const editor = hook.editor();
    editor.setContent('<p>First paragraph</p><p>Second paragraph</p>');
    TinySelections.setCursor(editor, [ 0, 0 ], 'Fi'.length);
    await MenuUtils.pOpenAlignMenu('Align');
    await pAssertFocusOnItem('Left');
    TinyUiActions.keydown(editor, Keys.down());
    await pAssertFocusOnItem('Center');

    // Check moving left and right closes the open dropdown and navigates to the next item
    TinyUiActions.keydown(editor, Keys.right());
    await pAssertFocusOnToolbarButton('Verdana'); // Font Select
    UiFinder.notExists(SugarBody.body(), '[role="menu"]');
    TinyUiActions.keydown(editor, Keys.down());
    await pAssertFocusOnItem('Andale Mono');
    TinyUiActions.keydown(editor, Keys.left());
    await pAssertFocusOnAlignToolbarButton(); // Alignment
    UiFinder.notExists(SugarBody.body(), '[role="menu"]');
  });

  context('Noneditable root', () => {
    const testDisableOnNoneditable = (title: string) => () => {
      TinyState.withNoneditableRootEditor(hook.editor(), (editor) => {
        editor.setContent('<div>Noneditable content</div><div contenteditable="true">Editable content</div>');
        TinySelections.setSelection(editor, [ 0, 0 ], 0, [ 0, 0 ], 2);
        UiFinder.exists(SugarBody.body(), `[aria-label^="${title}"]:disabled`);
        TinySelections.setSelection(editor, [ 1, 0 ], 0, [ 1, 0 ], 2);
        UiFinder.exists(SugarBody.body(), `[aria-label^="${title}"]:not(:disabled)`);
      });
    };

    it('TINY-9669: Disable align on noneditable content', testDisableOnNoneditable('Align'));
    it('TINY-9669: Disable fontfamily on noneditable content', testDisableOnNoneditable('Font'));
    it('TINY-9669: Disable fontsize on noneditable content', testDisableOnNoneditable('Font size'));
    it('TINY-9669: Disable blocks on noneditable content', testDisableOnNoneditable('Block'));
    it('TINY-9669: Disable styles on noneditable content', testDisableOnNoneditable('Format'));
  });

  context('Readonly modes', () => {
    const testDisableInReadonly = (title: string) => () => {
      const editor = hook.editor();
      UiFinder.exists(SugarBody.body(), `[aria-label^="${title}"]:not(:disabled)`);
      editor.mode.set('readonly');
      UiFinder.exists(SugarBody.body(), `[aria-label^="${title}"]:disabled`);

      editor.mode.set('readonly');
      UiFinder.exists(SugarBody.body(), `[aria-label^="${title}"]:disabled`);

      editor.mode.set('design');
      UiFinder.exists(SugarBody.body(), `[aria-label^="${title}"]:not(:disabled)`);
    };

    it('TINY-11211: Disable align on readonly mode', testDisableInReadonly('Align'));
    it('TINY-11211: Disable fontfamily on readonly mode', testDisableInReadonly('Font'));
    it('TINY-11211: Disable fontsize on readonly mode', testDisableInReadonly('Font size'));
    it('TINY-11211: Disable blocks on readonly mode', testDisableInReadonly('Block'));
    it('TINY-11211: Disable styles on readonly mode', testDisableInReadonly('Format'));
  });
});
