// Copyright (c) 2017, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

import fs from 'node:fs/promises';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

import {describe, expect, it} from 'vitest';
import winston from 'winston';

import {makeLogStream} from '../lib/logger.js';
import * as utils from '../lib/utils.js';
import {newTempDir} from './utils.js';

describe('Splits lines', () => {
    it('handles empty input', () => {
        expect(utils.splitLines('')).toEqual([]);
    });
    it('handles a single line with no newline', () => {
        expect(utils.splitLines('A line')).toEqual(['A line']);
    });
    it('handles a single line with a newline', () => {
        expect(utils.splitLines('A line\n')).toEqual(['A line']);
    });
    it('handles multiple lines', () => {
        expect(utils.splitLines('A line\nAnother line\n')).toEqual(['A line', 'Another line']);
    });
    it('handles multiple lines ending on a non-newline', () => {
        expect(utils.splitLines('A line\nAnother line\nLast line')).toEqual(['A line', 'Another line', 'Last line']);
    });
    it('handles empty lines', () => {
        expect(utils.splitLines('A line\n\nA line after an empty')).toEqual(['A line', '', 'A line after an empty']);
    });
    it('handles a single empty line', () => {
        expect(utils.splitLines('\n')).toEqual(['']);
    });
    it('handles multiple empty lines', () => {
        expect(utils.splitLines('\n\n\n')).toEqual(['', '', '']);
    });
    it('handles \\r\\n lines', () => {
        expect(utils.splitLines('Some\r\nLines\r\n')).toEqual(['Some', 'Lines']);
    });
});

describe('Expands tabs', () => {
    it('leaves non-tabs alone', () => {
        expect(utils.expandTabs('This has no tabs at all')).toEqual('This has no tabs at all');
    });
    it('at beginning of line', () => {
        expect(utils.expandTabs('\tOne tab')).toEqual('        One tab');
        expect(utils.expandTabs('\t\tTwo tabs')).toEqual('                Two tabs');
    });
    it('mid-line', () => {
        expect(utils.expandTabs('0\t1234567A')).toEqual('0       1234567A');
        expect(utils.expandTabs('01\t234567A')).toEqual('01      234567A');
        expect(utils.expandTabs('012\t34567A')).toEqual('012     34567A');
        expect(utils.expandTabs('0123\t4567A')).toEqual('0123    4567A');
        expect(utils.expandTabs('01234\t567A')).toEqual('01234   567A');
        expect(utils.expandTabs('012345\t67A')).toEqual('012345  67A');
        expect(utils.expandTabs('0123456\t7A')).toEqual('0123456 7A');
        expect(utils.expandTabs('01234567\tA')).toEqual('01234567        A');
    });
});

