// Copyright 2020 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 * as Common from '../../../core/common/common.js';
import type * as Platform from '../../../core/platform/platform.js';
import * as SDK from '../../../core/sdk/sdk.js';
import type * as Protocol from '../../../generated/protocol.js';
import {
  renderElementIntoDOM,
} from '../../../testing/DOMHelpers.js';
import {createTarget} from '../../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../../testing/MockConnection.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';

import * as ElementsComponents from './components.js';

describeWithMockConnection('LayoutPane', () => {
  let target: SDK.Target.Target;
  let domModel: SDK.DOMModel.DOMModel;
  let overlayModel: SDK.OverlayModel.OverlayModel;
  let getNodesByStyle: sinon.SinonStub;
  beforeEach(() => {
    target = createTarget();
    domModel = target.model(SDK.DOMModel.DOMModel) as SDK.DOMModel.DOMModel;
    assert.exists(domModel);
    getNodesByStyle = sinon.stub(domModel, 'getNodesByStyle').resolves([]);
    overlayModel = target.model(SDK.OverlayModel.OverlayModel) as SDK.OverlayModel.OverlayModel;
    assert.exists(overlayModel);
  });

  async function renderComponent() {
    const component = new ElementsComponents.LayoutPane.LayoutPane();
    renderElementIntoDOM(component);
    component.wasShown();
    await RenderCoordinator.done({waitForWork: true});
    return component;
  }

  function queryLabels(component: HTMLElement, selector: string) {
    assert.isNotNull(component.shadowRoot);
    return Array.from(component.shadowRoot.querySelectorAll(selector)).map(label => {
      const input = label.querySelector('[data-input]');
      assert.instanceOf(input, HTMLElement);

      return {
        label: label.getAttribute('title'),
        input: input.tagName,
      };
    });
  }

  it('renders settings', async () => {
    Common.Settings.Settings.instance()
        .moduleSetting('show-grid-line-labels')
        .setTitle('Enum setting title' as Platform.UIString.LocalizedString);
    Common.Settings.Settings.instance()
        .moduleSetting('show-grid-track-sizes')
        .setTitle('Boolean setting title' as Platform.UIString.LocalizedString);

    const component = await renderComponent();
    assert.deepEqual(queryLabels(component, '[data-enum-setting]'), [{label: 'Enum setting title', input: 'SELECT'}]);
    assert.deepEqual(
        queryLabels(component, '[data-boolean-setting]'),
        [{label: 'Boolean setting title', input: 'INPUT'}, {label: '', input: 'INPUT'}, {label: '', input: 'INPUT'}]);
  });

  it('stores a setting when changed', async () => {
    const component = await renderComponent();

    assert.isNotNull(component.shadowRoot);
    assert.isTrue(Common.Settings.Settings.instance().moduleSetting('show-grid-track-sizes').get());
    const input = component.shadowRoot.querySelector('[data-boolean-setting] [data-input]');
    assert.instanceOf(input, HTMLInputElement);

    input.click();

    assert.isFalse(Common.Settings.Settings.instance().moduleSetting('show-grid-track-sizes').get());
  });

  function makeNode(id: Protocol.DOM.NodeId) {
    return {
      id,
      path: () => 'body > div',
      ancestorUserAgentShadowRoot: () => false,
      localName: () => 'div',
      getAttribute: () => '',
      scrollIntoView: () => {},
      highlight: () => {},
      domModel: () => domModel,
    } as unknown as SDK.DOMModel.DOMNode;
  }

  const ID_1 = 1 as Protocol.DOM.NodeId;
  const ID_2 = 2 as Protocol.DOM.NodeId;
  const ID_3 = 3 as Protocol.DOM.NodeId;

  it('renders grid elements', async () => {
    getNodesByStyle.withArgs([{name: 'display', value: 'grid'}, {name: 'display', value: 'inline-grid'}]).resolves([
      ID_1,
      ID_2,
      ID_3,
    ]);
    sinon.stub(domModel, 'nodeForId')
        .withArgs(ID_1)
        .returns(makeNode(ID_1))
        .withArgs(ID_2)
        .returns(makeNode(ID_2))
        .withArgs(ID_3)
        .returns(makeNode(ID_2));

    const component = await renderComponent();
    assert.isNotNull(component.shadowRoot);

    assert.lengthOf(queryLabels(component, '[data-element]'), 3);
  });

  it('renders flex elements', async () => {
    getNodesByStyle.withArgs([{name: 'display', value: 'flex'}, {name: 'display', value: 'inline-flex'}]).resolves([
      ID_1,
      ID_2,
      ID_3,
    ]);
    sinon.stub(domModel, 'nodeForId')
        .withArgs(ID_1)
        .returns(makeNode(ID_1))
        .withArgs(ID_2)
        .returns(makeNode(ID_2))
        .withArgs(ID_3)
        .returns(makeNode(ID_3));

    const component = await renderComponent();
    assert.isNotNull(component.shadowRoot);

    assert.lengthOf(queryLabels(component, '[data-element]'), 3);
  });

  it('send an event when an element overlay is toggled', async () => {
    getNodesByStyle.withArgs([{name: 'display', value: 'grid'}, {name: 'display', value: 'inline-grid'}]).resolves([
      ID_1,
    ]);
    sinon.stub(domModel, 'nodeForId').withArgs(ID_1).returns(makeNode(ID_1));
    const highlightGrid = sinon.spy(overlayModel, 'highlightGridInPersistentOverlay');

    const component = await renderComponent();
    assert.isNotNull(component.shadowRoot);

    const input = component.shadowRoot.querySelector('[data-element] [data-input]');
    assert.instanceOf(input, HTMLInputElement);
    input.click();
    assert.isTrue(highlightGrid.calledOnceWith(ID_1));
  });

  it('send an event when an element’s Show element button is pressed', async () => {
    getNodesByStyle.withArgs([{name: 'display', value: 'grid'}, {name: 'display', value: 'inline-grid'}]).resolves([
      ID_1,
    ]);
    const node = makeNode(ID_1);
    sinon.stub(domModel, 'nodeForId').withArgs(ID_1).returns(node);
    const reveal = sinon.stub(Common.Revealer.RevealerRegistry.prototype, 'reveal').resolves();

    const component = await renderComponent();
    assert.isNotNull(component.shadowRoot);

    const button = component.shadowRoot.querySelector('.show-element');
    assert.instanceOf(button, HTMLElement);
    button.click();
    assert.isTrue(reveal.calledOnceWith(node, false));
  });

  it('expands/collapses <details> using ArrowLeft/ArrowRight keys', async () => {
    const component = await renderComponent();
    assert.isNotNull(component.shadowRoot);
    const details = component.shadowRoot.querySelector('details');
    assert.instanceOf(details, HTMLDetailsElement);
    const summary = details.querySelector('summary');
    assert.instanceOf(summary, HTMLElement);
    assert(details.open, 'The first details were not expanded by default');
    summary.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, cancelable: true, key: 'ArrowLeft'}));
    assert(!details.open, 'The details were not collapsed after sending ArrowLeft');
    summary.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, cancelable: true, key: 'ArrowRight'}));
    assert(details.open, 'The details were not expanded after sending ArrowRight');
  });

  const updatesUiOnEvent = <T extends keyof SDK.OverlayModel.EventTypes>(
      event: Platform.TypeScriptUtilities.NoUnion<T>, inScope: boolean) => async () => {
    SDK.TargetManager.TargetManager.instance().setScopeTarget(inScope ? target : null);
    const render = sinon.spy(ElementsComponents.LayoutPane.LayoutPane.prototype, 'render');
    await renderComponent();
    await RenderCoordinator.done();
    render.resetHistory();
    overlayModel.dispatchEventToListeners(
        event,
        ...[{nodeId: 42, enabled: true}] as unknown as
            Common.EventTarget.EventPayloadToRestParameters<SDK.OverlayModel.EventTypes, T>);
    await RenderCoordinator.done();
    assert.strictEqual(render.called, inScope);
  };

  it('updates UI on in scope grid overlay update event',
     updatesUiOnEvent(SDK.OverlayModel.Events.PERSISTENT_GRID_OVERLAY_STATE_CHANGED, true));
  it('does not update UI on out of scope grid overlay update event',
     updatesUiOnEvent(SDK.OverlayModel.Events.PERSISTENT_GRID_OVERLAY_STATE_CHANGED, false));
  it('updates UI on in scope flex overlay update event',
     updatesUiOnEvent(SDK.OverlayModel.Events.PERSISTENT_FLEX_CONTAINER_OVERLAY_STATE_CHANGED, true));
  it('does not update UI on out of scope flex overlay update event',
     updatesUiOnEvent(SDK.OverlayModel.Events.PERSISTENT_FLEX_CONTAINER_OVERLAY_STATE_CHANGED, false));
});
