import React from "react";
import { combineReducers, createStore } from "redux";
import { Provider } from "react-redux";
import { mount } from "enzyme";
import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
import { actionTypes as at } from "common/Actions.mjs";
import { Lists } from "content-src/components/Widgets/Lists/Lists";

const mockState = {
  ...INITIAL_STATE,
  ListsWidget: {
    selected: "test-list",
    lists: {
      "test-list": {
        label: "test",
        tasks: [{ id: "1", value: "task", completed: false, isUrl: false }],
        completed: [],
      },
    },
  },
};

function WrapWithProvider({ children, state = INITIAL_STATE }) {
  let store = createStore(combineReducers(reducers), state);
  return <Provider store={store}>{children}</Provider>;
}

describe("<Lists>", () => {
  let wrapper;
  let sandbox;
  let dispatch;
  let handleUserInteraction;

  beforeEach(() => {
    sandbox = sinon.createSandbox();
    dispatch = sandbox.stub();
    handleUserInteraction = sandbox.stub();

    wrapper = mount(
      <WrapWithProvider state={mockState}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );
  });

  afterEach(() => {
    // If we defined what the activeElement should be, remove our override
    delete document.activeElement;
  });

  it("should render the component and selected list", () => {
    assert.ok(wrapper.exists());
    assert.ok(wrapper.find(".lists").exists());
    assert.equal(wrapper.find("moz-option").length, 1);
    assert.equal(wrapper.find(".task-item").length, 1);
  });

  it("should update task input and add a new task on Enter key", () => {
    const input = wrapper.find("input").at(0);
    input.simulate("change", { target: { value: "nathan's cool task" } });

    // Override what the current active element so that the dispatch will trigger
    Object.defineProperty(document, "activeElement", {
      value: input.getDOMNode(),
      configurable: true,
    });

    input.simulate("keyDown", { key: "Enter" });

    assert.ok(dispatch.called, "Expected dispatch to be called");

    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.WIDGETS_LISTS_UPDATE);
    assert.ok(
      action.data.lists["test-list"].tasks.some(
        task => task.value === "nathan's cool task"
      )
    );
  });

  it("should toggle task completion", () => {
    const taskItem = wrapper.find(".task-item").at(0);
    const checkbox = wrapper.find("input[type='checkbox']").at(0);
    checkbox.simulate("change", { target: { checked: true } });
    // dispatch not called until transition has ended
    assert.equal(dispatch.callCount, 0);
    taskItem.simulate("transitionEnd", { propertyName: "opacity" });
    assert.ok(dispatch.calledThrice);
    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.WIDGETS_LISTS_UPDATE);
    assert.ok(action.data.lists["test-list"].completed[0].completed);

    // Verify old telemetry event
    const [oldTelemetryEvent] = dispatch.getCall(1).args;
    assert.equal(oldTelemetryEvent.type, at.WIDGETS_LISTS_USER_EVENT);
    assert.equal(oldTelemetryEvent.data.userAction, "task_complete");

    // Verify new unified telemetry event
    const [newTelemetryEvent] = dispatch.getCall(2).args;
    assert.equal(newTelemetryEvent.type, at.WIDGETS_USER_EVENT);
    assert.equal(newTelemetryEvent.data.widget_name, "lists");
    assert.equal(newTelemetryEvent.data.widget_source, "widget");
    assert.equal(newTelemetryEvent.data.user_action, "task_complete");
    assert.equal(newTelemetryEvent.data.widget_size, "medium");
  });

  it("should not dispatch an action when input is empty and Enter is pressed", () => {
    const input = wrapper.find("input").at(0);
    input.simulate("change", { target: { value: "" } });
    // Override what the current active element so that the dispatch will trigger
    Object.defineProperty(document, "activeElement", {
      value: input.getDOMNode(),
      configurable: true,
    });
    input.simulate("keyDown", { key: "Enter" });

    assert.ok(dispatch.notCalled);
  });

  it("should remove task when deleteTask is run from task item panel menu", () => {
    // confirm that there is a task available to delete
    const initialTasks = mockState.ListsWidget.lists["test-list"].tasks;
    assert.equal(initialTasks.length, 1);

    const deleteButton = wrapper.find("panel-item.delete-item").at(0);
    deleteButton.props().onClick();

    assert.ok(dispatch.calledThrice);
    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.WIDGETS_LISTS_UPDATE);

    // Check that the task list is now empty
    const updatedTasks = action.data.lists["test-list"].tasks;
    assert.equal(updatedTasks.length, 0, "Expected task to be removed");

    // Verify old telemetry event
    const [oldTelemetryEvent] = dispatch.getCall(1).args;
    assert.equal(oldTelemetryEvent.type, at.WIDGETS_LISTS_USER_EVENT);
    assert.equal(oldTelemetryEvent.data.userAction, "task_delete");

    // Verify new unified telemetry event
    const [newTelemetryEvent] = dispatch.getCall(2).args;
    assert.equal(newTelemetryEvent.type, at.WIDGETS_USER_EVENT);
    assert.equal(newTelemetryEvent.data.widget_name, "lists");
    assert.equal(newTelemetryEvent.data.widget_source, "widget");
    assert.equal(newTelemetryEvent.data.user_action, "task_delete");
    assert.equal(newTelemetryEvent.data.widget_size, "medium");
  });

  it("should add a task with a valid URL and render it as a link", () => {
    const input = wrapper.find("input").at(0);
    const testUrl = "https://www.example.com";

    input.simulate("change", { target: { value: testUrl } });

    // Set activeElement for Enter key detection
    Object.defineProperty(document, "activeElement", {
      value: input.getDOMNode(),
      configurable: true,
    });

    input.simulate("keyDown", { key: "Enter" });

    assert.ok(dispatch.calledThrice, "Expected dispatch to be called");

    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.WIDGETS_LISTS_UPDATE);

    const newHyperlinkedTask = action.data.lists["test-list"].tasks.find(
      t => t.value === testUrl
    );

    assert.ok(newHyperlinkedTask, "Task with URL should be added");
    assert.ok(newHyperlinkedTask.isUrl, "Task should be marked as a URL");

    // Verify old telemetry event
    const [oldTelemetryEvent] = dispatch.getCall(1).args;
    assert.equal(oldTelemetryEvent.type, at.WIDGETS_LISTS_USER_EVENT);
    assert.equal(oldTelemetryEvent.data.userAction, "task_create");

    // Verify new unified telemetry event
    const [newTelemetryEvent] = dispatch.getCall(2).args;
    assert.equal(newTelemetryEvent.type, at.WIDGETS_USER_EVENT);
    assert.equal(newTelemetryEvent.data.widget_name, "lists");
    assert.equal(newTelemetryEvent.data.widget_source, "widget");
    assert.equal(newTelemetryEvent.data.user_action, "task_create");
    assert.equal(newTelemetryEvent.data.widget_size, "medium");
  });

  it("should dispatch list change when dropdown selection changes", () => {
    const select = wrapper.find("moz-select").getDOMNode();
    // need to create a new event since I couldnt figure out a way to
    // trigger the change event to the moz-select component
    const event = new Event("change", { bubbles: true });
    select.value = "test-list";
    select.dispatchEvent(event);

    assert.ok(dispatch.calledOnce);
    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.WIDGETS_LISTS_CHANGE_SELECTED);
    assert.equal(action.data, "test-list");
  });

  it("should delete list and select a fallback list", () => {
    // Grab panel-item for deleting a list
    const deleteList = wrapper.find("panel-item").at(2);
    deleteList.props().onClick();

    assert.equal(dispatch.callCount, 4);
    assert.equal(dispatch.getCall(0).args[0].type, at.WIDGETS_LISTS_UPDATE);
    assert.equal(
      dispatch.getCall(1).args[0].type,
      at.WIDGETS_LISTS_CHANGE_SELECTED
    );

    // Verify old telemetry event
    const [oldTelemetryEvent] = dispatch.getCall(2).args;
    assert.equal(oldTelemetryEvent.type, at.WIDGETS_LISTS_USER_EVENT);
    assert.equal(oldTelemetryEvent.data.userAction, "list_delete");

    // Verify new unified telemetry event
    const [newTelemetryEvent] = dispatch.getCall(3).args;
    assert.equal(newTelemetryEvent.type, at.WIDGETS_USER_EVENT);
    assert.equal(newTelemetryEvent.data.widget_name, "lists");
    assert.equal(newTelemetryEvent.data.widget_source, "widget");
    assert.equal(newTelemetryEvent.data.user_action, "list_delete");
    assert.equal(newTelemetryEvent.data.widget_size, "medium");
  });

  it("should update list name when edited and saved", () => {
    // Grab panel-item for editing a list
    const editList = wrapper.find("panel-item").at(0);
    editList.props().onClick();
    wrapper.update();

    const editableInput = wrapper.find("input.edit-list");
    editableInput.simulate("change", { target: { value: "Updated List" } });
    editableInput.simulate("keyDown", { key: "Enter" });

    assert.ok(dispatch.calledThrice);
    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.WIDGETS_LISTS_UPDATE);
    assert.equal(action.data.lists["test-list"].label, "Updated List");

    // Verify old telemetry event
    const [oldTelemetryEvent] = dispatch.getCall(1).args;
    assert.equal(oldTelemetryEvent.type, at.WIDGETS_LISTS_USER_EVENT);
    assert.equal(oldTelemetryEvent.data.userAction, "list_edit");

    // Verify new unified telemetry event
    const [newTelemetryEvent] = dispatch.getCall(2).args;
    assert.equal(newTelemetryEvent.type, at.WIDGETS_USER_EVENT);
    assert.equal(newTelemetryEvent.data.widget_name, "lists");
    assert.equal(newTelemetryEvent.data.widget_source, "widget");
    assert.equal(newTelemetryEvent.data.user_action, "list_edit");
    assert.equal(newTelemetryEvent.data.widget_size, "medium");
  });

  it("should create a new list and dispatch update and select list actions", () => {
    const createListBtn = wrapper.find("panel-item").at(1); // assumes "Create a new list" is at index 1
    createListBtn.props().onClick();
    assert.equal(dispatch.callCount, 4);
    assert.equal(dispatch.getCall(0).args[0].type, at.WIDGETS_LISTS_UPDATE);
    assert.equal(
      dispatch.getCall(1).args[0].type,
      at.WIDGETS_LISTS_CHANGE_SELECTED
    );

    // Verify old telemetry event
    const [oldTelemetryEvent] = dispatch.getCall(2).args;
    assert.equal(oldTelemetryEvent.type, at.WIDGETS_LISTS_USER_EVENT);
    assert.equal(oldTelemetryEvent.data.userAction, "list_create");

    // Verify new unified telemetry event
    const [newTelemetryEvent] = dispatch.getCall(3).args;
    assert.equal(newTelemetryEvent.type, at.WIDGETS_USER_EVENT);
    assert.equal(newTelemetryEvent.data.widget_name, "lists");
    assert.equal(newTelemetryEvent.data.widget_source, "widget");
    assert.equal(newTelemetryEvent.data.user_action, "list_create");
    assert.equal(newTelemetryEvent.data.widget_size, "medium");
  });

  it("should copy the current list to clipboard with correct formatting", () => {
    // Set up task list with additional "completed" task
    const task1 = {
      id: "1",
      value: "task 1",
      completed: false,
      isUrl: false,
    };
    const task2 = {
      id: "2",
      value: "task 2",
      completed: true,
      isUrl: false,
    };

    mockState.ListsWidget.lists["test-list"].tasks = [task1, task2];

    wrapper = mount(
      <WrapWithProvider state={mockState}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const clipboardWriteTextStub = sinon.stub(navigator.clipboard, "writeText");

    // Grab panel-item for copying a list
    const copyList = wrapper.find("panel-item").at(3);
    copyList.props().onClick();

    assert.ok(
      clipboardWriteTextStub.calledOnce,
      "Expected clipboard.writeText to be called"
    );

    const [copiedText] = clipboardWriteTextStub.firstCall.args;
    assert.include(
      copiedText,
      "List: test",
      "Expected list title in copied text"
    );
    assert.include(
      copiedText,
      "- [ ] task 1",
      "- [x] task 2",
      "Expected uncompleted and completed tasks in copied text"
    );

    // Confirm WIDGETS_LISTS_USER_EVENT telemetry `list_copy` event
    assert.ok(dispatch.calledTwice);
    const [copyEvent] = dispatch.getCall(0).args;
    assert.equal(copyEvent.type, at.WIDGETS_LISTS_USER_EVENT);
    assert.equal(copyEvent.data.userAction, "list_copy");

    // Verify new unified telemetry event
    const [newTelemetryEvent] = dispatch.getCall(1).args;
    assert.equal(newTelemetryEvent.type, at.WIDGETS_USER_EVENT);
    assert.equal(newTelemetryEvent.data.widget_name, "lists");
    assert.equal(newTelemetryEvent.data.widget_source, "widget");
    assert.equal(newTelemetryEvent.data.user_action, "list_copy");
    assert.equal(newTelemetryEvent.data.widget_size, "medium");

    clipboardWriteTextStub.restore();
  });

  it("should reorder tasks via reorder event", () => {
    const task1 = {
      id: "1",
      value: "task 1",
      completed: false,
      isUrl: false,
    };
    const task2 = {
      id: "2",
      value: "task 2",
      completed: false,
      isUrl: false,
    };

    mockState.ListsWidget.lists["test-list"].tasks = [task1, task2];

    wrapper = mount(
      <WrapWithProvider state={mockState}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const reorderNode = wrapper.find("moz-reorderable-list").getDOMNode();

    // Simulate moving task2 before task1
    const event = new CustomEvent("reorder", {
      detail: {
        draggedElement: { id: "2" },
        targetElement: { id: "1" },
        position: -1,
      },
      bubbles: true,
    });

    reorderNode.dispatchEvent(event);

    assert.ok(dispatch.calledOnce);
    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.WIDGETS_LISTS_UPDATE);

    const reorderedTasks = action.data.lists["test-list"].tasks;
    assert.deepEqual(reorderedTasks, [task2, task1]);
  });

  it("should hide Lists widget when 'Hide widget' option is clicked", () => {
    const menuItem = wrapper.find(
      "panel-item[data-l10n-id='newtab-widget-menu-hide']"
    );
    menuItem.props().onClick();

    assert.ok(dispatch.calledTwice);

    const [setPrefAction] = dispatch.getCall(0).args;
    assert.equal(setPrefAction.type, at.SET_PREF);
    assert.equal(setPrefAction.data.name, "widgets.lists.enabled");
    assert.equal(setPrefAction.data.value, false);

    const [telemetryEvent] = dispatch.getCall(1).args;
    assert.equal(telemetryEvent.type, at.WIDGETS_ENABLED);
    assert.equal(telemetryEvent.data.widget_name, "lists");
    assert.equal(telemetryEvent.data.widget_source, "context_menu");
    assert.equal(telemetryEvent.data.enabled, false);
    assert.equal(telemetryEvent.data.widget_size, "medium");
  });

  it("should dispatch OPEN_LINK when the Learn More option is clicked", () => {
    const learnMoreItem = wrapper.find(".learn-more");
    learnMoreItem.props().onClick();

    assert.ok(dispatch.calledOnce);
    const [action] = dispatch.getCall(0).args;
    assert.equal(action.type, at.OPEN_LINK);
  });

  it("disables Create new list action (in the panel list) when at the max lists limit", () => {
    // Set temporary maximum list limit
    const stateAtMax = {
      ...mockState,
      Prefs: {
        ...INITIAL_STATE.Prefs,
        values: {
          ...INITIAL_STATE.Prefs.values,
          "widgets.lists.maxLists": 1,
        },
      },
    };

    const localWrapper = mount(
      <WrapWithProvider state={stateAtMax}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const createListBtn = localWrapper.find("panel-item.create-list").at(0);
    assert.strictEqual(createListBtn.prop("disabled"), true);
  });

  it("overrides `widgets.lists.maxLists` pref when below `1` value", () => {
    const state = {
      ...mockState,
      Prefs: {
        ...mockState.Prefs,
        values: {
          ...mockState.Prefs.values,
          "widgets.lists.maxLists": 0,
        },
      },
    };
    const localWrapper = mount(
      <WrapWithProvider state={state}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );
    const createListBtn = localWrapper.find("panel-item.create-list").at(0);
    // with 1 existing list, and maxLists coerced to 1, it should be disabled
    assert.strictEqual(createListBtn.prop("disabled"), true);
  });

  it("disables Create List option when at the maximum lists limit", () => {
    const state = {
      ...mockState,
      ListsWidget: {
        ...mockState.ListsWidget,
        lists: {
          "list-1": { label: "A", tasks: [], completed: [] },
          "list-2": { label: "B", tasks: [], completed: [] },
        },
      },
      Prefs: {
        ...mockState.Prefs,
        values: {
          ...mockState.Prefs.values,
          "widgets.lists.maxLists": 2,
        },
      },
    };
    const localWrapper = mount(
      <WrapWithProvider state={state}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );
    const createListBtn = localWrapper.find("panel-item.create-list").at(0);
    // with 2 existing lists, and maxLists is set to 2, it should be disabled
    assert.strictEqual(createListBtn.prop("disabled"), true);
  });

  it("disables add-task input when at maximum list items limit", () => {
    // total items = tasks + completed = 3
    const state = {
      ...mockState,
      ListsWidget: {
        selected: "test-list",
        lists: {
          "test-list": {
            label: "test",
            tasks: [
              { id: "1", value: "task 1", completed: false, isUrl: false },
              { id: "2", value: "task 2", completed: false, isUrl: false },
            ],
            completed: [
              { id: "c1", value: "done", completed: true, isUrl: false },
            ],
          },
        },
      },
      Prefs: {
        ...mockState.Prefs,
        values: {
          ...mockState.Prefs?.values,
          // At limit (3), so input should be disabled and icon greyed
          "widgets.lists.maxListItems": 3,
        },
      },
    };

    const localWrapper = mount(
      <WrapWithProvider state={state}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const input = localWrapper.find("input.add-task-input").at(0);
    const addIcon = localWrapper
      .find(".add-task-container .icon.icon-add")
      .at(0);

    assert.strictEqual(
      input.prop("disabled"),
      true,
      "Expected add-task input to be disabled at the maximum list items limit"
    );
    assert.strictEqual(
      addIcon.hasClass("icon-disabled"),
      true,
      "Expected add icon to have icon-disabled class at the maximum list items limit"
    );
  });

  it("enables add-task input when at maximum list items limit", () => {
    // with 3 items in current list, and maxLists coerced to 1, it should be enabled
    const state = {
      ...mockState,
      Prefs: {
        ...mockState.Prefs,
        values: {
          ...mockState.Prefs?.values,
          "widgets.lists.maxListItems": 5,
        },
      },
    };

    const localWrapper = mount(
      <WrapWithProvider state={state}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const input = localWrapper.find("input.add-task-input").at(0);
    const addIcon = localWrapper
      .find(".add-task-container .icon.icon-add")
      .at(0);

    assert.strictEqual(
      input.prop("disabled"),
      false,
      "Expected input to be enabled when under limit"
    );
    assert.strictEqual(
      addIcon.hasClass("icon-disabled"),
      false,
      "Expected add icon not to be greyed when under limit"
    );
  });

  it("should cancel creating a new list when Escape key is pressed", () => {
    const newListId = "new-list-id";

    // Provide a fallback list so CHANGE_SELECTED has somewhere to go after delete
    const stateWithEmptyAndFallback = {
      ...mockState,
      ListsWidget: {
        selected: newListId,
        lists: {
          [newListId]: { label: "", tasks: [], completed: [] }, // empty "new" list
          "test-list": { label: "test", tasks: [], completed: [] }, // fallback
        },
      },
    };

    const localWrapper = mount(
      <WrapWithProvider state={stateWithEmptyAndFallback}>
        <Lists
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const editableText = localWrapper.find("EditableText").at(0);

    assert.ok(editableText.exists());
    editableText.props().setIsEditing(true);
    localWrapper.update();

    let editableInput = localWrapper.find("input.edit-list");
    assert.ok(editableInput.exists());

    // Press Escape to cancel new-list creation
    editableInput.simulate("keyDown", { key: "Escape" });
    localWrapper.update();

    // Test dispatches from handleCancelNewList
    const types = dispatch.getCalls().map(call => call.args[0].type);
    assert.include(
      types,
      at.WIDGETS_LISTS_UPDATE,
      "Expected update dispatch on cancel"
    );
    assert.include(
      types,
      at.WIDGETS_LISTS_CHANGE_SELECTED,
      "Expected selected list to change after cancel"
    );
    assert.include(
      types,
      at.WIDGETS_LISTS_USER_EVENT,
      "Expected telemetry event on cancel"
    );

    const listsState = localWrapper.find(Provider).prop("store").getState()
      .ListsWidget.lists;
    assert.strictEqual(
      Object.keys(listsState).length,
      2,
      "expected total lists count to remain as 2 (no new list created on cancel)"
    );

    // After cancelling, the input should be gone (editing ended / list removed)
    editableInput = localWrapper.find("input.edit-list");
    assert.strictEqual(editableInput.exists(), false);
  });
});