describe('Parses compiler output', () => {
    it('handles simple cases', () => {
        expect(utils.parseOutput('Line one\nLine two', 'bob.cpp')).toEqual([{text: 'Line one'}, {text: 'Line two'}]);
        expect(utils.parseOutput('Line one\nbob.cpp:1 Line two', 'bob.cpp')).toEqual([
            {text: 'Line one'},
            {
                tag: {column: 0, line: 1, text: 'Line two', severity: 3, file: 'bob.cpp'},
                text: '<source>:1 Line two',
            },
        ]);
        expect(utils.parseOutput('Line one\nbob.cpp:1:5: Line two', 'bob.cpp')).toEqual([
            {text: 'Line one'},
            {
                tag: {column: 5, line: 1, text: 'Line two', severity: 3, file: 'bob.cpp'},
                text: '<source>:1:5: Line two',
            },
        ]);
    });
    it('handles windows output', () => {
        expect(utils.parseOutput('bob.cpp(1) Oh noes', 'bob.cpp')).toEqual([
            {
                tag: {column: 0, line: 1, text: 'Oh noes', severity: 3, file: 'bob.cpp'},
                text: '<source>(1) Oh noes',
            },
        ]);
    });
    it('does not treat c2rust AST node lines as source locations', () => {
        expect(utils.parseOutput('CTypeId(42): Located {', 'example.c')).toEqual([{text: 'CTypeId(42): Located {'}]);
    });
    it('replaces all references to input source', () => {
        expect(utils.parseOutput('bob.cpp:1 error in bob.cpp', 'bob.cpp')).toEqual([
            {
                tag: {column: 0, line: 1, text: 'error in <source>', severity: 3, file: 'bob.cpp'},
                text: '<source>:1 error in <source>',
            },
        ]);
    });
    it('treats warnings and notes as the correct severity', () => {
        expect(utils.parseOutput('Line one\nbob.cpp:1:5: warning Line two', 'bob.cpp')).toEqual([
            {text: 'Line one'},
            {
                tag: {column: 5, line: 1, text: 'warning Line two', severity: 2, file: 'bob.cpp'},
                text: '<source>:1:5: warning Line two',
            },
        ]);
        expect(utils.parseOutput('Line one\nbob.cpp:1:5: note Line two', 'bob.cpp')).toEqual([
            {text: 'Line one'},
            {
                tag: {column: 5, line: 1, text: 'note Line two', severity: 1, file: 'bob.cpp'},
                text: '<source>:1:5: note Line two',
            },
        ]);
    });
    it('treats <stdin> as if it were the compiler source', () => {
        expect(
            utils.parseOutput("<stdin>:120:25: error: variable or field 'transform_data' declared void", 'bob.cpp'),
        ).toEqual([
            {
                tag: {
                    column: 25,
                    line: 120,
                    text: "error: variable or field 'transform_data' declared void",
                    severity: 3,
                    file: 'bob.cpp',
                },
                text: "<source>:120:25: error: variable or field 'transform_data' declared void",
            },
        ]);
    });

    it('parser error with full path', () => {
        expect(utils.parseOutput("/app/example.cl:5:30: error: use of undeclared identifier 'ad'")).toEqual([
            {
                tag: {
                    file: 'example.cl',
                    column: 30,
                    line: 5,
                    text: "error: use of undeclared identifier 'ad'",
                    severity: 3,
                },
                text: "example.cl:5:30: error: use of undeclared identifier 'ad'",
            },
        ]);
    });

    it('removes hyperlink escape sequences', () => {
        expect(
            utils.parseOutput(
                't.c:3:1: warning: control reaches end of non-void function [\x1B]8;;https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Warning-Options.html#index-Wno-return-type\x1B\\-Wreturn-type\x1B]8;;\x1B\\]',
            ),
        ).toEqual([
            {
                tag: {
                    file: 't.c',
                    line: 3,
                    column: 1,
                    text: 'warning: control reaches end of non-void function [-Wreturn-type]',
                    severity: 2,
                },
                text: 't.c:3:1: warning: control reaches end of non-void function [\x1B]8;;https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Warning-Options.html#index-Wno-return-type\x1B\\-Wreturn-type\x1B]8;;\x1B\\]',
            },
        ]);
    });
});

describe('Pascal compiler output', () => {
    it('recognize fpc identifier not found error', () => {
        expect(utils.parseOutput('output.pas(13,23) Error: Identifier not found "adsadasd"', 'output.pas')).toEqual([
            {
                tag: {
                    column: 23,
                    line: 13,
                    text: 'Error: Identifier not found "adsadasd"',
                    severity: 3,
                    file: 'output.pas',
                },
                text: '<source>(13,23) Error: Identifier not found "adsadasd"',
            },
        ]);
    });

    it('recognize fpc exiting error', () => {
        expect(
            utils.parseOutput('output.pas(17) Fatal: There were 1 errors compiling module, stopping', 'output.pas'),
        ).toEqual([
            {
                tag: {
                    column: 0,
                    line: 17,
                    text: 'Fatal: There were 1 errors compiling module, stopping',
                    severity: 3,
                    file: 'output.pas',
                },
                text: '<source>(17) Fatal: There were 1 errors compiling module, stopping',
            },
        ]);
    });

    it('removes the temp path', () => {
        expect(
            utils.parseOutput(
                'Compiling /tmp/path/prog.dpr\noutput.pas(17) Fatal: There were 1 errors compiling module, stopping',
                'output.pas',
                '/tmp/path/',
            ),
        ).toEqual([
            {
                text: 'Compiling prog.dpr',
            },
            {
                tag: {
                    column: 0,
                    line: 17,
                    text: 'Fatal: There were 1 errors compiling module, stopping',
                    severity: 3,
                    file: 'output.pas',
                },
                text: '<source>(17) Fatal: There were 1 errors compiling module, stopping',
            },
        ]);
    });
});

