asynctest(
  'browser.tinymce.core.keyboard.BoundaryLocationTest',
  [
    'ephox.agar.api.Assertions',
    'ephox.agar.api.GeneralSteps',
    'ephox.agar.api.Logger',
    'ephox.agar.api.Pipeline',
    'ephox.agar.api.Step',
    'ephox.katamari.api.Fun',
    'ephox.sugar.api.dom.Hierarchy',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.Selectors',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.keyboard.BoundaryLocation',
    'tinymce.core.test.ViewBlock',
    'tinymce.core.text.Zwsp'
  ],
  function (Assertions, GeneralSteps, Logger, Pipeline, Step, Fun, Hierarchy, Element, Selectors, CaretPosition, BoundaryLocation, ViewBlock, Zwsp) {
    var success = arguments[arguments.length - 2];
    var failure = arguments[arguments.length - 1];
    var ZWSP = Zwsp.ZWSP;
    var viewBlock = ViewBlock();

    var createViewElement = function (html) {
      viewBlock.update(html);
      return Element.fromDom(viewBlock.get());
    };

    var createLocation = function (elm, elementPath, offset) {
      var container = Hierarchy.follow(elm, elementPath);
      var pos = new CaretPosition(container.getOrDie().dom(), offset);
      var location = BoundaryLocation.readLocation(elm.dom(), pos);
      return location;
    };

    var createPosition = function (elm, elementPath, offset) {
      var container = Hierarchy.follow(elm, elementPath);
      return new CaretPosition(container.getOrDie().dom(), offset);
    };

    var locationName = function (location) {
      return location.fold(
        Fun.constant('before'),
        Fun.constant('start'),
        Fun.constant('end'),
        Fun.constant('after')
      );
    };

    var locationElement = function (location) {
      return Element.fromDom(location.fold(
        Fun.identity,
        Fun.identity,
        Fun.identity,
        Fun.identity
      ));
    };

    var sTestValidLocation = function (html, elementPath, offset, expectedLocationName, expectedInline) {
      return Step.sync(function () {
        var elm = createViewElement(html);
        var location = createLocation(elm, elementPath, offset);
        Assertions.assertEq('Should be a valid location: ' + html, true, location.isSome());
        Assertions.assertEq('Should be expected location', expectedLocationName, locationName(location.getOrDie()));
        Assertions.assertDomEq('Should be expected element', Selectors.one(expectedInline, elm).getOrDie(), locationElement(location.getOrDie()));
      });
    };

    var sTestInvalidLocation = function (html, elementPath, offset) {
      return Step.sync(function () {
        var elm = createViewElement(html);
        var location = createLocation(elm, elementPath, offset);
        Assertions.assertEq('Should not be a valid location: ' + html, true, location.isNone());
      });
    };

    var sTestFindLocation = function (forward, html, elementPath, offset, expectedLocationName, expectedInline) {
      return Step.sync(function () {
        var elm = createViewElement(html);
        var position = createPosition(elm, elementPath, offset);
        var location = forward ? BoundaryLocation.nextLocation(elm.dom(), position) : BoundaryLocation.prevLocation(elm.dom(), position);

        Assertions.assertDomEq('Should be expected element', Selectors.one(expectedInline, elm).getOrDie(), locationElement(location.getOrDie()));
        Assertions.assertEq('Should be a valid location: ' + html, true, location.isSome());
        Assertions.assertEq('Should be expected location', expectedLocationName, locationName(location.getOrDie()));
      });
    };

    var sTestFindLocationInvalid = function (forward, html, elementPath, offset) {
      return Step.sync(function () {
        var elm = createViewElement(html);
        var position = createPosition(elm, elementPath, offset);
        var location = forward ? BoundaryLocation.nextLocation(elm.dom(), position) : BoundaryLocation.prevLocation(elm.dom(), position);
        Assertions.assertEq('Should not be a valid location: ' + html, true, location.isNone());
      });
    };

    var sTestPrevLocation = Fun.curry(sTestFindLocation, false);
    var sTestNextLocation = Fun.curry(sTestFindLocation, true);
    var sTestPrevLocationInvalid = Fun.curry(sTestFindLocationInvalid, false);
    var sTestNextLocationInvalid = Fun.curry(sTestFindLocationInvalid, true);

    var sTestValidLocations = Logger.t('sTestValidLocations', GeneralSteps.sequence([
      sTestValidLocation('<p><a href="a">a</a></p>', [0], 0, 'before', 'a'),
      sTestValidLocation('<p><a href="a">a</a></p>', [0, 0, 0], 0, 'start', 'a'),
      sTestValidLocation('<p><a href="a">a</a></p>', [0, 0, 0], 1, 'end', 'a'),
      sTestValidLocation('<p><a href="a">a</a></p>', [0], 1, 'after', 'a'),
      sTestValidLocation('<p>a<a href="a">a</a></p>', [0, 0], 1, 'before', 'a'),
      sTestValidLocation('<p><a href="a">a</a>a</p>', [0, 1], 0, 'after', 'a'),
      sTestValidLocation('<p><a href="a">ab</a></p>', [0, 0, 0], 0, 'start', 'a'),
      sTestValidLocation('<p><a href="a">ab</a></p>', [0, 0, 0], 2, 'end', 'a'),
      sTestValidLocation('<p><img src="a"><a href="a">a</a></p>', [0], 1, 'before', 'a'),
      sTestValidLocation('<p><a href="a"><img src="a"></a></p>', [0, 0], 0, 'start', 'a'),
      sTestValidLocation('<p><a href="a"><img src="a"></a></p>', [0, 0], 1, 'end', 'a'),
      sTestValidLocation('<p><a href="a">a</a><img src="a"></p>', [0], 1, 'after', 'a'),
      sTestValidLocation('<p><a href="a">a</a></p><p><a href="b">b</a></p>', [0], 1, 'after', 'a'),
      sTestValidLocation('<p><a href="a">a</a></p><p><a href="b">b</a></p>', [1], 0, 'before', 'p:nth-child(2) a')
    ]));

    var sTestValidZwspLocations = Logger.t('sTestValidZwspLocations', GeneralSteps.sequence([
      sTestValidLocation('<p>' + ZWSP + '<a href="a">a</a></p>', [0, 0], 0, 'before', 'a'),
      sTestValidLocation('<p><a href="a">' + ZWSP + 'a</a></p>', [0, 0, 0], 1, 'start', 'a'),
      sTestValidLocation('<p><a href="a">a' + ZWSP + '</a></p>', [0, 0, 0], 1, 'end', 'a'),
      sTestValidLocation('<p><a href="a">a</a>' + ZWSP + '</p>', [0, 1], 1, 'after', 'a')
    ]));

    var sTestInvalidLocations = Logger.t('sTestInvalidLocations', GeneralSteps.sequence([
      sTestInvalidLocation('<p>a</p>', [0, 0], 0),
      sTestInvalidLocation('<p><b>a</b></p>', [0], 0),
      sTestInvalidLocation('<p><b>a</b></p>', [0], 1),
      sTestInvalidLocation('<p>a<a href="a">a</a>b</p>', [0, 0], 0),
      sTestInvalidLocation('<p>a<a href="a">a</a>b</p>', [0, 2], 1),
      sTestInvalidLocation('<p><img src="a"><a href="a">a</a></p>', [0], 0),
      sTestInvalidLocation('<p><a href="a">a</a><img src="a"></p>', [0], 2),
      sTestInvalidLocation('<p><a href="a"><img src="a"><img src="a"></a><img src="a"></p>', [0, 0], 1),
      sTestInvalidLocation('<p dir="rtl"><a href="a">a</a></p>', [0, 0, 0], 0),
      sTestInvalidLocation('<p><a href="a">\u05D4</a></p>', [0, 0, 0], 0)
    ]));

    var sTestPrevLocations = Logger.t('sTestPrevLocations', GeneralSteps.sequence([
      sTestPrevLocation('<p><a href="a">a</a>b</p>', [0, 1], 1, 'after', 'a'),
      sTestPrevLocation('<p><a href="a">a</a></p>', [0], 1, 'end', 'a'),
      sTestPrevLocation('<p><a href="a">a</a></p>', [0, 0, 0], 1, 'start', 'a'),
      sTestPrevLocation('<p><a href="a">a</a></p>', [0, 0, 0], 0, 'before', 'a'),
      sTestPrevLocation('<p><a href="a"><img src="about:blank"></a></p>', [0], 1, 'end', 'a'),
      sTestPrevLocation('<p><a href="a"><img src="about:blank"></a></p>', [0, 0], 1, 'start', 'a'),
      sTestPrevLocation('<p><a href="a"><img src="about:blank"></a></p>', [0, 0], 0, 'before', 'a')
    ]));

    var sTestPrevLocationsBetweenInlines = Logger.t('sTestPrevLocationsBetweenInlines', GeneralSteps.sequence([
      sTestPrevLocation('<p><a href="a">a</a><a href="b">b</a></p>', [0, 1, 0], 0, 'before', 'a:nth-child(2)')
    ]));

    var sTestPrevLocationsBetweenBlocks = Logger.t('sTestPrevLocationsBetweenBlocks', GeneralSteps.sequence([
      sTestPrevLocation('<p><a href="a">a</a></p><p><a href="b">b</a></p>', [1], 0, 'end', 'p:nth-child(1) a'),
      sTestPrevLocation('<p><a href="a">a</a></p><p><a href="b">b</a></p>', [1, 0, 0], 0, 'before', 'p:nth-child(2) a'),
      sTestPrevLocation('<p><a href="a">a</a>b</p><p><a href="c">c</a></p>', [1, 0, 0], 0, 'before', 'p:nth-child(2) a'),
      sTestPrevLocation('<p><a href="a">a</a><br /></p><p><a href="c">c</a></p>', [1], 0, 'after', 'p:nth-child(1) a'),
      sTestPrevLocationInvalid('<p><a href="a">a</a></p><p>b<a href="c">c</a></p>', [1, 0], 1),
      sTestPrevLocationInvalid('<p><a href="a">a</a>b</p><p><a href="c">c</a></p>', [1], 0)
    ]));

    var sTestPrevZwspLocations = Logger.t('sTestPrevLocations', GeneralSteps.sequence([
      sTestPrevLocation('<p><a href="a">a</a>' + ZWSP + 'b</p>', [0, 1], 2, 'after', 'a'),
      sTestPrevLocation('<p><a href="a">a</a>' + ZWSP + '</p>', [0, 1], 1, 'end', 'a'),
      sTestPrevLocation('<p><a href="a">a' + ZWSP + '</a></p>', [0, 0, 0], 1, 'start', 'a'),
      sTestPrevLocation('<p><a href="a">' + ZWSP + 'a</a></p>', [0, 0, 0], 1, 'before', 'a')
    ]));

    var sTestNextLocations = Logger.t('sTestNextLocations', GeneralSteps.sequence([
      sTestNextLocation('<p>a<a href="a">b</a></p>', [0, 0], 0, 'before', 'a'),
      sTestNextLocation('<p><a href="a">a</a></p>', [0], 0, 'start', 'a'),
      sTestNextLocation('<p><a href="a">a</a></p>', [0, 0, 0], 0, 'end', 'a'),
      sTestNextLocation('<p><a href="a">a</a></p>', [0, 0, 0], 1, 'after', 'a'),
      sTestNextLocation('<p><a href="a"><img src="about:blank"></a></p>', [0], 0, 'start', 'a'),
      sTestNextLocation('<p><a href="a"><img src="about:blank"></a></p>', [0, 0], 0, 'end', 'a'),
      sTestNextLocation('<p><a href="a"><img src="about:blank"></a></p>', [0, 0], 1, 'after', 'a')
    ]));

    var sTestNextLocationsBetweenInlines = Logger.t('sTestNextLocationsBetweenInlines', GeneralSteps.sequence([
      sTestNextLocation('<p><a href="a">a</a><a href="a">b</a></p>', [0, 0, 0], 1, 'after', 'a:nth-child(1)')
    ]));

    var sTestNextLocationsBetweenBlocks = Logger.t('sTestNextLocationsBetweenBlocks', GeneralSteps.sequence([
      sTestNextLocation('<p><a href="a">a</a></p><p><a href="b">b</a></p>', [0], 1, 'start', 'p:nth-child(2) a'),
      sTestNextLocation('<p><a href="a">a</a></p><p><a href="b">b</a></p>', [0, 0, 0], 1, 'after', 'p:nth-child(1) a'),
      sTestNextLocationInvalid('<p><a href="a">a</a>b</p><p><a href="c">c</a></p>', [0, 1], 0),
      sTestNextLocationInvalid('<p><a href="a">a</a></p><p>b<a href="c">c</a></p>', [0], 1)
    ]));

    var sTestNextZwspLocations = Logger.t('sTestNextZwspLocations', GeneralSteps.sequence([
      sTestNextLocation('<p>a' + ZWSP + '<a href="a">b</a></p>', [0, 0], 0, 'before', 'a'),
      sTestNextLocation('<p>' + ZWSP + '<a href="a">a</a></p>', [0], 0, 'start', 'a'),
      sTestNextLocation('<p><a href="a">' + ZWSP + 'a</a></p>', [0, 0, 0], 1, 'end', 'a'),
      sTestNextLocation('<p><a href="a">a' + ZWSP + '</a></p>', [0, 0, 0], 1, 'after', 'a')
    ]));

    viewBlock.attach();
    Pipeline.async({}, [
      sTestValidLocations,
      sTestValidZwspLocations,
      sTestInvalidLocations,
      sTestPrevLocations,
      sTestPrevLocationsBetweenInlines,
      sTestPrevLocationsBetweenBlocks,
      sTestPrevZwspLocations,
      sTestNextLocations,
      sTestNextLocationsBetweenInlines,
      sTestNextLocationsBetweenBlocks,
      sTestNextZwspLocations
    ], function () {
      viewBlock.detach();
      success();
    }, failure);
  }
);