"use strict";

const { BaseAction } = ChromeUtils.importESModule(
  "resource://normandy/actions/BaseAction.sys.mjs"
);

class NoopAction extends BaseAction {
  constructor() {
    super();
    this._testPreExecutionFlag = false;
    this._testRunFlag = false;
    this._testFinalizeFlag = false;
  }

  _preExecution() {
    this._testPreExecutionFlag = true;
  }

  _run() {
    this._testRunFlag = true;
  }

  _finalize() {
    this._testFinalizeFlag = true;
  }
}

NoopAction._errorToThrow = new Error("test error");

class FailPreExecutionAction extends NoopAction {
  _preExecution() {
    throw NoopAction._errorToThrow;
  }
}

class FailRunAction extends NoopAction {
  _run() {
    throw NoopAction._errorToThrow;
  }
}

class FailFinalizeAction extends NoopAction {
  _finalize() {
    throw NoopAction._errorToThrow;
  }
}

// Test that constructor and override methods are run
decorate_task(async () => {
  let action = new NoopAction();
  is(
    action._testPreExecutionFlag,
    false,
    "_preExecution should not have been called on a new action"
  );
  is(
    action._testRunFlag,
    false,
    "_run has should not have been called on a new action"
  );
  is(
    action._testFinalizeFlag,
    false,
    "_finalize should not be called on a new action"
  );

  const recipe = recipeFactory();
  await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
  is(
    action._testPreExecutionFlag,
    true,
    "_preExecution should be called when a recipe is executed"
  );
  is(
    action._testRunFlag,
    true,
    "_run should be called when a recipe is executed"
  );
  is(
    action._testFinalizeFlag,
    false,
    "_finalize should not have been called when a recipe is executed"
  );

  await action.finalize();
  is(
    action._testFinalizeFlag,
    true,
    "_finalizeExecution should be called when finalize was called"
  );

  action = new NoopAction();
  await action.finalize();
  is(
    action._testPreExecutionFlag,
    true,
    "_preExecution should be called when finalized even if no recipes"
  );
  is(
    action._testRunFlag,
    false,
    "_run should be called if no recipes were run"
  );
  is(
    action._testFinalizeFlag,
    true,
    "_finalize should be called when finalized"
  );
});

// Recipes can't be run after finalize is called
decorate_task(async function () {
  const action = new NoopAction();
  const recipe1 = recipeFactory();
  const recipe2 = recipeFactory();

  await action.processRecipe(recipe1, BaseAction.suitability.FILTER_MATCH);
  await action.finalize();

  await Assert.rejects(
    action.processRecipe(recipe2, BaseAction.suitability.FILTER_MATCH),
    /^Error: Action has already been finalized$/,
    "running recipes after finalization is an error"
  );
});

// Test an action with a failing pre-execution step
decorate_task(async function () {
  const recipe = recipeFactory();
  const action = new FailPreExecutionAction();
  is(
    action.state,
    FailPreExecutionAction.STATE_PREPARING,
    "Pre-execution should not happen immediately"
  );

  // Should fail, putting the action into a "failed" state, but the entry
  // point `processRecipe` should not itself throw an exception.
  await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
  is(
    action.state,
    FailPreExecutionAction.STATE_FAILED,
    "Action fails if pre-execution fails"
  );
  is(
    action.lastError,
    NoopAction._errorToThrow,
    "The thrown error should be stored in lastError"
  );

  // Should not throw, even though the action is in a disabled state.
  await action.finalize();
  is(
    action.state,
    FailPreExecutionAction.STATE_FINALIZED,
    "Action should be finalized"
  );
  is(
    action.lastError,
    NoopAction._errorToThrow,
    "lastError should not have changed"
  );

  is(action._testRunFlag, false, "_run should not have been called");
  is(action._testFinalizeFlag, false, "_finalize should not have been called");
});

// Test an action with a failing recipe step
decorate_task(async function () {
  const recipe = recipeFactory();
  const action = new FailRunAction();
  await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
  is(
    action.state,
    FailRunAction.STATE_READY,
    "Action should not be marked as failed due to a recipe failure"
  );
  await action.finalize();
  is(
    action.state,
    FailRunAction.STATE_FINALIZED,
    "Action should be marked as finalized after finalize is called"
  );

  ok(action._testFinalizeFlag, "_finalize should have been called");
});

// Disable disables an action
decorate_task(async function () {
  const recipe = recipeFactory();
  const action = new NoopAction();

  action.disable();
  is(
    action.state,
    NoopAction.STATE_DISABLED,
    "Action should be marked as disabled"
  );

  // Should not throw, even though the action is disabled
  await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);

  // Should not throw, even though the action is disabled
  await action.finalize();

  is(action._testRunFlag, false, "_run should not have been called");
  is(action._testFinalizeFlag, false, "_finalize should not have been called");
});

// If the capabilities don't match, processRecipe shouldn't validate the arguments
decorate_task(async function () {
  const recipe = recipeFactory();
  const action = new NoopAction();
  const verifySpy = sinon.spy(action, "validateArguments");
  await action.processRecipe(
    recipe,
    BaseAction.suitability.CAPABILITIES_MISMATCH
  );
  ok(!verifySpy.called, "validateArguments should not be called");
});