describe('Rust compiler output', () => {
    it('handles simple cases', () => {
        expect(utils.parseRustOutput('Line one\nLine two', 'bob.rs')).toEqual([{text: 'Line one'}, {text: 'Line two'}]);
        expect(utils.parseRustOutput('Unrelated\nLine one\n --> bob.rs:1:0\nUnrelated', 'bob.rs')).toEqual([
            {text: 'Unrelated'},
            {
                tag: {file: 'bob.rs', column: 0, line: 1, text: 'Line one', severity: 3},
                text: 'Line one',
            },
            {
                tag: {file: 'bob.rs', column: 0, line: 1, text: '', severity: 3},
                text: ' --> <source>:1:0',
            },
            {text: 'Unrelated'},
        ]);
        expect(utils.parseRustOutput('Line one\n --> bob.rs:1:5', 'bob.rs')).toEqual([
            {
                tag: {file: 'bob.rs', column: 5, line: 1, text: 'Line one', severity: 3},
                text: 'Line one',
            },
            {
                tag: {file: 'bob.rs', column: 5, line: 1, text: '', severity: 3},
                text: ' --> <source>:1:5',
            },
        ]);
        expect(utils.parseRustOutput('Multiple spaces\n   --> bob.rs:1:5', 'bob.rs')).toEqual([
            {
                tag: {file: 'bob.rs', column: 5, line: 1, text: 'Multiple spaces', severity: 3},
                text: 'Multiple spaces',
            },
            {
                tag: {file: 'bob.rs', column: 5, line: 1, text: '', severity: 3},
                text: '   --> <source>:1:5',
            },
        ]);
    });

    it('replaces all references to input source', () => {
        expect(utils.parseRustOutput('error: Error in bob.rs\n --> bob.rs:1:42', 'bob.rs')).toEqual([
            {
                tag: {file: 'bob.rs', column: 42, line: 1, text: 'error: Error in <source>', severity: 3},
                text: 'error: Error in <source>',
            },
            {
                tag: {file: 'bob.rs', column: 42, line: 1, text: '', severity: 3},
                text: ' --> <source>:1:42',
            },
        ]);
    });

    it('treats <stdin> as if it were the compiler source', () => {
        expect(utils.parseRustOutput('error: <stdin> is sad\n --> <stdin>:120:25', 'bob.rs')).toEqual([
            {
                tag: {file: 'bob.rs', column: 25, line: 120, text: 'error: <source> is sad', severity: 3},
                text: 'error: <source> is sad',
            },
            {
                tag: {file: 'bob.rs', column: 25, line: 120, text: '', severity: 3},
                text: ' --> <source>:120:25',
            },
        ]);
    });

    it('removes hyperlink escape sequences', () => {
        expect(
            utils.parseRustOutput(
                'error[\x1B]8;;https://doc.rust-lang.org/error_codes/E0425.html\x07E0425\x1B]8;;\x07]: cannot find value `x` in this scope\n --> <source>:42:27',
            ),
        ).toEqual([
            {
                tag: {
                    line: 42,
                    column: 27,
                    text: 'error[E0425]: cannot find value `x` in this scope',
                    severity: 3,
                },
                text: 'error[\x1B]8;;https://doc.rust-lang.org/error_codes/E0425.html\x07E0425\x1B]8;;\x07]: cannot find value `x` in this scope',
            },
            {
                tag: {
                    line: 42,
                    column: 27,
                    text: '',
                    severity: 3,
                },
                text: ' --> <source>:42:27',
            },
        ]);
    });

    it('emits quickfixes', () => {
        expect(utils.parseRustOutput('error\n --> <source>:42:27\n15  + use std::collections::HashMap;')).toEqual([
            {
                tag: {
                    line: 42,
                    column: 27,
                    text: 'error',
                    severity: 3,
                },
                text: 'error',
            },
            {
                tag: {
                    line: 42,
                    column: 27,
                    text: '',
                    severity: 3,
                },
                text: ' --> <source>:42:27',
            },
            {
                text: '15  + use std::collections::HashMap;',
                tag: {
                    line: 42,
                    column: 27,
                    text: 'error',
                    severity: 3,
                    fixes: [
                        {
                            title: 'Add import for `std::collections::HashMap`',
                            edits: [
                                {
                                    line: 15,
                                    column: 1,
                                    endline: 15,
                                    endcolumn: 1,
                                    text: 'use std::collections::HashMap;\n',
                                },
                            ],
                        },
                    ],
                },
            },
        ]);

        expect(
            utils.parseRustOutput(
                'error\n --> <source>:42:27\n   = help: add `#![feature(num_midpoint_signed)]` to the crate attributes to enable',
            ),
        ).toEqual([
            {
                tag: {
                    line: 42,
                    column: 27,
                    text: 'error',
                    severity: 3,
                },
                text: 'error',
            },
            {
                tag: {
                    line: 42,
                    column: 27,
                    text: '',
                    severity: 3,
                },
                text: ' --> <source>:42:27',
            },
            {
                text: '   = help: add `#![feature(num_midpoint_signed)]` to the crate attributes to enable',
                tag: {
                    line: 42,
                    column: 27,
                    text: 'error',
                    severity: 3,
                    fixes: [
                        {
                            title: 'Add feature flag `num_midpoint_signed`',
                            edits: [
                                {
                                    line: 1,
                                    column: 1,
                                    endline: 1,
                                    endcolumn: 1,
                                    text: '#![feature(num_midpoint_signed)]\n',
                                },
                            ],
                        },
                    ],
                },
            },
        ]);
    });

    it('attaches filenames to errors from multiple files', () => {
        // https://godbolt.org/z/nWE3PTeGf
        expect(
            utils.parseRustOutput(
                `warning: function \`f1\` is never used
 --> <source>:3:4

warning: function \`f2\` is never used
 --> /app/m.rs:1:4

warning: 2 warnings emitted`,
                'example.rs',
            ),
        ).toEqual([
            {
                text: 'warning: function `f1` is never used',
                tag: {
                    line: 3,
                    column: 4,
                    file: 'example.rs',
                    severity: 2,
                    text: 'warning: function `f1` is never used',
                },
            },
            {
                text: ' --> <source>:3:4',
                tag: {
                    line: 3,
                    column: 4,
                    file: 'example.rs',
                    severity: 3,
                    text: '',
                },
            },
            {text: ''},
            {
                text: 'warning: function `f2` is never used',
                tag: {
                    line: 1,
                    column: 4,
                    file: 'm.rs',
                    severity: 2,
                    text: 'warning: function `f2` is never used',
                },
            },
            {
                text: ' --> /app/m.rs:1:4',
                tag: {
                    line: 1,
                    column: 4,
                    file: 'm.rs',
                    severity: 3,
                    text: '',
                },
            },
            {text: ''},
            {text: 'warning: 2 warnings emitted'},
        ]);
    });
});

