/**
 * @fileoverview Tests for createReportTranslator
 * @author Teddy Katz
 */
"use strict";

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

const assert = require("chai").assert;
const { SourceCode } = require("../../../lib/source-code");
const espree = require("espree");
const createReportTranslator = require("../../../lib/linter/report-translator");

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

describe("createReportTranslator", () => {

    /**
     * Creates a SourceCode instance out of JavaScript text
     * @param {string} text Source text
     * @returns {SourceCode} A SourceCode instance for that text
     */
    function createSourceCode(text) {
        return new SourceCode(
            text,
            espree.parse(
                text.replace(/^\uFEFF/u, ""),
                {
                    loc: true,
                    range: true,
                    raw: true,
                    tokens: true,
                    comment: true
                }
            )
        );
    }

    let node, location, message, translateReport, suggestion1, suggestion2;

    beforeEach(() => {
        const sourceCode = createSourceCode("foo\nbar");

        node = sourceCode.ast.body[0];
        location = sourceCode.ast.body[1].loc.start;
        message = "foo";
        suggestion1 = "First suggestion";
        suggestion2 = "Second suggestion {{interpolated}}";
        translateReport = createReportTranslator({
            ruleId: "foo-rule",
            severity: 2,
            sourceCode,
            messageIds: {
                testMessage: message,
                suggestion1,
                suggestion2
            }
        });
    });

    describe("old-style call with location", () => {
        it("should extract the location correctly", () => {
            assert.deepStrictEqual(
                translateReport(node, location, message, {}),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement"
                }
            );
        });
    });

    describe("old-style call without location", () => {
        it("should use the start location and end location of the node", () => {
            assert.deepStrictEqual(
                translateReport(node, message, {}),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 1,
                    column: 1,
                    endLine: 1,
                    endColumn: 4,
                    nodeType: "ExpressionStatement"
                }
            );
        });
    });

    describe("new-style call with all options", () => {
        it("should include the new-style options in the report", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                fix: () => ({ range: [1, 2], text: "foo" }),
                suggest: [{
                    desc: "suggestion 1",
                    fix: () => ({ range: [2, 3], text: "s1" })
                }, {
                    desc: "suggestion 2",
                    fix: () => ({ range: [3, 4], text: "s2" })
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [1, 2],
                        text: "foo"
                    },
                    suggestions: [{
                        desc: "suggestion 1",
                        fix: { range: [2, 3], text: "s1" }
                    }, {
                        desc: "suggestion 2",
                        fix: { range: [3, 4], text: "s2" }
                    }]
                }
            );
        });

        it("should translate the messageId into a message", () => {
            const reportDescriptor = {
                node,
                loc: location,
                messageId: "testMessage",
                fix: () => ({ range: [1, 2], text: "foo" })
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    messageId: "testMessage",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [1, 2],
                        text: "foo"
                    }
                }
            );
        });

        it("should throw when both messageId and message are provided", () => {
            const reportDescriptor = {
                node,
                loc: location,
                messageId: "testMessage",
                message: "bar",
                fix: () => ({ range: [1, 2], text: "foo" })
            };

            assert.throws(
                () => translateReport(reportDescriptor),
                TypeError,
                "context.report() called with a message and a messageId. Please only pass one."
            );
        });

        it("should throw when an invalid messageId is provided", () => {
            const reportDescriptor = {
                node,
                loc: location,
                messageId: "thisIsNotASpecifiedMessageId",
                fix: () => ({ range: [1, 2], text: "foo" })
            };

            assert.throws(
                () => translateReport(reportDescriptor),
                TypeError,
                /^context\.report\(\) called with a messageId of '[^']+' which is not present in the 'messages' config:/u
            );
        });

        it("should throw when no message is provided", () => {
            const reportDescriptor = { node };

            assert.throws(
                () => translateReport(reportDescriptor),
                TypeError,
                "Missing `message` property in report() call; add a message that describes the linting problem."
            );
        });

        it("should support messageIds for suggestions and output resulting descriptions", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    messageId: "suggestion1",
                    fix: () => ({ range: [2, 3], text: "s1" })
                }, {
                    messageId: "suggestion2",
                    data: { interpolated: "'interpolated value'" },
                    fix: () => ({ range: [3, 4], text: "s2" })
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    suggestions: [{
                        messageId: "suggestion1",
                        desc: "First suggestion",
                        fix: { range: [2, 3], text: "s1" }
                    }, {
                        messageId: "suggestion2",
                        data: { interpolated: "'interpolated value'" },
                        desc: "Second suggestion 'interpolated value'",
                        fix: { range: [3, 4], text: "s2" }
                    }]
                }
            );
        });

        it("should throw when a suggestion defines both a desc and messageId", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "The description",
                    messageId: "suggestion1",
                    fix: () => ({ range: [2, 3], text: "s1" })
                }]
            };

            assert.throws(
                () => translateReport(reportDescriptor),
                TypeError,
                "context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one."
            );
        });

        it("should throw when a suggestion uses an invalid messageId", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    messageId: "noMatchingMessage",
                    fix: () => ({ range: [2, 3], text: "s1" })
                }]
            };

            assert.throws(
                () => translateReport(reportDescriptor),
                TypeError,
                /^context\.report\(\) called with a suggest option with a messageId '[^']+' which is not present in the 'messages' config:/u
            );
        });

        it("should throw when a suggestion does not provide either a desc or messageId", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    fix: () => ({ range: [2, 3], text: "s1" })
                }]
            };

            assert.throws(
                () => translateReport(reportDescriptor),
                TypeError,
                "context.report() called with a suggest option that doesn't have either a `desc` or `messageId`"
            );
        });

        it("should throw when a suggestion does not provide a fix function", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "The description",
                    fix: false
                }]
            };

            assert.throws(
                () => translateReport(reportDescriptor),
                TypeError,
                /^context\.report\(\) called with a suggest option without a fix function. See:/u
            );
        });
    });

    describe("combining autofixes", () => {
        it("should merge fixes to one if 'fix' function returns an array of fixes.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [1, 5],
                        text: "fooo\nbar"
                    }
                }
            );
        });

        it("should merge fixes to one if 'fix' function returns an iterator of fixes.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                *fix() {
                    yield { range: [1, 2], text: "foo" };
                    yield { range: [4, 5], text: "bar" };
                }
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [1, 5],
                        text: "fooo\nbar"
                    }
                }
            );
        });

        it("should respect ranges of empty insertions when merging fixes to one.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                *fix() {
                    yield { range: [4, 5], text: "cd" };
                    yield { range: [2, 2], text: "" };
                    yield { range: [7, 7], text: "" };
                }
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [2, 7],
                        text: "o\ncdar"
                    }
                }
            );
        });

        it("should pass through fixes if only one is present", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                fix: () => [{ range: [1, 2], text: "foo" }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [1, 2],
                        text: "foo"
                    }
                }
            );
        });

        it("should handle inserting BOM correctly.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [4, 5], text: "x" }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [0, 5],
                        text: "\uFEFFfoo\nx"
                    }
                }
            );
        });


        it("should handle removing BOM correctly.", () => {
            const sourceCode = createSourceCode("\uFEFFfoo\nbar");

            node = sourceCode.ast.body[0];

            const reportDescriptor = {
                node,
                message,
                fix: () => [{ range: [-1, 3], text: "foo" }, { range: [4, 5], text: "x" }]
            };

            assert.deepStrictEqual(
                createReportTranslator({ ruleId: "foo-rule", severity: 1, sourceCode })(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 1,
                    message: "foo",
                    line: 1,
                    column: 1,
                    endLine: 1,
                    endColumn: 4,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [-1, 5],
                        text: "foo\nx"
                    }
                }
            );
        });

        it("should throw an assertion error if ranges are overlapped.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [2, 5], text: "x" }]
            };

            assert.throws(
                translateReport.bind(null, reportDescriptor),
                "Fix objects must not be overlapped in a report."
            );
        });

        it("should include a fix passed as the last argument when location is passed", () => {
            assert.deepStrictEqual(
                translateReport(
                    node,
                    { line: 42, column: 23 },
                    "my message {{1}}{{0}}",
                    ["!", "testing"],
                    () => ({ range: [1, 1], text: "" })
                ),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "my message testing!",
                    line: 42,
                    column: 24,
                    nodeType: "ExpressionStatement",
                    fix: {
                        range: [1, 1],
                        text: ""
                    }
                }
            );
        });
    });

    describe("suggestions", () => {
        it("should support multiple suggestions.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "A first suggestion for the issue",
                    fix: () => [{ range: [1, 2], text: "foo" }]
                }, {
                    desc: "A different suggestion for the issue",
                    fix: () => [{ range: [1, 3], text: "foobar" }]
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    suggestions: [{
                        desc: "A first suggestion for the issue",
                        fix: { range: [1, 2], text: "foo" }
                    }, {
                        desc: "A different suggestion for the issue",
                        fix: { range: [1, 3], text: "foobar" }
                    }]
                }
            );
        });

        it("should merge suggestion fixes to one if 'fix' function returns an array of fixes.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "A suggestion for the issue",
                    fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }]
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    suggestions: [{
                        desc: "A suggestion for the issue",
                        fix: {
                            range: [1, 5],
                            text: "fooo\nbar"
                        }
                    }]
                }
            );
        });

        it("should remove the whole suggestion if 'fix' function returned `null`.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "A suggestion for the issue",
                    fix: () => null
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement"
                }
            );
        });

        it("should remove the whole suggestion if 'fix' function returned an empty array.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "A suggestion for the issue",
                    fix: () => []
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement"
                }
            );
        });

        it("should remove the whole suggestion if 'fix' function returned an empty sequence.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "A suggestion for the issue",
                    *fix() {}
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement"
                }
            );
        });

        // This isn't officially supported, but autofix works the same way
        it("should remove the whole suggestion if 'fix' function didn't return anything.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "A suggestion for the issue",
                    fix() {}
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement"
                }
            );
        });

        it("should keep suggestion before a removed suggestion.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "Suggestion with a fix",
                    fix: () => ({ range: [1, 2], text: "foo" })
                }, {
                    desc: "Suggestion without a fix",
                    fix: () => null
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    suggestions: [{
                        desc: "Suggestion with a fix",
                        fix: { range: [1, 2], text: "foo" }
                    }]
                }
            );
        });

        it("should keep suggestion after a removed suggestion.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "Suggestion without a fix",
                    fix: () => null
                }, {
                    desc: "Suggestion with a fix",
                    fix: () => ({ range: [1, 2], text: "foo" })
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    suggestions: [{
                        desc: "Suggestion with a fix",
                        fix: { range: [1, 2], text: "foo" }
                    }]
                }
            );
        });

        it("should remove multiple suggestions that didn't provide a fix and keep those that did.", () => {
            const reportDescriptor = {
                node,
                loc: location,
                message,
                suggest: [{
                    desc: "Keep #1",
                    fix: () => ({ range: [1, 2], text: "foo" })
                }, {
                    desc: "Remove #1",
                    fix() {
                        return null;
                    }
                }, {
                    desc: "Keep #2",
                    fix: () => ({ range: [1, 2], text: "bar" })
                }, {
                    desc: "Remove #2",
                    fix() {
                        return [];
                    }
                }, {
                    desc: "Keep #3",
                    fix: () => ({ range: [1, 2], text: "baz" })
                }, {
                    desc: "Remove #3",
                    *fix() {}
                }, {
                    desc: "Keep #4",
                    fix: () => ({ range: [1, 2], text: "quux" })
                }]
            };

            assert.deepStrictEqual(
                translateReport(reportDescriptor),
                {
                    ruleId: "foo-rule",
                    severity: 2,
                    message: "foo",
                    line: 2,
                    column: 1,
                    nodeType: "ExpressionStatement",
                    suggestions: [{
                        desc: "Keep #1",
                        fix: { range: [1, 2], text: "foo" }
                    }, {
                        desc: "Keep #2",
                        fix: { range: [1, 2], text: "bar" }
                    }, {
                        desc: "Keep #3",
                        fix: { range: [1, 2], text: "baz" }
                    }, {
                        desc: "Keep #4",
                        fix: { range: [1, 2], text: "quux" }
                    }]
                }
            );
        });
    });

    describe("message interpolation", () => {
        it("should correctly parse a message when being passed all options in an old-style report", () => {
            assert.deepStrictEqual(
                translateReport(node, node.loc.end, "hello {{dynamic}}", { dynamic: node.type }),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello ExpressionStatement",
                    nodeType: "ExpressionStatement",
                    line: 1,
                    column: 4
                }
            );
        });

        it("should correctly parse a message when being passed all options in a new-style report", () => {
            assert.deepStrictEqual(
                translateReport({ node, loc: node.loc.end, message: "hello {{dynamic}}", data: { dynamic: node.type } }),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello ExpressionStatement",
                    nodeType: "ExpressionStatement",
                    line: 1,
                    column: 4
                }
            );
        });

        it("should correctly parse a message with object keys as numbers", () => {
            assert.strictEqual(
                translateReport(node, "my message {{name}}{{0}}", { 0: "!", name: "testing" }).message,
                "my message testing!"
            );
        });

        it("should correctly parse a message with array", () => {
            assert.strictEqual(
                translateReport(node, "my message {{1}}{{0}}", ["!", "testing"]).message,
                "my message testing!"
            );
        });

        it("should allow template parameter with inner whitespace", () => {
            assert.strictEqual(
                translateReport(node, "message {{parameter name}}", { "parameter name": "yay!" }).message,
                "message yay!"
            );
        });

        it("should allow template parameter with non-identifier characters", () => {
            assert.strictEqual(
                translateReport(node, "message {{parameter-name}}", { "parameter-name": "yay!" }).message,
                "message yay!"
            );
        });

        it("should allow template parameter wrapped in braces", () => {
            assert.strictEqual(
                translateReport(node, "message {{{param}}}", { param: "yay!" }).message,
                "message {yay!}"
            );
        });

        it("should ignore template parameter with no specified value", () => {
            assert.strictEqual(
                translateReport(node, "message {{parameter}}", {}).message,
                "message {{parameter}}"
            );
        });

        it("should handle leading whitespace in template parameter", () => {
            assert.strictEqual(
                translateReport({ node, message: "message {{ parameter}}", data: { parameter: "yay!" } }).message,
                "message yay!"
            );
        });

        it("should handle trailing whitespace in template parameter", () => {
            assert.strictEqual(
                translateReport({ node, message: "message {{parameter }}", data: { parameter: "yay!" } }).message,
                "message yay!"
            );
        });

        it("should still allow inner whitespace as well as leading/trailing", () => {
            assert.strictEqual(
                translateReport(node, "message {{ parameter name }}", { "parameter name": "yay!" }).message,
                "message yay!"
            );
        });

        it("should still allow non-identifier characters as well as leading/trailing whitespace", () => {
            assert.strictEqual(
                translateReport(node, "message {{ parameter-name }}", { "parameter-name": "yay!" }).message,
                "message yay!"
            );
        });
    });

    describe("location inference", () => {
        it("should use the provided location when given in an old-style call", () => {
            assert.deepStrictEqual(
                translateReport(node, { line: 42, column: 13 }, "hello world"),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: "ExpressionStatement",
                    line: 42,
                    column: 14
                }
            );
        });

        it("should use the provided location when given in an new-style call", () => {
            assert.deepStrictEqual(
                translateReport({ node, loc: { line: 42, column: 13 }, message: "hello world" }),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: "ExpressionStatement",
                    line: 42,
                    column: 14
                }
            );
        });

        it("should extract the start and end locations from a node if no location is provided", () => {
            assert.deepStrictEqual(
                translateReport(node, "hello world"),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: "ExpressionStatement",
                    line: 1,
                    column: 1,
                    endLine: 1,
                    endColumn: 4
                }
            );
        });

        it("should have 'endLine' and 'endColumn' when 'loc' property has 'end' property.", () => {
            assert.deepStrictEqual(
                translateReport({ loc: node.loc, message: "hello world" }),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: null,
                    line: 1,
                    column: 1,
                    endLine: 1,
                    endColumn: 4
                }
            );
        });

        it("should not have 'endLine' and 'endColumn' when 'loc' property does not have 'end' property.", () => {
            assert.deepStrictEqual(
                translateReport({ loc: node.loc.start, message: "hello world" }),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: null,
                    line: 1,
                    column: 1
                }
            );
        });

        it("should infer an 'endLine' and 'endColumn' property when using the object-based context.report API", () => {
            assert.deepStrictEqual(
                translateReport({ node, message: "hello world" }),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: "ExpressionStatement",
                    line: 1,
                    column: 1,
                    endLine: 1,
                    endColumn: 4
                }
            );
        });
    });

    describe("converting old-style calls", () => {
        it("should include a fix passed as the last argument when location is not passed", () => {
            assert.deepStrictEqual(
                translateReport(node, "my message {{1}}{{0}}", ["!", "testing"], () => ({ range: [1, 1], text: "" })),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "my message testing!",
                    nodeType: "ExpressionStatement",
                    line: 1,
                    column: 1,
                    endLine: 1,
                    endColumn: 4,
                    fix: { range: [1, 1], text: "" }
                }
            );
        });
    });

    describe("validation", () => {

        it("should throw an error if node is not an object", () => {
            assert.throws(
                () => translateReport("not a node", "hello world"),
                "Node must be an object"
            );
        });


        it("should not throw an error if location is provided and node is not in an old-style call", () => {
            assert.deepStrictEqual(
                translateReport(null, { line: 1, column: 1 }, "hello world"),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: null,
                    line: 1,
                    column: 2
                }
            );
        });

        it("should not throw an error if location is provided and node is not in a new-style call", () => {
            assert.deepStrictEqual(
                translateReport({ loc: { line: 1, column: 1 }, message: "hello world" }),
                {
                    severity: 2,
                    ruleId: "foo-rule",
                    message: "hello world",
                    nodeType: null,
                    line: 1,
                    column: 2
                }
            );
        });

        it("should throw an error if neither node nor location is provided", () => {
            assert.throws(
                () => translateReport(null, "hello world"),
                "Node must be provided when reporting error if location is not provided"
            );
        });

        it("should throw an error if fix range is invalid", () => {
            assert.throws(
                () => translateReport({ node, messageId: "testMessage", fix: () => ({ text: "foo" }) }),
                "Fix has invalid range"
            );

            for (const badRange of [[0], [0, null], [null, 0], [void 0, 1], [0, void 0], [void 0, void 0], []]) {
                assert.throws(
                    () => translateReport(
                        { node, messageId: "testMessage", fix: () => ({ range: badRange, text: "foo" }) }
                    ),
                    "Fix has invalid range"
                );

                assert.throws(
                    () => translateReport(
                        {
                            node,
                            messageId: "testMessage",
                            fix: () => [
                                { range: [0, 0], text: "foo" },
                                { range: badRange, text: "bar" },
                                { range: [1, 1], text: "baz" }
                            ]
                        }
                    ),
                    "Fix has invalid range"
                );
            }
        });
    });
});
