// Copyright (c) 2026, 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 {TimeoutError} from 'p-queue';
import PromClient from 'prom-client';
import {describe, expect, it} from 'vitest';

import {CompilationQueue} from '../lib/compilation-queue.js';

async function completedCount(): Promise<number> {
    const counter = PromClient.register.getSingleMetric('ce_compilation_queue_completed_total');
    const metric = await (counter as PromClient.Counter).get();
    return metric.values[0].value;
}

describe('CompilationQueue', () => {
    it('runs an enqueued job and returns its result', async () => {
        const queue = new CompilationQueue(1, 1000, 1000);
        await expect(queue.enqueue(async () => 42)).resolves.toEqual(42);
        expect(queue.status().busy).toBe(false);
    });

    it('does not deadlock when a job enqueues another job', async () => {
        const queue = new CompilationQueue(1, 1000, 1000);
        const result = await queue.enqueue(() => {
            return queue.enqueue(async () => 42);
        });
        expect(result).toEqual(42);
    });

    it('times out a job that never settles, freeing the queue', async () => {
        // A job whose promise never settles (e.g. a download severed mid-stream, see #8811)
        // must not occupy a queue slot forever: the queue timeout has to reject it so the
        // queue can go back to being non-busy (which gates temp dir cleanup).
        const queue = new CompilationQueue(1, 100, 1000);
        const wedged = queue.enqueue(() => new Promise(() => {}));
        await expect(wedged).rejects.toThrow(TimeoutError);

        await expect(queue.enqueue(async () => 'still works')).resolves.toEqual('still works');
        expect(queue.status().busy).toBe(false);
        // Generous test budget: the 100ms queue timeout can fire very late when vitest workers
        // saturate the machine (e.g. during pre-commit runs).
    }, 15_000);

    it('counts a job as completed when it settles, not when it starts', async () => {
        const queue = new CompilationQueue(1, 1000, 1000);
        const before = await completedCount();
        let release: () => void = () => {};
        const gate = new Promise<void>(resolve => {
            release = resolve;
        });
        let started: () => void = () => {};
        const startedGate = new Promise<void>(resolve => {
            started = resolve;
        });
        const jobPromise = queue.enqueue(() => {
            started();
            return gate;
        });
        await startedGate;
        expect(await completedCount()).toEqual(before);
        release();
        await jobPromise;
        expect(await completedCount()).toEqual(before + 1);
    });
});