describe('Tool output', () => {
    it('removes the relative path', () => {
        expect(
            utils.parseOutput(
                './example.cpp:1:1: Fatal: There were 1 errors compiling module, stopping',
                './example.cpp',
            ),
        ).toEqual([
            {
                tag: {
                    column: 1,
                    line: 1,
                    text: 'Fatal: There were 1 errors compiling module, stopping',
                    severity: 3,
                    file: 'example.cpp',
                },
                text: '<source>:1:1: Fatal: There were 1 errors compiling module, stopping',
            },
        ]);
    });

    it('removes fortran relative path', () => {
        expect(
            utils.parseOutput("./example.f90:5:22: error: No explicit type declared for 'y'", './example.f90'),
        ).toEqual([
            {
                tag: {
                    column: 22,
                    line: 5,
                    text: "error: No explicit type declared for 'y'",
                    severity: 3,
                    file: 'example.f90',
                },
                text: "<source>:5:22: error: No explicit type declared for 'y'",
            },
        ]);
    });

    it('removes the jailed path', () => {
        expect(
            utils.parseOutput(
                '/home/ubuntu/example.cpp:1:1: Fatal: There were 1 errors compiling module, stopping',
                './example.cpp',
            ),
        ).toEqual([
            {
                tag: {
                    column: 1,
                    line: 1,
                    text: 'Fatal: There were 1 errors compiling module, stopping',
                    severity: 3,
                    file: 'example.cpp',
                },
                text: '<source>:1:1: Fatal: There were 1 errors compiling module, stopping',
            },
        ]);
    });
});

