namespace ts {
    describe("unittests:: builder", () => {
        it("emits dependent files", () => {
            const files: NamedSourceText[] = [
                { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") },
                { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") },
                { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") },
            ];

            let program = newProgram(files, ["/a.ts"], {});
            const assertChanges = makeAssertChanges(() => program);

            assertChanges(["/c.js", "/b.js", "/a.js"]);

            program = updateProgramFile(program, "/a.ts", "//comment");
            assertChanges(["/a.js"]);

            program = updateProgramFile(program, "/b.ts", "export const b = c + 1;");
            assertChanges(["/b.js", "/a.js"]);

            program = updateProgramFile(program, "/c.ts", "export const c = 1;");
            assertChanges(["/c.js", "/b.js"]);
        });

        it("if emitting all files, emits the changed file first", () => {
            const files: NamedSourceText[] = [
                { name: "/a.ts", text: SourceText.New("", "", "namespace A { export const x = 0; }") },
                { name: "/b.ts", text: SourceText.New("", "", "namespace B { export const x = 0; }") },
            ];

            let program = newProgram(files, ["/a.ts", "/b.ts"], {});
            const assertChanges = makeAssertChanges(() => program);

            assertChanges(["/a.js", "/b.js"]);

            program = updateProgramFile(program, "/a.ts", "namespace A { export const x = 1; }");
            assertChanges(["/a.js", "/b.js"]);

            program = updateProgramFile(program, "/b.ts", "namespace B { export const x = 1; }");
            assertChanges(["/b.js", "/a.js"]);
        });

        it("keeps the file in affected files if cancellation token throws during the operation", () => {
            const files: NamedSourceText[] = [
                { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") },
                { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") },
                { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") },
                { name: "/d.ts", text: SourceText.New("", "", "export const dd = 0;") },
                { name: "/e.ts", text: SourceText.New("", "", "export const ee = 0;") },
            ];

            let program = newProgram(files, ["/d.ts", "/e.ts", "/a.ts"], {});
            const assertChanges = makeAssertChangesWithCancellationToken(() => program);
            // No cancellation
            assertChanges(["/d.js", "/e.js", "/c.js", "/b.js", "/a.js"]);

            // cancel when emitting a.ts
            program = updateProgramFile(program, "/a.ts", "export function foo() { }");
            assertChanges(["/a.js"], 0);
            // Change d.ts and verify previously pending a.ts is emitted as well
            program = updateProgramFile(program, "/d.ts", "export function bar() { }");
            assertChanges(["/a.js", "/d.js"]);

            // Cancel when emitting b.js
            program = updateProgramFile(program, "/b.ts", "export class b { foo() { c + 1; } }");
            program = updateProgramFile(program, "/d.ts", "export function bar2() { }");
            assertChanges(["/d.js", "/b.js", "/a.js"], 1);
            // Change e.ts and verify previously b.js as well as a.js get emitted again since previous change was consumed completely but not d.ts
            program = updateProgramFile(program, "/e.ts", "export function bar3() { }");
            assertChanges(["/b.js", "/a.js", "/e.js"]);
        });
    });

    function makeAssertChanges(getProgram: () => Program): (fileNames: readonly string[]) => void {
        const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue };
        let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined;
        return fileNames => {
            const program = getProgram();
            builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram);
            const outputFileNames: string[] = [];
            // eslint-disable-next-line no-empty
            while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName))) {
            }
            assert.deepEqual(outputFileNames, fileNames);
        };
    }

    function makeAssertChangesWithCancellationToken(getProgram: () => Program): (fileNames: readonly string[], cancelAfterEmitLength?: number) => void {
        const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue };
        let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined;
        let cancel = false;
        const cancellationToken: CancellationToken = {
            isCancellationRequested: () => cancel,
            throwIfCancellationRequested: () => {
                if (cancel) {
                    throw new OperationCanceledException();
                }
            },
        };
        return (fileNames, cancelAfterEmitLength?: number) => {
            cancel = false;
            let operationWasCancelled = false;
            const program = getProgram();
            builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram);
            const outputFileNames: string[] = [];
            try {
                do {
                    assert.isFalse(cancel);
                    if (outputFileNames.length === cancelAfterEmitLength) {
                        cancel = true;
                    }
                } while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName), cancellationToken));
            }
            catch (e) {
                assert.isFalse(operationWasCancelled);
                assert(e instanceof OperationCanceledException, e.toString());
                operationWasCancelled = true;
            }
            assert.equal(cancel, operationWasCancelled);
            assert.equal(operationWasCancelled, fileNames.length > cancelAfterEmitLength!);
            assert.deepEqual(outputFileNames, fileNames.slice(0, cancelAfterEmitLength));
        };
    }

    function updateProgramFile(program: ProgramWithSourceTexts, fileName: string, fileContent: string): ProgramWithSourceTexts {
        return updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => {
            updateProgramText(files, fileName, fileContent);
        });
    }
}
