define([
    'bitbucket/internal/util/markup'
], function(
    markupUtil
) {

    'use strict';

    //
    // NOTE:
    //
    // The Return Symbol (⏎) is used instead of a \n to denote newlines.
    // This makes for much easier character counting to work out the expected token positions

    //
    // NOTE:
    //
    // In many of these tests a vertical bar has been inserted in strings so that
    // a position or set of positions can be automatically found and tested.

    module('util-markup');

    module('isPositionInCodeBlock');

    /**
     * find all indices of vertical bars in the given string
     * If a single index is found, returns it, otherwise returns an array
     *
     * @param {string} str
     * @returns {number|Array}
     */
    function getPositions(str) {
        var pos = 0;
        var positions = [];
        while (pos !== -1) {
            pos = str.indexOf('|', pos + 1);
            if (pos !== -1) {
                positions.push(pos);
            }
        }
        if (positions.length === 1) {
            return positions[0];
        }
        return positions;
    }

    /**
     * Replace a Return symbol (⏎) with a \n
     *
     * @param {string} str
     * @returns {string}
     */
    function nlify(str) {
        return str.replace(/⏎/g, '\n');
    }

    test('find the position in an inline code block', function(assert) {

        var codeBlock = "`This is a| code block`";

        assert.ok(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), 'Single position');

        // multpiple code blocks in a block of text
        codeBlock = "one: `|` two: `|`";
        getPositions(codeBlock).forEach(function(position, i) {
            assert.ok(markupUtil.isPositionInsideCodeBlock(position, codeBlock), '(' + i + ') Multiple positions (' + position + ')');
        });

        // an unfortunate situation - you have a single backtick and now the rest of the line is considered in a code block
        codeBlock = "foo ` | bar";
        assert.ok(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), 'Position after single backtick is "inside code block"');

        // a backtick can be escaped to not invoke this behaviour
        codeBlock = "foo \\` | bar \\` | foo";
        getPositions(codeBlock).forEach(function(position, i) {
            assert.notOk(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), '(' + i + ') Position after single escaped backtick is "not inside code block"');
        });

        // inline blocks are also terminated by a newline
        codeBlock = nlify("foo `inline block ⏎ |");
        assert.notOk(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), 'newline closes inline code block');

    });

    test('find the position in a fenced code block', function(assert) {

        var codeBlock = nlify("```⏎This is a| fenced code block⏎```");

        assert.ok(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), 'Single position');

        // annotated fenced code blocks
        codeBlock = nlify("```javascript⏎foo| bar⏎```");
        assert.ok(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), 'Annotated fenced code block');

        // alternate fenced code blocks syntax
        codeBlock = nlify("~~~javascript⏎foo| bar⏎~~~");
        assert.ok(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), 'alternate syntax fenced code block');

        // can't mix syntax
        codeBlock = nlify("```javascript⏎foo| bar⏎~~~⏎ | baz⏎```");
        getPositions(codeBlock).forEach(function(position, i) {
            assert.ok(markupUtil.isPositionInsideCodeBlock(position, codeBlock), '(' + i + ') No mixed syntax for closing a code block');
        });

        // fenced code blocks must start on a new line BUT it's fine to use multiple codeblock
        // tokens in a row (i.e. the following code block is valid, it just isn't a fenced code block)
        // but only by virtue of the odd number of tokens on either side
        codeBlock = "foo ``` hello | ```";
        assert.ok(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), '"inline fenced code blocks" e.g. "hello ``` world ```" is valid');

        // code blocks must start on a new line
        codeBlock = nlify("foo ```⏎ hello | ⏎```⏎");
        assert.notOk(markupUtil.isPositionInsideCodeBlock(getPositions(codeBlock), codeBlock), 'Code blocks must start on a new line (wrong newlines)');

        // multiple and mixed syntax blocks
        codeBlock = nlify("1: ⏎```⏎foo |⏎```⏎⏎2: ⏎~~~⏎ foo | ⏎~~~⏎ 3: ⏎```⏎ foo | ⏎```");
        getPositions(codeBlock).forEach(function(position, i) {
            assert.ok(markupUtil.isPositionInsideCodeBlock(position, codeBlock), '(' + i + ') multiple and mixed syntax blocks');
        });

    });

    test('find the cursor in mixed code blocks', function(assert) {
        var codeBlock = nlify("```⏎This is a| fenced code block⏎```⏎ This is an `inline | code block`");

        getPositions(codeBlock).forEach(function(position, i) {
            assert.ok(markupUtil.isPositionInsideCodeBlock(position, codeBlock), '(' + i + ') Simple mixed blocks');
        });

        codeBlock = nlify("`inline | block`⏎```⏎ fenced | ⏎```");
        getPositions(codeBlock).forEach(function(position, i) {
            assert.ok(markupUtil.isPositionInsideCodeBlock(position, codeBlock), '(' + i + ') Simple mixed blocks reversed');
        });

        // A position *after* the inline code block inside the fenced block should still be inside a code block
        // because a fenced code block waits until it is closed because it allows inline blocks to be recognised
        codeBlock = nlify("```⏎This is a| fenced code block and This is an `inline | code block` inside a | fenced block ⏎```⏎");
        getPositions(codeBlock).forEach(function(position, i) {
            assert.ok(markupUtil.isPositionInsideCodeBlock(position, codeBlock), '(' + i + ') Find position in nested blocks (nested inline block)');
        });

        // An alternate syntax code block should be ignored so the third position on each line should be found
        codeBlock = nlify("```⏎This is a| fenced code ⏎block and ⏎~~~⏎alternate syntax | code block⏎~~~⏎ inside a | fenced block ⏎```⏎" +
            "~~~⏎This is a| fenced code ⏎block and ⏎```⏎alternate syntax | code block⏎```⏎ inside a | fenced block ⏎~~~⏎");
        getPositions(codeBlock).forEach(function(position, i) {
            assert.ok(markupUtil.isPositionInsideCodeBlock(position, codeBlock), '(' + i + ') Find position in nested blocks (nested fenced block)');
        });

    });

    module('codeBlockTokenizer');

    function token(start, end, value, type, blockType) {
        return {
            start: start,
            end: end,
            value: value,
            type: type,
            blockType: blockType
        };
    }

    // Utility functions for creating various tokens
    function inlineStartToken(start, end, value) { return token(start, end, value || '`', markupUtil.TokenTypes.CODE_START, markupUtil.BlockTypes.INLINE); }
    function inlineEndToken(start, end, value) { return token(start, end, value || '`', markupUtil.TokenTypes.CODE_END, markupUtil.BlockTypes.INLINE); }
    function fencedStartToken(start, end, value) { return token(start, end, value || '```', markupUtil.TokenTypes.CODE_START, markupUtil.BlockTypes.FENCED); }
    function fencedEndToken(start, end, value) { return token(start, end, value || '```', markupUtil.TokenTypes.CODE_END, markupUtil.BlockTypes.FENCED); }

    test('check simple inline tokens', function(assert) {
        // A single inline code block
        var codeBlock = "`123`";
        var expectedTokens = [
            inlineStartToken(0, 1),
            inlineEndToken(4, 5)
        ];
        var tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for a simple inline block');

        // Multiple inline blocks
        codeBlock = "`123` `456`";
        expectedTokens = [
            inlineStartToken(0, 1),
            inlineEndToken(4, 5),
            inlineStartToken(6, 7),
            inlineEndToken(10, 11)
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for multiple inline blocks');

        // Multiple inline blocks and newlines
        codeBlock = nlify("`123` ⏎`45⏎6`");
        expectedTokens = [
            inlineStartToken(0, 1),
            inlineEndToken(4, 5),
            inlineStartToken(7, 8),
            inlineEndToken(10, 11, '\n'),
            inlineStartToken(12, 13)
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for multiple inline blocks that are terminated early by a newline');

        // Multiple inline blocks
        codeBlock = "`123` ~456~";
        expectedTokens = [
            inlineStartToken(0, 1),
            inlineEndToken(4, 5)
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokenizer ignores inline ~ blocks');
    });

    test('simple fenced blocks', function(assert) {
        var codeBlock = nlify("```⏎123⏎```");
        var expectedTokens = [
            fencedStartToken(0, 3),
            fencedEndToken(8, 11)
        ];
        var tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for a single fenced block');

        codeBlock = nlify("```⏎123⏎```⏎ foo ⏎```⏎456⏎```");
        expectedTokens = [
            fencedStartToken(0, 3),
            fencedEndToken(8, 11),
            fencedStartToken(18, 21),
            fencedEndToken(26, 29)
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for multiple fenced blocks');

        codeBlock = nlify("```javascript⏎123⏎```");
        expectedTokens = [
            fencedStartToken(0, 13, '```javascript'),
            fencedEndToken(18, 21)
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for a single fenced block with annotation');

    });

    test('mixed blocks', function(assert) {
        // mixed fenced / inline block and inverse
        var codeBlock = nlify("```⏎foo⏎```⏎⏎`Hello world`");
        var expectedTokens = [
            fencedStartToken(0, 3),
            fencedEndToken(8, 11),
            inlineStartToken(13, 14),
            inlineEndToken(25, 26)
        ];
        var tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for a fenced + inline code block');

        codeBlock = nlify("`Hello world`⏎⏎```⏎foo⏎```");
        expectedTokens = [
            inlineStartToken(0, 1),
            inlineEndToken(12, 13),
            fencedStartToken(15, 18),
            fencedEndToken(23, 26)
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for an inline + fenced block');

        // An inline block inside a fenced block will be ignored
        codeBlock = nlify("```⏎foo `bar`⏎```");
        expectedTokens = [
            fencedStartToken(0, 3),
            fencedEndToken(14, 17)
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for an inline block nested in a fenced block');

        // a fenced block is not ended when a different token is used to close it.
        // i.e. ``` does not close a ~~~ block
        codeBlock = nlify("~~~⏎foo⏎```⏎~~~");
        expectedTokens = [
            fencedStartToken(0, 3, '~~~'),
            fencedEndToken(12, 15, '~~~')
        ];
        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for an alternate fenced token nested in a fenced block');

        // Some mixing and matching of token types and inline blocks
        codeBlock = nlify("Hello `<-yup`<-yup. Foo `<-yup⏎<-yup````<-nope.⏎```⏎<-yup⏎~~~⏎<-nope⏎```⏎<-yup");
        expectedTokens = [
            inlineStartToken(6, 7),
            inlineEndToken(12, 13),
            inlineStartToken(24, 25),
            inlineEndToken(30, 31, '\n'),
            inlineStartToken(36, 37),
            inlineEndToken(37, 38),
            inlineStartToken(38, 39),
            inlineEndToken(39, 40),
            fencedStartToken(48, 51),
            fencedEndToken(69, 72)
        ];

        tokens = markupUtil.codeBlockTokenizer(codeBlock);
        assert.deepEqual(tokens, expectedTokens, 'tokens are correct for a mix of inline and fenced blocks');

    });

    module('findNextTokenIndex');

    test('it finds the tokens at the correct position', function(assert) {
        var str = 'Hello world!';
        var token = '!';
        var tokenPos = 11;
        assert.equal(markupUtil.findNextTokenIndex(str, token), tokenPos, 'Found with default opts (no start and end)');
        assert.equal(markupUtil.findNextTokenIndex(str, token, 5), tokenPos, 'Found with a start that is < token position');
        assert.equal(markupUtil.findNextTokenIndex(str, token, null), tokenPos, 'Found with null start');
        assert.equal(markupUtil.findNextTokenIndex(str, token, str.length + 1), null, 'Null when start > str.length');
        assert.equal(markupUtil.findNextTokenIndex(str, token, null, 5), null, 'Null when end < token position');
        assert.equal(markupUtil.findNextTokenIndex(str, token, null, str.length + 1), tokenPos, 'Found with end > str.length');
    });
});