describe('Anonymizes all kind of IPs', () => {
    it('Ignores localhost', () => {
        expect(utils.anonymizeIp('localhost')).toEqual('localhost');
        expect(utils.anonymizeIp('localhost:42')).toEqual('localhost:42');
    });
    it('Removes last octet from IPv4 addresses', () => {
        expect(utils.anonymizeIp('127.0.0.0')).toEqual('127.0.0.0');
        expect(utils.anonymizeIp('127.0.0.10')).toEqual('127.0.0.0');
        expect(utils.anonymizeIp('127.0.0.255')).toEqual('127.0.0.0');
    });
    it('Removes last 3 hextets from IPv6 addresses', () => {
        // Not necessarily valid addresses, we're interested in the format
        expect(utils.anonymizeIp('ffff:aaaa:dead:beef')).toEqual('ffff:0:0:0');
        expect(utils.anonymizeIp('bad:c0de::')).toEqual('bad:0:0:0');
        expect(utils.anonymizeIp(':1d7e::c0fe')).toEqual(':0:0:0');
    });
});

describe('Logger functionality', () => {
    it('correctly logs streams split over lines', () => {
        const logs: {level: string; msg: string}[] = [];
        const fakeLog = {log: (level: string, msg: string) => logs.push({level, msg})} as any as winston.Logger;
        const infoStream = makeLogStream('info', fakeLog);
        infoStream.write('first\n');
        infoStream.write('part');
        infoStream.write('ial\n');
        expect(logs).toEqual([
            {
                level: 'info',
                msg: 'first',
            },
            {
                level: 'info',
                msg: 'partial',
            },
        ]);
    });
    it('correctly logs streams to the right destination', () => {
        const logs: {level: string; msg: string}[] = [];
        const fakeLog = {log: (level: string, msg: string) => logs.push({level, msg})} as any as winston.Logger;
        const infoStream = makeLogStream('warn', fakeLog);
        infoStream.write('ooh\n');
        expect(logs).toEqual([
            {
                level: 'warn',
                msg: 'ooh',
            },
        ]);
    });
});

describe('Hash interface', () => {
    it('correctly hashes strings', () => {
        const version = 'Compiler Explorer Tests Version 0';
        expect(utils.getHash('cream cheese', version)).toEqual(
            'cfff2d1f7a213e314a67cce8399160ae884f794a3ee9d4a01cd37a8c22c67d94',
        );
        expect(utils.getHash('large eggs', version)).toEqual(
            '9144dec50b8df5bc5cc24ba008823cafd6616faf2f268af84daf49ac1d24feb0',
        );
        expect(utils.getHash('sugar', version)).toEqual(
            'afa3c89d0f6a61de6805314c9bd7c52d020425a3a3c7bbdfa7c0daec594e5ef1',
        );
    });
    it('correctly hashes objects', () => {
        expect(
            utils.getHash({
                toppings: [
                    {name: 'raspberries', optional: false},
                    {name: 'ground cinnamon', optional: true},
                ],
            }),
        ).toEqual('e205d63abd5db363086621fdc62c4c23a51b733bac5855985a8b56642d570491');
    });
});

describe('GoldenLayout utils', () => {
    it('finds every editor & compiler', async () => {
        const state = JSON.parse(await fs.readFile('test/example-states/default-state.json', 'utf-8'));
        const contents = utils.glGetMainContents(state.content);
        expect(contents).toEqual({
            editors: [
                {source: 'Editor 1', language: 'c++'},
                {source: 'Editor 2', language: 'c++'},
                {source: 'Editor 3', language: 'c++'},
                {source: 'Editor 4', language: 'c++'},
            ],
            compilers: [
                {compiler: 'clang_trunk'},
                {compiler: 'gsnapshot'},
                {compiler: 'clang_trunk'},
                {compiler: 'gsnapshot'},
                {compiler: 'rv32-clang'},
            ],
        });
    });
});

