// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {
  dispatchKeyDownEvent,
  getEventPromise,
  renderElementIntoDOM,
} from '../../../testing/DOMHelpers.js';
// eslint-disable-next-line rulesdir/es-modules-import
import * as EnvironmentHelpers from '../../../testing/EnvironmentHelpers.js';
import type * as SuggestionInput from '../../../ui/components/suggestion_input/suggestion_input.js';
import * as Models from '../models/models.js';
// eslint-disable-next-line rulesdir/es-modules-import
import * as RecorderHelpers from '../testing/RecorderHelpers.js';

import type * as Components from './components.js';

const {describeWithLocale} = EnvironmentHelpers;

function getStepEditedPromise(editor: Components.StepEditor.StepEditor) {
  return getEventPromise<Components.StepEditor.StepEditedEvent>(
             editor,
             'stepedited',
             )
      .then(({data}) => data);
}

const triggerMicroTaskQueue = async (n = 1) => {
  while (n > 0) {
    --n;
    await new Promise(resolve => setTimeout(resolve, 0));
  }
};

describeWithLocale('StepEditor', () => {
  async function renderEditor(
      step: Models.Schema.Step,
      ): Promise<Components.StepEditor.StepEditor> {
    const editor = document.createElement('devtools-recorder-step-editor');
    editor.step = structuredClone(step) as typeof editor.step;
    renderElementIntoDOM(editor, {});
    await editor.updateComplete;
    return editor;
  }

  function getInputByAttribute(
      editor: Components.StepEditor.StepEditor,
      attribute: string,
      ): SuggestionInput.SuggestionInput.SuggestionInput {
    const input = editor.renderRoot.querySelector(
        `.attribute[data-attribute="${attribute}"] devtools-suggestion-input`,
    );
    if (!input) {
      throw new Error(`${attribute} devtools-suggestion-input not found`);
    }
    return input as SuggestionInput.SuggestionInput.SuggestionInput;
  }

  function getAllInputValues(
      editor: Components.StepEditor.StepEditor,
      ): string[] {
    const result = [];
    const inputs = editor.renderRoot.querySelectorAll(
        'devtools-suggestion-input',
    );
    for (const input of inputs) {
      result.push(input.value);
    }
    return result;
  }

  async function addOptionalField(
      editor: Components.StepEditor.StepEditor,
      attribute: string,
      ): Promise<void> {
    const button = editor.renderRoot.querySelector(
        `devtools-button.add-row[data-attribute="${attribute}"]`,
    );
    assert.instanceOf(button, HTMLElement);
    button.click();
    await triggerMicroTaskQueue();
    await editor.updateComplete;
  }

  async function deleteOptionalField(
      editor: Components.StepEditor.StepEditor,
      attribute: string,
      ): Promise<void> {
    const button = editor.renderRoot.querySelector(
        `devtools-button.delete-row[data-attribute="${attribute}"]`,
    );
    assert.instanceOf(button, HTMLElement);
    button.click();
    await triggerMicroTaskQueue();
    await editor.updateComplete;
  }

  async function clickFrameLevelButton(
      editor: Components.StepEditor.StepEditor,
      className: string,
      ): Promise<void> {
    const button = editor.renderRoot.querySelector(
        `.attribute[data-attribute="frame"] devtools-button${className}`,
    );
    assert.instanceOf(button, HTMLElement);
    button.click();
    await editor.updateComplete;
  }

  async function clickSelectorLevelButton(
      editor: Components.StepEditor.StepEditor,
      path: number[],
      className: string,
      ): Promise<void> {
    const button = editor.renderRoot.querySelector(
        `[data-selector-path="${path.join('.')}"] devtools-button${className}`,
    );
    assert.instanceOf(button, HTMLElement);
    button.click();
    await editor.updateComplete;
  }

  /**
   * Extra button to be able to focus on it in tests to see how
   * the step editor reacts when the focus moves outside of it.
   */
  function createFocusOutsideButton() {
    const button = document.createElement('button');
    button.innerText = 'click';
    renderElementIntoDOM(button, {allowMultipleChildren: true});

    return {
      focus() {
        button.focus();
      },
    };
  }

  beforeEach(() => {
    RecorderHelpers.installMocksForRecordingPlayer();
  });

  it('should edit step type', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Click,
      selectors: [['.cls']],
      offsetX: 1,
      offsetY: 1,
    });
    const step = getStepEditedPromise(editor);

    const input = getInputByAttribute(editor, 'type');
    input.focus();
    input.value = 'change';
    await input.updateComplete;

    input.dispatchEvent(
        new KeyboardEvent('keydown', {
          key: 'Enter',
          bubbles: true,
          composed: true,
        }),
    );
    await editor.updateComplete;

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.Change,
      selectors: ['.cls'],
      value: 'Value',
    });
    assert.deepEqual(getAllInputValues(editor), [
      'change',
      '.cls',
      'Value',
    ]);
  });

  it('should edit step type via dropdown', async () => {
    const editor = await renderEditor({type: Models.Schema.StepType.Scroll});
    const step = getStepEditedPromise(editor);

    const input = getInputByAttribute(editor, 'type');
    input.focus();
    input.value = '';
    await input.updateComplete;

    // Use the drop down.
    input.dispatchEvent(
        new KeyboardEvent('keydown', {
          key: 'ArrowDown',
          bubbles: true,
          composed: true,
        }),
    );
    input.dispatchEvent(
        new KeyboardEvent('keydown', {
          key: 'Enter',
          bubbles: true,
          composed: true,
        }),
    );
    await editor.updateComplete;

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.Click,
      selectors: ['.cls'],
      offsetX: 1,
      offsetY: 1,
    });
    assert.deepEqual(getAllInputValues(editor), [
      'click',
      '.cls',
      '1',
      '1',
    ]);
  });

  it('should edit other attributes', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.CustomStep,
      name: 'test',
      parameters: {},
    });
    const step = getStepEditedPromise(editor);

    const input = getInputByAttribute(editor, 'parameters');
    input.focus();
    input.value = '{"custom":"test"}';
    await input.updateComplete;

    input.dispatchEvent(
        new KeyboardEvent('keydown', {
          key: 'Enter',
          bubbles: true,
          composed: true,
        }),
    );
    await editor.updateComplete;

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.CustomStep,
      name: 'test',
      parameters: {custom: 'test'},
    });
    assert.deepEqual(getAllInputValues(editor), [
      'customStep',
      'test',
      '{"custom":"test"}',
    ]);
  });

  it('should close dropdown on Enter', async () => {
    const editor = await renderEditor({type: Models.Schema.StepType.Scroll});

    const input = getInputByAttribute(editor, 'type');
    input.focus();
    input.value = '';
    await input.updateComplete;

    const suggestions = input.renderRoot.querySelector(
        'devtools-suggestion-box',
    );
    if (!suggestions) {
      throw new Error('Failed to find element');
    }
    assert.strictEqual(
        window.getComputedStyle(suggestions).display,
        'block',
    );

    input.dispatchEvent(
        new KeyboardEvent('keydown', {
          key: 'Enter',
          bubbles: true,
          composed: true,
        }),
    );
    assert.strictEqual(
        window.getComputedStyle(suggestions).display,
        'none',
    );
  });

  it('should close dropdown on focus elsewhere', async () => {
    const editor = await renderEditor({type: Models.Schema.StepType.Scroll});
    const button = createFocusOutsideButton();

    const input = getInputByAttribute(editor, 'type');
    input.focus();
    input.value = '';
    await input.updateComplete;

    const suggestions = input.renderRoot.querySelector(
        'devtools-suggestion-box',
    );
    if (!suggestions) {
      throw new Error('Failed to find element');
    }
    assert.strictEqual(
        window.getComputedStyle(suggestions).display,
        'block',
    );

    button.focus();
    assert.strictEqual(
        window.getComputedStyle(suggestions).display,
        'none',
    );
  });

  it('should add optional fields', async () => {
    const editor = await renderEditor({type: Models.Schema.StepType.Scroll});
    const step = getStepEditedPromise(editor);

    await addOptionalField(editor, 'x');

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.Scroll,
      x: 0,
    });
    assert.deepEqual(getAllInputValues(editor), ['scroll', '0']);
  });

  it('should add the duration field', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Click,
      offsetX: 1,
      offsetY: 1,
      selectors: ['.cls'],
    });
    const step = getStepEditedPromise(editor);

    await addOptionalField(editor, 'duration');

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.Click,
      offsetX: 1,
      offsetY: 1,
      selectors: ['.cls'],
      duration: 50,
    });
    assert.deepEqual(getAllInputValues(editor), [
      'click',
      '.cls',
      '1',
      '1',
      '50',
    ]);
  });

  it('should add the parameters field', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.WaitForElement,
      selectors: ['.cls'],
    });
    const step = getStepEditedPromise(editor);

    await addOptionalField(editor, 'properties');

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.WaitForElement,
      selectors: ['.cls'],
      properties: {},
    });
    assert.deepEqual(getAllInputValues(editor), [
      'waitForElement',
      '.cls',
      '{}',
    ]);
  });

  it('should edit timeout fields', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Navigate,
      url: 'https://example.com',
    });
    const step = getStepEditedPromise(editor);

    await addOptionalField(editor, 'timeout');

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.Navigate,
      url: 'https://example.com',
      timeout: 5000,
    });
    assert.deepEqual(getAllInputValues(editor), [
      'navigate',
      'https://example.com',
      '5000',
    ]);
  });

  it('should delete optional fields', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Scroll,
      x: 1,
    });
    const step = getStepEditedPromise(editor);

    await deleteOptionalField(editor, 'x');

    assert.deepEqual(await step, {type: Models.Schema.StepType.Scroll});
    assert.deepEqual(getAllInputValues(editor), ['scroll']);
  });

  it('should add/remove frames', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Scroll,
      frame: [0],
    });
    {
      const step = getStepEditedPromise(editor);

      await clickFrameLevelButton(editor, '.add-frame');

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.Scroll,
        frame: [0, 0],
      });
      assert.deepEqual(getAllInputValues(editor), ['scroll', '0', '0']);

      assert.isTrue(
          editor.shadowRoot?.activeElement?.matches(
              'devtools-suggestion-input[data-path="frame.1"]',
              ),
      );
    }
    {
      const step = getStepEditedPromise(editor);

      await clickFrameLevelButton(editor, '.remove-frame');

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.Scroll,
        frame: [0],
      });
      assert.deepEqual(getAllInputValues(editor), ['scroll', '0']);

      assert.isTrue(
          editor.shadowRoot?.activeElement?.matches(
              'devtools-suggestion-input[data-path="frame.0"]',
              ),
      );
    }
  });

  it('should add/remove selector parts', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Scroll,
      selectors: [['.part1']],
    });

    {
      const step = getStepEditedPromise(editor);

      await clickSelectorLevelButton(editor, [0, 0], '.add-selector-part');

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.Scroll,
        selectors: [['.part1', '.cls']],
      });
      assert.deepEqual(getAllInputValues(editor), [
        'scroll',
        '.part1',
        '.cls',
      ]);

      assert.isTrue(
          editor.shadowRoot?.activeElement?.matches(
              'devtools-suggestion-input[data-path="selectors.0.1"]',
              ),
      );
    }

    {
      const step = getStepEditedPromise(editor);

      await clickSelectorLevelButton(editor, [0, 0], '.remove-selector-part');

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.Scroll,
        selectors: ['.cls'],
      });
      assert.deepEqual(getAllInputValues(editor), ['scroll', '.cls']);

      assert.isTrue(
          editor.shadowRoot?.activeElement?.matches(
              'devtools-suggestion-input[data-path="selectors.0.0"]',
              ),
      );
    }
  });

  it('should add/remove selectors', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Scroll,
      selectors: [['.part1']],
    });
    {
      const step = getStepEditedPromise(editor);

      await clickSelectorLevelButton(editor, [0], '.add-selector');

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.Scroll,
        selectors: ['.part1', '.cls'],
      });
      assert.deepEqual(getAllInputValues(editor), [
        'scroll',
        '.part1',
        '.cls',
      ]);
      assert.isTrue(
          editor.shadowRoot?.activeElement?.matches(
              'devtools-suggestion-input[data-path="selectors.1.0"]',
              ),
      );
    }
    {
      const step = getStepEditedPromise(editor);

      await clickSelectorLevelButton(editor, [1], '.remove-selector');

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.Scroll,
        selectors: ['.part1'],
      });
      assert.deepEqual(getAllInputValues(editor), ['scroll', '.part1']);
      assert.isTrue(
          editor.shadowRoot?.activeElement?.matches(
              'devtools-suggestion-input[data-path="selectors.0.0"]',
              ),
      );
    }
  });

  it('should become readonly if disabled', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Scroll,
      selectors: [['.part1']],
    });
    editor.disabled = true;
    await editor.updateComplete;

    for (const input of editor.renderRoot.querySelectorAll(
             'devtools-suggestion-input',
             )) {
      assert.isTrue(input.disabled);
    }
  });

  it('clears text selection when navigating away from devtools-suggestion-input', async () => {
    const editor = await renderEditor({type: Models.Schema.StepType.Scroll});

    // Clicking on the type devtools-suggestion-input should select the entire text in the field.
    const input = getInputByAttribute(editor, 'type');
    input.focus();
    input.click();
    assert.strictEqual(window.getSelection()?.toString(), 'scroll');

    // Navigating away should remove the selection.
    dispatchKeyDownEvent(input, {
      key: 'Enter',
      bubbles: true,
      composed: true,
    });
    assert.strictEqual(window.getSelection()?.toString(), '');
  });

  it('should add an attribute after another\'s deletion', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.WaitForElement,
      selectors: [['.cls']],
    });

    await addOptionalField(editor, 'operator');
    await deleteOptionalField(editor, 'operator');
    const step = getStepEditedPromise(editor);
    await addOptionalField(editor, 'count');

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.WaitForElement,
      selectors: ['.cls'],
      count: 1,
    });
    assert.deepEqual(getAllInputValues(editor), [
      'waitForElement',
      '.cls',
      '1',
    ]);
  });

  it('should edit asserted events', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.Navigate,
      url: 'www.example.com',
      assertedEvents: [{
        type: 'navigation' as Models.Schema.AssertedEventType,
        title: 'Test',
        url: 'www.example.com',
      }],
    });

    const step = getStepEditedPromise(editor);

    const input = getInputByAttribute(editor, 'assertedEvents');
    input.focus();
    input.value = 'None';
    await input.updateComplete;

    input.dispatchEvent(
        new KeyboardEvent('keydown', {
          key: 'Enter',
          bubbles: true,
          composed: true,
        }),
    );
    await editor.updateComplete;

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.Navigate,
      url: 'www.example.com',
      assertedEvents: [{
        type: 'navigation' as Models.Schema.AssertedEventType,
        title: 'None',
        url: 'www.example.com',
      }],
    });
  });

  it('should add/remove attribute assertion', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.WaitForElement,
      selectors: ['.part1'],
      attributes: {
        a: 'b',
      },
    });
    {
      const step = getStepEditedPromise(editor);

      editor.renderRoot.querySelectorAll<HTMLElement>('.add-attribute-assertion')[0]?.click();

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.WaitForElement,
        selectors: ['.part1'],
        attributes: {a: 'b', attribute: 'value'},
      });
      assert.deepEqual(getAllInputValues(editor), [
        'waitForElement',
        '.part1',
        'a',
        'b',
        'attribute',
        'value',
      ]);
    }
    {
      const step = getStepEditedPromise(editor);

      editor.renderRoot.querySelectorAll<HTMLElement>('.remove-attribute-assertion')[1]?.click();

      assert.deepEqual(await step, {
        type: Models.Schema.StepType.WaitForElement,
        selectors: ['.part1'],
        attributes: {a: 'b'},
      });
      assert.deepEqual(getAllInputValues(editor), [
        'waitForElement',
        '.part1',
        'a',
        'b',
      ]);
    }
  });

  it('should edit attribute assertion', async () => {
    const editor = await renderEditor({
      type: Models.Schema.StepType.WaitForElement,
      selectors: ['.part1'],
      attributes: {
        a: 'b',
      },
    });

    const step = getStepEditedPromise(editor);

    const input = getInputByAttribute(editor, 'attributes');
    input.focus();
    input.value = 'innerText';
    await input.updateComplete;

    input.dispatchEvent(
        new KeyboardEvent('keydown', {
          key: 'Enter',
          bubbles: true,
          composed: true,
        }),
    );
    await editor.updateComplete;

    assert.deepEqual(await step, {
      type: Models.Schema.StepType.WaitForElement,
      selectors: ['.part1'],
      attributes: {
        innerText: 'b',
      },
    });
  });
});
