// lots of tests use quoted code
/* eslint-disable no-template-curly-in-string */

interface ClassificationEntry {
    value: any;
    classification: ts.TokenClass;
    position?: number;
}

describe("unittests:: services:: Colorization", () => {
    // Use the shim adapter to ensure test coverage of the shim layer for the classifier
    const languageServiceAdapter = new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false);
    const classifier = languageServiceAdapter.getClassifier();

    function getEntryAtPosition(result: ts.ClassificationResult, position: number) {
        let entryPosition = 0;
        for (const entry of result.entries) {
            if (entryPosition === position) {
                return entry;
            }
            entryPosition += entry.length;
        }
        return undefined;
    }

    function punctuation(text: string, position?: number) { return createClassification(text, ts.TokenClass.Punctuation, position); }
    function keyword(text: string, position?: number) { return createClassification(text, ts.TokenClass.Keyword, position); }
    function operator(text: string, position?: number) { return createClassification(text, ts.TokenClass.Operator, position); }
    function comment(text: string, position?: number) { return createClassification(text, ts.TokenClass.Comment, position); }
    function whitespace(text: string, position?: number) { return createClassification(text, ts.TokenClass.Whitespace, position); }
    function identifier(text: string, position?: number) { return createClassification(text, ts.TokenClass.Identifier, position); }
    function numberLiteral(text: string, position?: number) { return createClassification(text, ts.TokenClass.NumberLiteral, position); }
    function stringLiteral(text: string, position?: number) { return createClassification(text, ts.TokenClass.StringLiteral, position); }
    function finalEndOfLineState(value: number): ClassificationEntry { return { value, classification: undefined!, position: 0 }; } // TODO: GH#18217
    function createClassification(value: string, classification: ts.TokenClass, position?: number): ClassificationEntry {
        return { value, classification, position };
    }

    function testLexicalClassification(text: string, initialEndOfLineState: ts.EndOfLineState, ...expectedEntries: ClassificationEntry[]): void {
        const result = classifier.getClassificationsForLine(text, initialEndOfLineState, /*syntacticClassifierAbsent*/ false);

        for (const expectedEntry of expectedEntries) {
            if (expectedEntry.classification === undefined) {
                assert.equal(result.finalLexState, expectedEntry.value, "final endOfLineState does not match expected.");
            }
            else {
                const actualEntryPosition = expectedEntry.position !== undefined ? expectedEntry.position : text.indexOf(expectedEntry.value);
                assert(actualEntryPosition >= 0, "token: '" + expectedEntry.value + "' does not exit in text: '" + text + "'.");

                const actualEntry = getEntryAtPosition(result, actualEntryPosition)!;

                assert(actualEntry, "Could not find classification entry for '" + expectedEntry.value + "' at position: " + actualEntryPosition);
                assert.equal(actualEntry.classification, expectedEntry.classification, "Classification class does not match expected. Expected: " + ts.TokenClass[expectedEntry.classification] + ", Actual: " + ts.TokenClass[actualEntry.classification]);
                assert.equal(actualEntry.length, expectedEntry.value.length, "Classification length does not match expected. Expected: " + ts.TokenClass[expectedEntry.value.length] + ", Actual: " + ts.TokenClass[actualEntry.length]);
            }
        }
    }

    describe("test getClassifications", () => {
        it("returns correct token classes", () => {
            testLexicalClassification("var x: string = \"foo\" ?? \"bar\"; //Hello",
                ts.EndOfLineState.None,
                keyword("var"),
                whitespace(" "),
                identifier("x"),
                punctuation(":"),
                keyword("string"),
                operator("="),
                stringLiteral("\"foo\""),
                whitespace(" "),
                operator("??"),
                stringLiteral("\"foo\""),
                comment("//Hello"),
                punctuation(";"));
        });

        it("correctly classifies a comment after a divide operator", () => {
            testLexicalClassification("1 / 2 // comment",
                ts.EndOfLineState.None,
                numberLiteral("1"),
                whitespace(" "),
                operator("/"),
                numberLiteral("2"),
                comment("// comment"));
        });

        it("correctly classifies a literal after a divide operator", () => {
            testLexicalClassification("1 / 2, 3 / 4",
                ts.EndOfLineState.None,
                numberLiteral("1"),
                whitespace(" "),
                operator("/"),
                numberLiteral("2"),
                numberLiteral("3"),
                numberLiteral("4"),
                operator(","));
        });

        it("correctly classifies a multiline string with one backslash", () => {
            testLexicalClassification("'line1\\",
                ts.EndOfLineState.None,
                stringLiteral("'line1\\"),
                finalEndOfLineState(ts.EndOfLineState.InSingleQuoteStringLiteral));
        });

        it("correctly classifies a multiline string with three backslashes", () => {
            testLexicalClassification("'line1\\\\\\",
                ts.EndOfLineState.None,
                stringLiteral("'line1\\\\\\"),
                finalEndOfLineState(ts.EndOfLineState.InSingleQuoteStringLiteral));
        });

        it("correctly classifies an unterminated single-line string with no backslashes", () => {
            testLexicalClassification("'line1",
                ts.EndOfLineState.None,
                stringLiteral("'line1"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies an unterminated single-line string with two backslashes", () => {
            testLexicalClassification("'line1\\\\",
                ts.EndOfLineState.None,
                stringLiteral("'line1\\\\"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies an unterminated single-line string with four backslashes", () => {
            testLexicalClassification("'line1\\\\\\\\",
                ts.EndOfLineState.None,
                stringLiteral("'line1\\\\\\\\"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies the continuing line of a multiline string ending in one backslash", () => {
            testLexicalClassification("\\",
                ts.EndOfLineState.InDoubleQuoteStringLiteral,
                stringLiteral("\\"),
                finalEndOfLineState(ts.EndOfLineState.InDoubleQuoteStringLiteral));
        });

        it("correctly classifies the continuing line of a multiline string ending in three backslashes", () => {
            testLexicalClassification("\\",
                ts.EndOfLineState.InDoubleQuoteStringLiteral,
                stringLiteral("\\"),
                finalEndOfLineState(ts.EndOfLineState.InDoubleQuoteStringLiteral));
        });

        it("correctly classifies the last line of an unterminated multiline string ending in no backslashes", () => {
            testLexicalClassification("  ",
                ts.EndOfLineState.InDoubleQuoteStringLiteral,
                stringLiteral("  "),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies the last line of an unterminated multiline string ending in two backslashes", () => {
            testLexicalClassification("\\\\",
                ts.EndOfLineState.InDoubleQuoteStringLiteral,
                stringLiteral("\\\\"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies the last line of an unterminated multiline string ending in four backslashes", () => {
            testLexicalClassification("\\\\\\\\",
                ts.EndOfLineState.InDoubleQuoteStringLiteral,
                stringLiteral("\\\\\\\\"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies the last line of a multiline string", () => {
            testLexicalClassification("'",
                ts.EndOfLineState.InSingleQuoteStringLiteral,
                stringLiteral("'"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies an unterminated multiline comment", () => {
            testLexicalClassification("/*",
                ts.EndOfLineState.None,
                comment("/*"),
                finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia));
        });

        it("correctly classifies the termination of a multiline comment", () => {
            testLexicalClassification("   */     ",
                ts.EndOfLineState.InMultiLineCommentTrivia,
                comment("   */"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("correctly classifies the continuation of a multiline comment", () => {
            testLexicalClassification("LOREM IPSUM DOLOR   ",
                ts.EndOfLineState.InMultiLineCommentTrivia,
                comment("LOREM IPSUM DOLOR   "),
                finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia));
        });

        it("correctly classifies an unterminated multiline comment on a line ending in '/*/'", () => {
            testLexicalClassification("   /*/",
                ts.EndOfLineState.None,
                comment("/*/"),
                finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia));
        });

        it("correctly classifies an unterminated multiline comment with trailing space", () => {
            testLexicalClassification("/* ",
                ts.EndOfLineState.None,
                comment("/* "),
                finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia));
        });

        it("correctly classifies a keyword after a dot", () => {
            testLexicalClassification("a.var",
                ts.EndOfLineState.None,
                identifier("var"));
        });

        it("correctly classifies a string literal after a dot", () => {
            testLexicalClassification("a.\"var\"",
                ts.EndOfLineState.None,
                stringLiteral("\"var\""));
        });

        it("correctly classifies a keyword after a dot separated by comment trivia", () => {
            testLexicalClassification("a./*hello world*/ var",
                ts.EndOfLineState.None,
                identifier("a"),
                punctuation("."),
                comment("/*hello world*/"),
                identifier("var"));
        });

        it("classifies a property access with whitespace around the dot", () => {
            testLexicalClassification("   x  .\tfoo ()",
                ts.EndOfLineState.None,
                identifier("x"),
                identifier("foo"));
        });

        it("classifies a keyword after a dot on previous line", () => {
            testLexicalClassification("var",
                ts.EndOfLineState.None,
                keyword("var"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("classifies multiple keywords properly", () => {
            testLexicalClassification("public static",
                ts.EndOfLineState.None,
                keyword("public"),
                keyword("static"),
                finalEndOfLineState(ts.EndOfLineState.None));

            testLexicalClassification("public var",
                ts.EndOfLineState.None,
                keyword("public"),
                identifier("var"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("classifies a single line no substitution template string correctly", () => {
            testLexicalClassification("`number number public string`",
                ts.EndOfLineState.None,
                stringLiteral("`number number public string`"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });
        it("classifies substitution parts of a template string correctly", () => {
            testLexicalClassification("`number '${ 1 + 1 }' string '${ 'hello' }'`",
                ts.EndOfLineState.None,
                stringLiteral("`number '${"),
                numberLiteral("1"),
                operator("+"),
                numberLiteral("1"),
                stringLiteral("}' string '${"),
                stringLiteral("'hello'"),
                stringLiteral("}'`"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });
        it("classifies an unterminated no substitution template string correctly", () => {
            testLexicalClassification("`hello world",
                ts.EndOfLineState.None,
                stringLiteral("`hello world"),
                finalEndOfLineState(ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate));
        });
        it("classifies the entire line of an unterminated multiline no-substitution/head template", () => {
            testLexicalClassification("...",
                ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate,
                stringLiteral("..."),
                finalEndOfLineState(ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate));
        });
        it("classifies the entire line of an unterminated multiline template middle/end", () => {
            testLexicalClassification("...",
                ts.EndOfLineState.InTemplateMiddleOrTail,
                stringLiteral("..."),
                finalEndOfLineState(ts.EndOfLineState.InTemplateMiddleOrTail));
        });
        it("classifies a termination of a multiline template head", () => {
            testLexicalClassification("...${",
                ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate,
                stringLiteral("...${"),
                finalEndOfLineState(ts.EndOfLineState.InTemplateSubstitutionPosition));
        });
        it("classifies the termination of a multiline no substitution template", () => {
            testLexicalClassification("...`",
                ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate,
                stringLiteral("...`"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });
        it("classifies the substitution parts and middle/tail of a multiline template string", () => {
            testLexicalClassification("${ 1 + 1 }...`",
                ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate,
                stringLiteral("${"),
                numberLiteral("1"),
                operator("+"),
                numberLiteral("1"),
                stringLiteral("}...`"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });
        it("classifies a template middle and propagates the end of line state", () => {
            testLexicalClassification("${ 1 + 1 }...`",
                ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate,
                stringLiteral("${"),
                numberLiteral("1"),
                operator("+"),
                numberLiteral("1"),
                stringLiteral("}...`"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });
        it("classifies substitution expressions with curly braces appropriately", () => {
            let pos = 0;
            let lastLength = 0;

            testLexicalClassification("...${ () => { } } ${ { x: `1` } }...`",
                ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate,
                stringLiteral(track("...${"), pos),
                punctuation(track(" ", "("), pos),
                punctuation(track(")"), pos),
                punctuation(track(" ", "=>"), pos),
                punctuation(track(" ", "{"), pos),
                punctuation(track(" ", "}"), pos),
                stringLiteral(track(" ", "} ${"), pos),
                punctuation(track(" ", "{"), pos),
                identifier(track(" ", "x"), pos),
                punctuation(track(":"), pos),
                stringLiteral(track(" ", "`1`"), pos),
                punctuation(track(" ", "}"), pos),
                stringLiteral(track(" ", "}...`"), pos),
                finalEndOfLineState(ts.EndOfLineState.None));

            // Adjusts 'pos' by accounting for the length of each portion of the string,
            // but only return the last given string
            function track(...vals: string[]): string {
                for (const val of vals) {
                    pos += lastLength;
                    lastLength = val.length;
                }
                return ts.last(vals);
            }
        });

        it("classifies partially written generics correctly.", () => {
            testLexicalClassification("Foo<number",
                ts.EndOfLineState.None,
                identifier("Foo"),
                operator("<"),
                identifier("number"),
                finalEndOfLineState(ts.EndOfLineState.None));

            // Looks like a cast, should get classified as a keyword.
            testLexicalClassification("<number",
                ts.EndOfLineState.None,
                operator("<"),
                keyword("number"),
                finalEndOfLineState(ts.EndOfLineState.None));

            // handle nesting properly.
            testLexicalClassification("Foo<Foo,Foo<number",
                ts.EndOfLineState.None,
                identifier("Foo"),
                operator("<"),
                identifier("Foo"),
                operator(","),
                identifier("Foo"),
                operator("<"),
                identifier("number"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("LexicallyClassifiesConflictTokens", () => {
            // Test conflict markers.
            testLexicalClassification(
                "class C {\r\n\
<<<<<<< HEAD\r\n\
    v = 1;\r\n\
=======\r\n\
    v = 2;\r\n\
>>>>>>> Branch - a\r\n\
}",
                ts.EndOfLineState.None,
                keyword("class"),
                identifier("C"),
                punctuation("{"),
                comment("<<<<<<< HEAD"),
                identifier("v"),
                operator("="),
                numberLiteral("1"),
                punctuation(";"),
                comment("=======\r\n    v = 2;\r\n"),
                comment(">>>>>>> Branch - a"),
                punctuation("}"),
                finalEndOfLineState(ts.EndOfLineState.None));

            testLexicalClassification(
                "<<<<<<< HEAD\r\n\
class C { }\r\n\
=======\r\n\
class D { }\r\n\
>>>>>>> Branch - a\r\n",
                ts.EndOfLineState.None,
                comment("<<<<<<< HEAD"),
                keyword("class"),
                identifier("C"),
                punctuation("{"),
                punctuation("}"),
                comment("=======\r\nclass D { }\r\n"),
                comment(">>>>>>> Branch - a"),
                finalEndOfLineState(ts.EndOfLineState.None));

            testLexicalClassification(
                "class C {\r\n\
<<<<<<< HEAD\r\n\
    v = 1;\r\n\
||||||| merged common ancestors\r\n\
    v = 3;\r\n\
=======\r\n\
    v = 2;\r\n\
>>>>>>> Branch - a\r\n\
}",
                ts.EndOfLineState.None,
                keyword("class"),
                identifier("C"),
                punctuation("{"),
                comment("<<<<<<< HEAD"),
                identifier("v"),
                operator("="),
                numberLiteral("1"),
                punctuation(";"),
                comment("||||||| merged common ancestors\r\n    v = 3;\r\n"),
                comment("=======\r\n    v = 2;\r\n"),
                comment(">>>>>>> Branch - a"),
                punctuation("}"),
                finalEndOfLineState(ts.EndOfLineState.None));

            testLexicalClassification(
                "<<<<<<< HEAD\r\n\
class C { }\r\n\
||||||| merged common ancestors\r\n\
class E { }\r\n\
=======\r\n\
class D { }\r\n\
>>>>>>> Branch - a\r\n",
                ts.EndOfLineState.None,
                comment("<<<<<<< HEAD"),
                keyword("class"),
                identifier("C"),
                punctuation("{"),
                punctuation("}"),
                comment("||||||| merged common ancestors\r\nclass E { }\r\n"),
                comment("=======\r\nclass D { }\r\n"),
                comment(">>>>>>> Branch - a"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });

        it("'of' keyword", () => {
            testLexicalClassification("for (var of of of) { }",
                ts.EndOfLineState.None,
                keyword("for"),
                punctuation("("),
                keyword("var"),
                keyword("of"),
                keyword("of"),
                keyword("of"),
                punctuation(")"),
                punctuation("{"),
                punctuation("}"),
                finalEndOfLineState(ts.EndOfLineState.None));
        });
    });
});