describe('squashes horizontal whitespace', () => {
    it('handles empty input', () => {
        expect(utils.squashHorizontalWhitespace('')).toEqual('');
        expect(utils.squashHorizontalWhitespace(' ')).toEqual('');
        expect(utils.squashHorizontalWhitespace('    ')).toEqual('');
    });
    it('handles leading spaces', () => {
        expect(utils.squashHorizontalWhitespace(' abc')).toEqual(' abc');
        expect(utils.squashHorizontalWhitespace('   abc')).toEqual('  abc');
        expect(utils.squashHorizontalWhitespace('       abc')).toEqual('  abc');
    });
    it('handles interline spaces', () => {
        expect(utils.squashHorizontalWhitespace('abc abc')).toEqual('abc abc');
        expect(utils.squashHorizontalWhitespace('abc   abc')).toEqual('abc abc');
        expect(utils.squashHorizontalWhitespace('abc     abc')).toEqual('abc abc');
    });
    it('handles leading and interline spaces', () => {
        expect(utils.squashHorizontalWhitespace(' abc  abc')).toEqual(' abc abc');
        expect(utils.squashHorizontalWhitespace('  abc abc')).toEqual('  abc abc');
        expect(utils.squashHorizontalWhitespace('  abc     abc')).toEqual('  abc abc');
        expect(utils.squashHorizontalWhitespace('    abc   abc')).toEqual('  abc abc');
    });
});

describe('encodes in our version of base32', () => {
    function doTest(original: string, expected: string) {
        expect(utils.base32Encode(Buffer.from(original))).toEqual(expected);
    }

    // Done by hand to check that they are valid

    it('works for empty strings', () => {
        doTest('', '');
    });

    it('works for lengths multiple of 5 bits', () => {
        doTest('aaaaa', '3Mn4ha7P');
    });

    it('works for lengths not multiple of 5 bits', () => {
        // 3
        doTest('a', '35');

        // 1
        doTest('aa', '3Mn1');

        // 4
        doTest('aaa', '3Mn48');

        // 2
        doTest('aaaa', '3Mn4ha3');
    });

    it('works for some random strings', () => {
        // I also calculated this ones so lets put them
        doTest('foo', '8rrx8');

        doTest('foobar', '8rrx8b7Pc5');
    });
});

const thisFilename = fileURLToPath(import.meta.url);

describe('fileExists', () => {
    it('Returns true for files that exists', async () => {
        await expect(utils.fileExists(thisFilename)).resolves.toBe(true);
    });
    it("Returns false for files that don't exist", async () => {
        await expect(utils.fileExists('./ABC-FileThatDoesNotExist.extension')).resolves.toBe(false);
    });
    it('Returns false for directories that exist', async () => {
        await expect(utils.fileExists(path.resolve(path.dirname(thisFilename)))).resolves.toBe(false);
    });
});

describe('safe semver', () => {
    it('should understand most kinds of semvers', () => {
        expect(utils.asSafeVer('0')).toEqual('0.0.0');
        expect(utils.asSafeVer('1')).toEqual('1.0.0');

        expect(utils.asSafeVer('1.0')).toEqual('1.0.0');
        expect(utils.asSafeVer('1.1')).toEqual('1.1.0');

        expect(utils.asSafeVer('1.1.0')).toEqual('1.1.0');
        expect(utils.asSafeVer('1.1.1')).toEqual('1.1.1');

        expect(utils.asSafeVer('trunk')).toEqual(utils.magic_semver.trunk);
        expect(utils.asSafeVer('(trunk)')).toEqual(utils.magic_semver.trunk);
        expect(utils.asSafeVer('(123.456.789 test)')).toEqual(utils.magic_semver.non_trunk);

        expect(utils.asSafeVer('0..0')).toEqual(utils.magic_semver.non_trunk);
        expect(utils.asSafeVer('0.0.')).toEqual(utils.magic_semver.non_trunk);
        expect(utils.asSafeVer('0.')).toEqual(utils.magic_semver.non_trunk);
        expect(utils.asSafeVer('.0.0')).toEqual(utils.magic_semver.non_trunk);
        expect(utils.asSafeVer('.0..')).toEqual(utils.magic_semver.non_trunk);
        expect(utils.asSafeVer('0..')).toEqual(utils.magic_semver.non_trunk);

        expect(utils.asSafeVer('123 TEXT')).toEqual('123.0.0');
        expect(utils.asSafeVer('123.456 TEXT')).toEqual('123.456.0');
        expect(utils.asSafeVer('123.456.789 TEXT')).toEqual('123.456.789');
    });
});

