// Copyright 2024 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 type * as Common from '../../core/common/common.js';
import type * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.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 UI from '../../ui/legacy/legacy.js';

import * as Resources from './application.js';

import View = Resources.ExtensionStorageItemsView;

class ExtensionStorageItemsListener {
  #dispatcher: Common.ObjectWrapper.ObjectWrapper<View.ExtensionStorageItemsDispatcher.EventTypes>;
  #edited: boolean = false;
  #refreshed: boolean = false;

  constructor(dispatcher: Common.ObjectWrapper.ObjectWrapper<View.ExtensionStorageItemsDispatcher.EventTypes>) {
    this.#dispatcher = dispatcher;
    this.#dispatcher.addEventListener(View.ExtensionStorageItemsDispatcher.Events.ITEM_EDITED, this.#itemsEdited, this);
    this.#dispatcher.addEventListener(
        View.ExtensionStorageItemsDispatcher.Events.ITEMS_REFRESHED, this.#itemsRefreshed, this);
  }

  dispose(): void {
    this.#dispatcher.removeEventListener(
        View.ExtensionStorageItemsDispatcher.Events.ITEM_EDITED, this.#itemsEdited, this);
    this.#dispatcher.removeEventListener(
        View.ExtensionStorageItemsDispatcher.Events.ITEMS_REFRESHED, this.#itemsRefreshed, this);
  }

  resetEdited(): void {
    this.#edited = false;
  }

  #itemsEdited(): void {
    this.#edited = true;
  }

  async waitForItemsEdited(): Promise<void> {
    if (!this.#edited) {
      await this.#dispatcher.once(View.ExtensionStorageItemsDispatcher.Events.ITEM_EDITED);
    }
    this.#edited = true;
  }

  resetRefreshed(): void {
    this.#refreshed = false;
  }

  #itemsRefreshed(): void {
    this.#refreshed = true;
  }

  async waitForItemsRefreshed(): Promise<void> {
    if (!this.#refreshed) {
      await this.#dispatcher.once(View.ExtensionStorageItemsDispatcher.Events.ITEMS_REFRESHED);
    }
    this.#refreshed = true;
  }
}

describeWithMockConnection('ExtensionStorageItemsView', function() {
  let target: SDK.Target.Target;
  let extensionStorageModel: Resources.ExtensionStorageModel.ExtensionStorageModel|null;
  let extensionStorage: Resources.ExtensionStorageModel.ExtensionStorage;

  const TEST_EXTENSION_ID = 'abc';
  const TEST_EXTENSION_NAME = 'Hello World';

  const EXAMPLE_DATA: {[key: string]: string} = {a: 'foo', b: 'bar'};

  beforeEach(() => {
    target = createTarget();
    extensionStorageModel = target.model(Resources.ExtensionStorageModel.ExtensionStorageModel);
    assert.exists(extensionStorageModel);
    extensionStorage = new Resources.ExtensionStorageModel.ExtensionStorage(
        extensionStorageModel, TEST_EXTENSION_ID, TEST_EXTENSION_NAME, Protocol.Extensions.StorageArea.Local);
  });

  function createView(): {view: View.ExtensionStorageItemsView, viewFunction: sinon.SinonStub} {
    const viewFunction = sinon.stub();
    viewFunction.callsFake((_input, output, _target) => {
      output.splitWidget = sinon.createStubInstance(UI.SplitWidget.SplitWidget);
      output.preview = new UI.Widget.VBox();
      output.resizer = sinon.createStubInstance(HTMLElement);
    });
    const view = new View.ExtensionStorageItemsView(extensionStorage, viewFunction);
    return {view, viewFunction};
  }

  it('displays items', async () => {
    assert.exists(extensionStorageModel);
    sinon.stub(extensionStorageModel.agent, 'invoke_getStorageItems')
        .withArgs({id: TEST_EXTENSION_ID, storageArea: Protocol.Extensions.StorageArea.Local})
        .resolves({
          data: EXAMPLE_DATA,
          getError: () => undefined,
        });

    const {view, viewFunction} = createView();

    const itemsListener = new ExtensionStorageItemsListener(view.extensionStorageItemsDispatcher);
    await itemsListener.waitForItemsRefreshed();

    assert.deepEqual(
        viewFunction.lastCall.firstArg.items, Object.keys(EXAMPLE_DATA).map(key => ({key, value: EXAMPLE_DATA[key]})));
  });

  it('correctly parses set values as JSON, with string fallback', async () => {
    assert.exists(extensionStorageModel);
    sinon.stub(extensionStorageModel.agent, 'invoke_getStorageItems')
        .withArgs({id: TEST_EXTENSION_ID, storageArea: Protocol.Extensions.StorageArea.Local})
        .resolves({
          data: EXAMPLE_DATA,
          getError: () => undefined,
        });
    const setStorageItems =
        sinon.stub(extensionStorageModel.agent, 'invoke_setStorageItems').resolves({getError: () => undefined});

    const {view, viewFunction} = createView();
    const itemsListener = new ExtensionStorageItemsListener(view.extensionStorageItemsDispatcher);
    await itemsListener.waitForItemsRefreshed();

    const expectedResults = [
      {input: '{foo: "bar"}', parsedValue: {foo: 'bar'}},
      {input: 'value', parsedValue: 'value'},
    ];

    for (const {input, parsedValue} of expectedResults) {
      const key = Object.keys(EXAMPLE_DATA)[0];
      viewFunction.lastCall.firstArg.onEdit(new CustomEvent('edit', {
        detail: {node: {dataset: {key}}, columnId: 'value', valueBeforeEditing: EXAMPLE_DATA[key], newText: input}
      }));

      await itemsListener.waitForItemsEdited();
      setStorageItems.calledOnceWithExactly(
          {id: TEST_EXTENSION_ID, storageArea: Protocol.Extensions.StorageArea.Local, values: {[key]: parsedValue}});

      setStorageItems.reset();
    }

    await RenderCoordinator.done();
    view.detach();
  });
});
