/**
 * @fileoverview Tests for SourceCodeFixer.
 * @author Nicholas C. Zakas
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const assert = require("chai").assert,
    sinon = require("sinon"),
    SourceCodeFixer = require("../../../lib/linter/source-code-fixer");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const TEST_CODE = "var answer = 6 * 7;";
const INSERT_AT_END = {
        message: "End",
        fix: {
            range: [TEST_CODE.length, TEST_CODE.length],
            text: "// end"
        }
    },
    INSERT_AT_START = {
        message: "Start",
        fix: {
            range: [0, 0],
            text: "// start\n"
        }
    },
    INSERT_IN_MIDDLE = {
        message: "Multiply",
        fix: {
            range: [13, 13],
            text: "5 *"
        }
    },
    REPLACE_ID = {
        message: "foo",
        fix: {
            range: [4, 10],
            text: "foo"
        }
    },
    REPLACE_VAR = {
        message: "let",
        fix: {
            range: [0, 3],
            text: "let"
        }
    },
    REPLACE_NUM = {
        message: "5",
        fix: {
            range: [13, 14],
            text: "5"
        }
    },
    REMOVE_START = {
        message: "removestart",
        fix: {
            range: [0, 4],
            text: ""
        }
    },
    REMOVE_MIDDLE = {
        message: "removemiddle",
        fix: {
            range: [5, 10],
            text: ""
        }
    },
    REMOVE_END = {
        message: "removeend",
        fix: {
            range: [14, 18],
            text: ""
        }
    },
    NO_FIX = {
        message: "nofix"
    },
    INSERT_BOM = {
        message: "insert-bom",
        fix: {
            range: [0, 0],
            text: "\uFEFF"
        }
    },
    INSERT_BOM_WITH_TEXT = {
        message: "insert-bom",
        fix: {
            range: [0, 0],
            text: "\uFEFF// start\n"
        }
    },
    REMOVE_BOM = {
        message: "remove-bom",
        fix: {
            range: [-1, 0],
            text: ""
        }
    },
    REPLACE_BOM_WITH_TEXT = {
        message: "remove-bom",
        fix: {
            range: [-1, 0],
            text: "// start\n"
        }
    },
    NO_FIX1 = {
        message: "nofix1",
        line: 1,
        column: 3
    },
    NO_FIX2 = {
        message: "nofix2",
        line: 1,
        column: 7
    },
    REVERSED_RANGE = {
        message: "reversed range",
        fix: {
            range: [3, 0],
            text: " "
        }
    };

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

describe("SourceCodeFixer", () => {

    describe("constructor", () => {

        it("Should not be able to add anything to this", () => {
            const result = new SourceCodeFixer();

            assert.throws(() => {
                result.test = 1;
            });
        });
    });

    describe("applyFixes() with no BOM", () => {
        describe("shouldFix parameter", () => {
            it("Should not perform any fixes if 'shouldFix' is false", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END], false);

                assert.isFalse(result.fixed);
                assert.strictEqual(result.output, TEST_CODE);
            });

            it("Should perform fixes if 'shouldFix' is not provided", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END]);

                assert.isTrue(result.fixed);
            });

            it("should call a function provided as 'shouldFix' for each message", () => {
                const shouldFixSpy = sinon.spy();

                SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END], shouldFixSpy);
                assert.isTrue(shouldFixSpy.calledThrice);
            });

            it("should provide a message object as an argument to 'shouldFix'", () => {
                const shouldFixSpy = sinon.spy();

                SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);
                assert.strictEqual(shouldFixSpy.firstCall.args[0], INSERT_AT_START);
            });

            it("should not perform fixes if 'shouldFix' function returns false", () => {
                const shouldFixSpy = sinon.spy(() => false);
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);

                assert.isFalse(result.fixed);
            });

            it("should return original text as output if 'shouldFix' function prevents all fixes", () => {
                const shouldFixSpy = sinon.spy(() => false);
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);

                assert.strictEqual(result.output, TEST_CODE);
            });

            it("should only apply fixes for which the 'shouldFix' function returns true", () => {
                const shouldFixSpy = sinon.spy(problem => problem.message === "foo");
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START, REPLACE_ID], shouldFixSpy);

                assert.strictEqual(result.output, "var foo = 6 * 7;");
            });

            it("is called without access to internal eslint state", () => {
                const shouldFixSpy = sinon.spy();

                SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);

                assert.isUndefined(shouldFixSpy.thisValues[0]);
            });
        });

        describe("Text Insertion", () => {

            it("should insert text at the end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END]);

                assert.strictEqual(result.output, TEST_CODE + INSERT_AT_END.fix.text);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert text at the beginning of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START]);

                assert.strictEqual(result.output, INSERT_AT_START.fix.text + TEST_CODE);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert text in the middle of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_IN_MIDDLE]);

                assert.strictEqual(result.output, TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`));
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert text at the beginning, middle, and end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END]);

                assert.strictEqual(result.output, INSERT_AT_START.fix.text + TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`) + INSERT_AT_END.fix.text);
                assert.strictEqual(result.messages.length, 0);
            });


            it("should ignore reversed ranges", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REVERSED_RANGE]);

                assert.strictEqual(result.output, TEST_CODE);
            });

        });


        describe("Text Replacement", () => {

            it("should replace text at the end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_VAR]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, TEST_CODE.replace("var", "let"));
                assert.isTrue(result.fixed);
            });

            it("should replace text at the beginning of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, TEST_CODE.replace("answer", "foo"));
                assert.isTrue(result.fixed);
            });

            it("should replace text in the middle of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_NUM]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, TEST_CODE.replace("6", "5"));
                assert.isTrue(result.fixed);
            });

            it("should replace text at the beginning and end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID, REPLACE_VAR, REPLACE_NUM]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, "let foo = 5 * 7;");
                assert.isTrue(result.fixed);
            });

        });

        describe("Text Removal", () => {

            it("should remove text at the start of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_START]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, TEST_CODE.replace("var ", ""));
                assert.isTrue(result.fixed);
            });

            it("should remove text in the middle of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, TEST_CODE.replace("answer", "a"));
                assert.isTrue(result.fixed);
            });

            it("should remove text towards the end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_END]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, TEST_CODE.replace(" * 7", ""));
                assert.isTrue(result.fixed);
            });

            it("should remove text at the beginning, middle, and end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_END, REMOVE_START, REMOVE_MIDDLE]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, "a = 6;");
                assert.isTrue(result.fixed);
            });
        });

        describe("Combination", () => {

            it("should replace text at the beginning, remove text in the middle, and insert text at the end", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END, REMOVE_END, REPLACE_VAR]);

                assert.strictEqual(result.output, "let answer = 6;// end");
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should only apply one fix when ranges overlap", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE, REPLACE_ID]);

                assert.strictEqual(result.output, TEST_CODE.replace("answer", "foo"));
                assert.strictEqual(result.messages.length, 1);
                assert.strictEqual(result.messages[0].message, "removemiddle");
                assert.isTrue(result.fixed);
            });

            it("should apply one fix when the end of one range is the same as the start of a previous range overlap", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_START, REPLACE_ID]);

                assert.strictEqual(result.output, TEST_CODE.replace("var ", ""));
                assert.strictEqual(result.messages.length, 1);
                assert.strictEqual(result.messages[0].message, "foo");
                assert.isTrue(result.fixed);
            });

            it("should only apply one fix when ranges overlap and one message has no fix", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE, REPLACE_ID, NO_FIX]);

                assert.strictEqual(result.output, TEST_CODE.replace("answer", "foo"));
                assert.strictEqual(result.messages.length, 2);
                assert.strictEqual(result.messages[0].message, "nofix");
                assert.strictEqual(result.messages[1].message, "removemiddle");
                assert.isTrue(result.fixed);
            });

            it("should apply the same fix when ranges overlap regardless of order", () => {
                const result1 = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE, REPLACE_ID]);
                const result2 = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID, REMOVE_MIDDLE]);

                assert.strictEqual(result1.output, result2.output);
            });
        });

        describe("No Fixes", () => {

            it("should only apply one fix when ranges overlap and one message has no fix", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [NO_FIX]);

                assert.strictEqual(result.output, TEST_CODE);
                assert.strictEqual(result.messages.length, 1);
                assert.strictEqual(result.messages[0].message, "nofix");
                assert.isFalse(result.fixed);
            });

            it("should sort the no fix messages correctly", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID, NO_FIX2, NO_FIX1]);

                assert.strictEqual(result.output, TEST_CODE.replace("answer", "foo"));
                assert.strictEqual(result.messages.length, 2);
                assert.strictEqual(result.messages[0].message, "nofix1");
                assert.strictEqual(result.messages[1].message, "nofix2");
                assert.isTrue(result.fixed);
            });

        });

        describe("BOM manipulations", () => {

            it("should insert BOM with an insertion of '\uFEFF' at 0", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_BOM]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE}`);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert BOM with an insertion of '\uFEFFfoobar' at 0", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_BOM_WITH_TEXT]);

                assert.strictEqual(result.output, `\uFEFF// start\n${TEST_CODE}`);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should remove BOM with a negative range", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_BOM]);

                assert.strictEqual(result.output, TEST_CODE);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should replace BOM with a negative range and 'foobar'", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_BOM_WITH_TEXT]);

                assert.strictEqual(result.output, `// start\n${TEST_CODE}`);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

        });

    });

    /*
     * This section is almost same as "with no BOM".
     * Just `result.output` has BOM.
     */
    describe("applyFixes() with BOM", () => {

        const TEST_CODE_WITH_BOM = `\uFEFF${TEST_CODE}`;

        describe("Text Insertion", () => {

            it("should insert text at the end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_AT_END]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE}${INSERT_AT_END.fix.text}`);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert text at the beginning of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_AT_START]);

                assert.strictEqual(result.output, `\uFEFF${INSERT_AT_START.fix.text}${TEST_CODE}`);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert text in the middle of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_IN_MIDDLE]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`)}`);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert text at the beginning, middle, and end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END]);
                const insertInMiddle = TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`);

                assert.strictEqual(result.output, `\uFEFF${INSERT_AT_START.fix.text}${insertInMiddle}${INSERT_AT_END.fix.text}`);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should ignore reversed ranges", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REVERSED_RANGE]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE}`);
            });

        });

        describe("Text Replacement", () => {

            it("should replace text at the end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_VAR]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("var", "let")}`);
                assert.isTrue(result.fixed);
            });

            it("should replace text at the beginning of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_ID]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("answer", "foo")}`);
                assert.isTrue(result.fixed);
            });

            it("should replace text in the middle of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_NUM]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("6", "5")}`);
                assert.isTrue(result.fixed);
            });

            it("should replace text at the beginning and end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_ID, REPLACE_VAR, REPLACE_NUM]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, "\uFEFFlet foo = 5 * 7;");
                assert.isTrue(result.fixed);
            });

        });

        describe("Text Removal", () => {

            it("should remove text at the start of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_START]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("var ", "")}`);
                assert.isTrue(result.fixed);
            });

            it("should remove text in the middle of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("answer", "a")}`);
                assert.isTrue(result.fixed);
            });

            it("should remove text towards the end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_END]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace(" * 7", "")}`);
                assert.isTrue(result.fixed);
            });

            it("should remove text at the beginning, middle, and end of the code", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_END, REMOVE_START, REMOVE_MIDDLE]);

                assert.strictEqual(result.messages.length, 0);
                assert.strictEqual(result.output, "\uFEFFa = 6;");
                assert.isTrue(result.fixed);
            });
        });

        describe("Combination", () => {

            it("should replace text at the beginning, remove text in the middle, and insert text at the end", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_AT_END, REMOVE_END, REPLACE_VAR]);

                assert.strictEqual(result.output, "\uFEFFlet answer = 6;// end");
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should only apply one fix when ranges overlap", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE, REPLACE_ID]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("answer", "foo")}`);
                assert.strictEqual(result.messages.length, 1);
                assert.strictEqual(result.messages[0].message, "removemiddle");
                assert.isTrue(result.fixed);
            });

            it("should apply one fix when the end of one range is the same as the start of a previous range overlap", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_START, REPLACE_ID]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("var ", "")}`);
                assert.strictEqual(result.messages.length, 1);
                assert.strictEqual(result.messages[0].message, "foo");
                assert.isTrue(result.fixed);
            });

            it("should only apply one fix when ranges overlap and one message has no fix", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE, REPLACE_ID, NO_FIX]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE.replace("answer", "foo")}`);
                assert.strictEqual(result.messages.length, 2);
                assert.strictEqual(result.messages[0].message, "nofix");
                assert.strictEqual(result.messages[1].message, "removemiddle");
                assert.isTrue(result.fixed);
            });

            it("should apply the same fix when ranges overlap regardless of order", () => {
                const result1 = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE, REPLACE_ID]);
                const result2 = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_ID, REMOVE_MIDDLE]);

                assert.strictEqual(result1.output, result2.output);
            });

        });

        describe("No Fixes", () => {

            it("should only apply one fix when ranges overlap and one message has no fix", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [NO_FIX]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE}`);
                assert.strictEqual(result.messages.length, 1);
                assert.strictEqual(result.messages[0].message, "nofix");
                assert.isFalse(result.fixed);
            });

        });

        describe("BOM manipulations", () => {

            it("should insert BOM with an insertion of '\uFEFF' at 0", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_BOM]);

                assert.strictEqual(result.output, `\uFEFF${TEST_CODE}`);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should insert BOM with an insertion of '\uFEFFfoobar' at 0", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_BOM_WITH_TEXT]);

                assert.strictEqual(result.output, `\uFEFF// start\n${TEST_CODE}`);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should remove BOM with a negative range", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_BOM]);

                assert.strictEqual(result.output, TEST_CODE);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

            it("should replace BOM with a negative range and 'foobar'", () => {
                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_BOM_WITH_TEXT]);

                assert.strictEqual(result.output, `// start\n${TEST_CODE}`);
                assert.isTrue(result.fixed);
                assert.strictEqual(result.messages.length, 0);
            });

        });

    });

});