describe('tries to load text file', () => {
    it('should load files that exist', async () => {
        const selfText = await utils.tryReadTextFile(thisFilename);
        expect(selfText).toContain('should load files that exist');
    });
    it('should be undefined for non-existent files', async () => {
        const selfText = await utils.tryReadTextFile(thisFilename + '.doesntexist');
        expect(selfText).to.be.undefined;
    });
});

describe('output files', async () => {
    const tmpDir = newTempDir();
    it('should work in simple cases', async () => {
        const filepath = path.join(tmpDir, 'file.txt');
        await utils.outputTextFile(filepath, 'hello');
        expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
    });
    it('should create subddirectories ok', async () => {
        const filepath = path.join(tmpDir, 'a', 'b', 'file.txt');
        await utils.outputTextFile(filepath, 'hello');
        expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
    });
    it('should ensure files exist', async () => {
        const filepath = path.join(tmpDir, 'moo', 'foo', 'file.txt');
        await utils.ensureFileExists(filepath);
        expect(await utils.tryReadTextFile(filepath)).toEqual('');
        await utils.outputTextFile(filepath, 'hello');
        expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
        await utils.ensureFileExists(filepath);
        expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
    });
});

describe('maskRootdir', () => {
    it('masks a CE temp path down to the user-facing filename', () => {
        expect(utils.maskRootdir('/tmp/compiler-explorer-compiler123-4-abc/example.cpp')).toEqual('example.cpp');
    });

    it('keeps /app/ when the temp path is embedded (e.g. a -I include flag)', () => {
        expect(utils.maskRootdir('-I/tmp/compiler-explorer-compiler123-4-abc/include')).toEqual('-I/app/include');
    });

    // The recorded path's tmpdir root need not match this process's os.tmpdir(), so
    // masking keys off the ce_temp_prefix marker, not the root.
    it.each([
        ['/tmp', '/tmp/compiler-explorer-compilerXYZ/example.cpp'],
        ['/usr/tmp', '/usr/tmp/compiler-explorer-compilerXYZ/example.cpp'],
        ['macOS /private/var', '/private/var/folders/ab/xyz/T/compiler-explorer-compilerXYZ/example.cpp'],
        ['Windows', 'C:/Users/ce/AppData/Local/Temp/compiler-explorer-compilerXYZ/example.cpp'],
    ])('masks a temp path rooted at %s regardless of the local tmpdir', (_root, input) => {
        expect(utils.maskRootdir(input)).toEqual('example.cpp');
    });

    it('leaves non-temp paths untouched', () => {
        expect(utils.maskRootdir('/usr/include/stdio.h')).toEqual('/usr/include/stdio.h');
    });

    // The marker segment must be followed by `/`, so a bare dir with no trailing slash
    // is left alone. (Real inputs always name a file inside the dir.)
    it('leaves a bare temp dir with no trailing slash untouched', () => {
        expect(utils.maskRootdir('/tmp/compiler-explorer-compiler123-4-abc')).toEqual(
            '/tmp/compiler-explorer-compiler123-4-abc',
        );
    });

    it('masks a bare temp dir with a trailing slash to empty', () => {
        expect(utils.maskRootdir('/tmp/compiler-explorer-compiler123-4-abc/')).toEqual('');
    });

    it('leaves paths with invalid marker suffix chars untouched', () => {
        expect(utils.maskRootdir('/tmp/compiler-explorer-compiler+bad/example.cpp')).toEqual(
            '/tmp/compiler-explorer-compiler+bad/example.cpp',
        );
    });

    it('handles long non-matching path-like inputs without altering them', () => {
        const input = `-I/${'segment/'.repeat(4000)}not-temp/include`;
        expect(utils.maskRootdir(input)).toEqual(input);
    });

    // The regex stops at whitespace (so multi-token output lines aren't over-masked),
    // so a temp root containing a space is only masked from the last space onward.
    it('masks only from the last space in a spaced temp root (documented limitation)', () => {
        expect(utils.maskRootdir('/tmp/with space/compiler-explorer-compilerXXX/example.cpp')).toEqual(
            '/tmp/with space/app/example.cpp',
        );
    });

    it('passes empty input through unchanged', () => {
        expect(utils.maskRootdir('')).toEqual('');
    });
});
