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 {
  FocusTimer,
  isNumericValue,
  isAtMaxLength,
} from "content-src/components/Widgets/FocusTimer/FocusTimer";

const PREF_WIDGETS_SYSTEM_NOTIFICATIONS_ENABLED =
  "widgets.focusTimer.showSystemNotifications";

const mockState = {
  ...INITIAL_STATE,
  Prefs: {
    ...INITIAL_STATE.Prefs,
    values: {
      ...INITIAL_STATE.Prefs.values,
      [PREF_WIDGETS_SYSTEM_NOTIFICATIONS_ENABLED]: true,
    },
  },
  TimerWidget: {
    timerType: "focus",
    focus: {
      duration: 1500,
      initialDuration: 1500,
      startTime: null,
      isRunning: null,
    },
    break: {
      duration: 300,
      initialDuration: 300,
      startTime: null,
      isRunning: null,
    },
  },
};

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

describe("<FocusTimer>", () => {
  let wrapper;
  let sandbox;
  let dispatch;
  let clock; // for use with the sinon fake timers api
  let handleUserInteraction;

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

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

  afterEach(() => {
    // restore real timers after each test to avoid leaking sinon's fakeTimers()
    sandbox.restore();
    wrapper?.unmount();
  });

  it("should render timer widget", () => {
    assert.ok(wrapper.exists());
    assert.ok(wrapper.find(".focus-timer").exists());
  });

  it("should show default minutes for Focus timer (25 minutes)", () => {
    const minutes = wrapper.find(".timer-set-minutes").text();
    const seconds = wrapper.find(".timer-set-seconds").text();
    assert.equal(minutes, "25");
    assert.equal(seconds, "00");
  });

  it("should show default minutes for Break timer (5 minutes)", () => {
    const breakState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        timerType: "break", // setting timer type to break
      },
    };

    wrapper = mount(
      <WrapWithProvider state={breakState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const minutes = wrapper.find(".timer-set-minutes").text();
    const seconds = wrapper.find(".timer-set-seconds").text();

    assert.equal(minutes, "05");
    assert.equal(seconds, "00");
  });

  it("should start timer and show progress bar when pressing play", () => {
    wrapper
      .find("moz-button[data-l10n-id='newtab-widget-timer-label-play']")
      .props()
      .onClick();
    wrapper.update();
    assert.ok(wrapper.find(".progress-circle-wrapper").exists());
    assert.equal(dispatch.getCall(0).args[0].type, at.WIDGETS_TIMER_PLAY);
  });

  it("should pause the timer when pressing pause", () => {
    const now = Math.floor(Date.now() / 1000);
    const runningState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        focus: {
          ...mockState.TimerWidget.focus,
          isRunning: true,
          startTime: now,
        },
      },
    };

    wrapper = mount(
      <WrapWithProvider state={runningState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const pauseBtn = wrapper.find(
      "moz-button[data-l10n-id='newtab-widget-timer-label-pause']"
    );
    assert.ok(pauseBtn.exists(), "Pause button should be rendered");
    pauseBtn.props().onClick();
    assert.equal(dispatch.getCall(0).args[0].type, at.WIDGETS_TIMER_PAUSE);
  });

  it("should reset timer should be hidden when timer is not running", () => {
    const resetBtn = wrapper.find(
      "moz-button[data-l10n-id='newtab-widget-timer-reset']"
    );
    assert.ok(!resetBtn.exists(), "Reset buttons should not be rendered");
  });

  it("should reset timer when pressing reset", () => {
    const now = Math.floor(Date.now() / 1000);
    const runningState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        focus: {
          ...mockState.TimerWidget.focus,
          isRunning: true,
          startTime: now,
        },
      },
    };

    wrapper = mount(
      <WrapWithProvider state={runningState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const resetBtn = wrapper.find(
      "moz-button[data-l10n-id='newtab-widget-timer-reset']"
    );

    assert.ok(resetBtn.exists(), "Reset buttons should be rendered");
    resetBtn.props().onClick();
    assert.equal(dispatch.getCall(0).args[0].type, at.WIDGETS_TIMER_RESET);

    const initialUserDuration = 12 * 60;

    const resetState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        focus: {
          duration: initialUserDuration,
          initialDuration: initialUserDuration,
          startTime: null,
          isRunning: false,
        },
      },
    };

    wrapper = mount(
      <WrapWithProvider state={resetState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    assert.equal(wrapper.find(".progress-circle-wrapper.visible").length, 0);
    const minutes = wrapper.find(".timer-set-minutes").text();
    const seconds = wrapper.find(".timer-set-seconds").text();
    assert.equal(minutes, "12");
    assert.equal(seconds, "00");
  });

  it("should dispatch pause and set type and when clicking the break timer", () => {
    const breakBtn = wrapper.find(
      "moz-button[data-l10n-id='newtab-widget-timer-mode-break']"
    );
    assert.ok(breakBtn.exists(), "break button should be rendered");
    breakBtn.props().onClick();

    const types = dispatch
      .getCalls()
      .map(call => call.args[0].type)
      .filter(Boolean);

    assert.ok(types.includes(at.WIDGETS_TIMER_PAUSE));
    assert.ok(types.includes(at.WIDGETS_TIMER_SET_TYPE));

    const findTypeToggled = dispatch
      .getCalls()
      .map(call => call.args[0])
      .find(action => action.type === at.WIDGETS_TIMER_SET_TYPE);

    assert.equal(
      findTypeToggled.data.timerType,
      "break",
      "timer should switch to break mode"
    );
  });

  it("should dispatch set type when clicking the focus timer", () => {
    const focusBtn = wrapper.find(
      "moz-button[data-l10n-id='newtab-widget-timer-mode-focus']"
    );
    assert.ok(focusBtn.exists(), "focus button should be rendered");
    focusBtn.props().onClick();

    const types = dispatch
      .getCalls()
      .map(call => call.args[0].type)
      .filter(Boolean);

    assert.ok(types.includes(at.WIDGETS_TIMER_PAUSE));
    assert.ok(types.includes(at.WIDGETS_TIMER_SET_TYPE));

    const findTypeToggled = dispatch
      .getCalls()
      .map(call => call.args[0])
      .find(action => action.type === at.WIDGETS_TIMER_SET_TYPE);

    assert.equal(
      findTypeToggled.data.timerType,
      "focus",
      "focus should switch to break mode"
    );
  });

  it("should toggle from focus to break timer automatically on end", () => {
    const now = Math.floor(Date.now() / 1000);

    const endState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        focus: {
          ...mockState.TimerWidget.break,
          isRunning: true,
          startTime: now - 300,
        },
      },
    };

    wrapper = mount(
      <WrapWithProvider state={endState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    // Let interval fire and start the timer_end logic
    clock.tick(1000);

    // Allowing time for the chained timeouts for animation
    clock.tick(3000);
    wrapper.update();

    const types = dispatch
      .getCalls()
      .map(call => call.args[0].type)
      .filter(Boolean);

    assert.ok(types.includes(at.WIDGETS_TIMER_END));
    assert.ok(types.includes(at.WIDGETS_TIMER_SET_TYPE));

    const findTypeToggled = dispatch
      .getCalls()
      .map(call => call.args[0])
      .find(action => action.type === at.WIDGETS_TIMER_SET_TYPE);

    assert.equal(
      findTypeToggled.data.timerType,
      "break",
      "timer should switch to break mode"
    );
  });

  it("should toggle from break to focus timer automatically on end", () => {
    const now = Math.floor(Date.now() / 1000);

    const endState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        timerType: "break",
        break: {
          ...mockState.TimerWidget.break,
          isRunning: true,
          startTime: now - 300,
        },
      },
    };

    wrapper = mount(
      <WrapWithProvider state={endState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    // Let interval fire and start the timer_end logic
    clock.tick(1000);

    // Allowing time for the chained timeouts for animation
    clock.tick(3000);
    wrapper.update();

    const types = dispatch.getCalls().map(call => call.args[0].type);

    assert.ok(types.includes(at.WIDGETS_TIMER_END));
    assert.ok(types.includes(at.WIDGETS_TIMER_SET_TYPE));

    const findTypeToggled = dispatch
      .getCalls()
      .map(call => call.args[0])
      .find(action => action.type === at.WIDGETS_TIMER_SET_TYPE);

    assert.equal(
      findTypeToggled.data.timerType,
      "focus",
      "timer should switch to focus mode"
    );
  });

  it("should pause when time input is focused", () => {
    const activeState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        timerType: "focus",
        focus: {
          ...mockState.TimerWidget.focus,
          isRunning: true,
        },
      },
    };

    const activeWrapper = mount(
      <WrapWithProvider state={activeState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    const minutesSpan = activeWrapper.find(".timer-set-minutes").at(0);
    assert.ok(minutesSpan.exists());

    minutesSpan.simulate("focus");
    assert.equal(dispatch.getCall(0).args[0].type, at.WIDGETS_TIMER_PAUSE);
  });

  it("should reset to user's initial duration after timer ends", () => {
    const now = Math.floor(Date.now() / 1000);

    // mock up a user's initial duration (12 minutes)
    const initialUserDuration = 12 * 60;

    const endState = {
      ...mockState,
      TimerWidget: {
        ...mockState.TimerWidget,
        timerType: "focus",
        focus: {
          duration: initialUserDuration,
          initialDuration: initialUserDuration,
          startTime: now - initialUserDuration,
          isRunning: true,
        },
      },
    };

    wrapper = mount(
      <WrapWithProvider state={endState}>
        <FocusTimer
          dispatch={dispatch}
          handleUserInteraction={handleUserInteraction}
        />
      </WrapWithProvider>
    );

    // Let interval fire and start the timer_end logic
    clock.tick(1000);

    // Allowing time for the chained timeouts for animation
    clock.tick(3000);
    wrapper.update();

    const endCall = dispatch
      .getCalls()
      .map(call => call.args[0])
      .find(action => action.type === at.WIDGETS_TIMER_END);

    assert.ok(
      endCall,
      "WIDGETS_TIMER_END should be dispatched when timer runs out"
    );
    assert.equal(
      endCall.data.duration,
      initialUserDuration,
      "timer should restore to user's initial input"
    );

    assert.equal(
      endCall.data.initialDuration,
      initialUserDuration,
      "initialDuration should also be restored to user's initial input"
    );
  });

  describe("context menu", () => {
    it("should render default context menu", () => {
      assert.ok(wrapper.find(".focus-timer-context-menu-button").exists());
      assert.ok(wrapper.find("#focus-timer-context-menu").exists());

      // "Turn notifications off" option
      assert.ok(
        wrapper
          .find(
            "panel-item[data-l10n-id='newtab-widget-timer-menu-notifications']"
          )
          .exists()
      );

      assert.ok(
        wrapper
          .find("panel-item[data-l10n-id='newtab-widget-timer-menu-hide']")
          .exists()
      );

      assert.ok(
        wrapper
          .find(
            "panel-item[data-l10n-id='newtab-widget-timer-menu-learn-more']"
          )
          .exists()
      );

      // Make sure "Turn notifications on" is not there
      assert.isFalse(
        wrapper.contains(
          "panel-item[data-l10n-id='newtab-widget-timer-menu-notifications-on']"
        )
      );
    });

    it("should render context menu with 'turn notifications on' if notifications are disabled", () => {
      const noNotificationsState = {
        ...mockState,
        Prefs: {
          ...INITIAL_STATE.Prefs,
          values: {
            ...INITIAL_STATE.Prefs.values,
            [PREF_WIDGETS_SYSTEM_NOTIFICATIONS_ENABLED]: false,
          },
        },
      };

      wrapper = mount(
        <WrapWithProvider state={noNotificationsState}>
          <FocusTimer
            dispatch={dispatch}
            handleUserInteraction={handleUserInteraction}
          />
        </WrapWithProvider>
      );

      // "Turn notifications on" option
      assert.ok(
        wrapper
          .find(
            "panel-item[data-l10n-id='newtab-widget-timer-menu-notifications-on']"
          )
          .exists()
      );

      // Make sure "Turn notifications off" is not there
      assert.isFalse(
        wrapper.contains(
          "panel-item[data-l10n-id='newtab-widget-timer-menu-notifications']"
        )
      );
    });

    it("should turn off notifications when the 'Turn off notifications' option is clicked", () => {
      const menuItem = wrapper.find(
        "panel-item[data-l10n-id='newtab-widget-timer-menu-notifications']"
      );
      menuItem.props().onClick();

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

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

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

    it("should dispatch OPEN_LINK when the Learn More option is clicked", () => {
      const menuItem = wrapper.find(
        "panel-item[data-l10n-id='newtab-widget-timer-menu-learn-more']"
      );
      menuItem.props().onClick();

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

  // Tests for the focus timer input. It should only allow numbers
  describe("isNumericValue", () => {
    it("should return true for single digit numbers", () => {
      assert.isTrue(isNumericValue("0"));
      assert.isTrue(isNumericValue("1"));
      assert.isTrue(isNumericValue("5"));
      assert.isTrue(isNumericValue("9"));
    });

    it("should return true for multi-digit numbers", () => {
      assert.isTrue(isNumericValue("10"));
      assert.isTrue(isNumericValue("25"));
      assert.isTrue(isNumericValue("99"));
    });

    it("should return false for non-numeric characters", () => {
      assert.isFalse(isNumericValue("a"));
      assert.isFalse(isNumericValue("Z"));
      assert.isFalse(isNumericValue("!"));
      assert.isFalse(isNumericValue("@"));
      assert.isFalse(isNumericValue(" "));
    });

    it("should return false for special characters", () => {
      assert.isFalse(isNumericValue("-"));
      assert.isFalse(isNumericValue("+"));
      assert.isFalse(isNumericValue("."));
      assert.isFalse(isNumericValue(","));
    });

    it("should return false for mixed alphanumeric strings", () => {
      assert.isFalse(isNumericValue("1a"));
      assert.isFalse(isNumericValue("a1"));
      assert.isFalse(isNumericValue("5x"));
    });

    it("should return false for empty string", () => {
      assert.isFalse(isNumericValue(" "));
    });
  });

  // Tests for the 2-character limit (enforces max 99 minutes, 59 seconds)
  describe("isAtMaxLength", () => {
    it("should return false for empty string", () => {
      assert.isFalse(isAtMaxLength(""));
    });

    it("should return false for single character", () => {
      assert.isFalse(isAtMaxLength("5"));
      assert.isFalse(isAtMaxLength("9"));
    });

    it("should return true for 2 characters", () => {
      assert.isTrue(isAtMaxLength("25"));
      assert.isTrue(isAtMaxLength("99"));
      assert.isTrue(isAtMaxLength("00"));
    });

    it("should return true for more than 2 characters", () => {
      assert.isTrue(isAtMaxLength("123"));
      assert.isTrue(isAtMaxLength("999"));
    });
  });

  it("should clamp minutes to 99 and seconds to 59 when setting duration", () => {
    // Find the editable fields
    const minutes = wrapper.find(".timer-set-minutes").at(0);
    const seconds = wrapper.find(".timer-set-seconds").at(0);

    // Simulate user typing values beyond limits
    minutes.getDOMNode().innerText = "100";
    seconds.getDOMNode().innerText = "85";

    // Trigger blur, which calls setTimerDuration()
    seconds.simulate("blur");

    // Clamp check
    const clampedMinutes = Math.min(
      parseInt(minutes.getDOMNode().innerText, 10),
      99
    );
    const clampedSeconds = Math.min(
      parseInt(seconds.getDOMNode().innerText, 10),
      59
    );

    assert.equal(clampedMinutes, 99, "minutes should be clamped to 99");
    assert.equal(clampedSeconds, 59, "seconds should be clamped to 59");
  });
});
