'use strict';

const { test } = require('node:test');
const assert = require('node:assert/strict');

const LatestWinsGate = require('../../src/domain/LatestWinsGate');

// Helper: a deferred promise so a test can pause a dispatch and inspect
// gate state before resolving. Avoids real timers entirely.
function deferred() {
    let resolve;
    let reject;
    const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
    return { promise, resolve, reject };
}

test('single fire calls dispatch with the value', async () => {
    const calls = [];
    const gate = new LatestWinsGate(async (v) => { calls.push(v); });
    gate.fire('a');
    await gate.drain();
    assert.deepEqual(calls, ['a']);
});

test('two fires while in-flight: second value runs after first settles', async () => {
    const calls = [];
    const gates = [deferred(), deferred()];
    const started = [deferred(), deferred()];
    let n = 0;
    const gate = new LatestWinsGate(async (v) => {
        const slot = n++;
        calls.push(v);
        started[slot].resolve();
        await gates[slot].promise;
    });

    gate.fire('first');
    gate.fire('second');                 // parks while 'first' is in flight
    await started[0].promise;
    assert.deepEqual(calls, ['first']);
    assert.equal(gate.size, 2);

    gates[0].resolve();
    await started[1].promise;
    assert.deepEqual(calls, ['first', 'second']);

    gates[1].resolve();
    await gate.drain();
});

test('three fires back-to-back: only the last runs after the first settles', async () => {
    const calls = [];
    const first = deferred();
    const firstStarted = deferred();
    let count = 0;
    const gate = new LatestWinsGate(async (v) => {
        calls.push(v);
        if (count++ === 0) {
            firstStarted.resolve();
            await first.promise;
        }
    });

    gate.fire(1);
    gate.fire(2);  // parked
    gate.fire(3);  // overwrites 2

    await firstStarted.promise;
    assert.deepEqual(calls, [1]);
    first.resolve();
    await gate.drain();
    assert.deepEqual(calls, [1, 3]);
});

test('drain() resolves only after all queued work has run', async () => {
    const calls = [];
    const d = deferred();
    let started = 0;
    const gate = new LatestWinsGate(async (v) => {
        calls.push(v);
        if (started++ === 0) await d.promise;
    });

    gate.fire('x');
    gate.fire('y');

    let drained = false;
    const p = gate.drain().then(() => { drained = true; });

    // While first is paused, drain must not have resolved yet.
    await Promise.resolve();
    await Promise.resolve();
    assert.equal(drained, false);

    d.resolve();
    await p;
    assert.deepEqual(calls, ['x', 'y']);
    assert.equal(drained, true);
});

test('error in dispatch does not prevent subsequent fire from working', async () => {
    const calls = [];
    let throwNext = true;
    const errors = [];
    const logger = { error: (e) => errors.push(e) };
    const gate = new LatestWinsGate(async (v) => {
        calls.push(v);
        if (throwNext) {
            throwNext = false;
            throw new Error('boom');
        }
    }, { logger });

    gate.fire('a');
    await gate.drain();
    assert.equal(calls.length, 1);
    assert.equal(errors.length, 1);
    assert.match(errors[0].message, /boom/);
    assert.ok(gate.lastError instanceof Error);

    // Gate must still accept further work.
    gate.fire('b');
    await gate.drain();
    assert.deepEqual(calls, ['a', 'b']);
});

test('error is recorded on lastError when no logger is supplied', async () => {
    const gate = new LatestWinsGate(async () => { throw new Error('silent'); });
    gate.fire('only');
    await gate.drain();
    assert.ok(gate.lastError instanceof Error);
    assert.match(gate.lastError.message, /silent/);
});

test('size reports 0 / 1 / 2 across the lifecycle', async () => {
    const d1 = deferred();
    const gate = new LatestWinsGate(async () => { await d1.promise; });

    assert.equal(gate.size, 0);

    gate.fire('one');
    // fire is sync, but _dispatch starts on a microtask. Either way the
    // gate is marked in-flight synchronously.
    assert.equal(gate.size, 1);

    gate.fire('two');               // parked
    assert.equal(gate.size, 2);

    d1.resolve();
    await gate.drain();
    assert.equal(gate.size, 0);
});

test('fireAndWait resolves when the dispatch for that value settles', async () => {
    const calls = [];
    const gate = new LatestWinsGate(async (v) => { calls.push(v); return `done:${v}`; });
    const result = await gate.fireAndWait('a');
    assert.deepEqual(calls, ['a']);
    assert.equal(result, 'done:a');
});

test('fireAndWait while in-flight: caller awaits OWN settlement, not the first call', async () => {
    const calls = [];
    const d = deferred();
    let count = 0;
    const gate = new LatestWinsGate(async (v) => {
        calls.push(v);
        if (count++ === 0) await d.promise;
        return `r:${v}`;
    });

    const p1 = gate.fireAndWait('first');
    // p1 in flight. Park second; second's promise should resolve only
    // after second's OWN dispatch runs, not after first's.
    const p2 = gate.fireAndWait('second');

    let p2Settled = false;
    p2.then(() => { p2Settled = true; });
    await Promise.resolve(); await Promise.resolve();
    assert.equal(p2Settled, false);

    d.resolve();
    const r1 = await p1;
    assert.equal(r1, 'r:first');
    const r2 = await p2;
    assert.equal(r2, 'r:second');
    assert.deepEqual(calls, ['first', 'second']);
});

test('fireAndWait superseded by a later fireAndWait resolves with { superseded: true }', async () => {
    const calls = [];
    const d = deferred();
    let count = 0;
    const gate = new LatestWinsGate(async (v) => {
        calls.push(v);
        if (count++ === 0) await d.promise;
    });

    const p1 = gate.fireAndWait('first');     // in flight
    const pParked = gate.fireAndWait('parked'); // gets superseded
    const pLatest = gate.fireAndWait('latest'); // wins

    d.resolve();
    const supersedeRes = await pParked;
    assert.equal(supersedeRes.superseded, true);

    await p1;
    await pLatest;
    assert.deepEqual(calls, ['first', 'latest']);  // 'parked' dropped
});

test('fireAndWait + fire intermix: a plain fire supersedes a pending fireAndWait', async () => {
    const d = deferred();
    let count = 0;
    const calls = [];
    const gate = new LatestWinsGate(async (v) => {
        calls.push(v);
        if (count++ === 0) await d.promise;
    });

    gate.fire('first');                        // in flight, no settle
    const pParked = gate.fireAndWait('parked');
    gate.fire('latest');                        // supersedes parked

    d.resolve();
    const res = await pParked;
    assert.equal(res.superseded, true);
    await gate.drain();
    assert.deepEqual(calls, ['first', 'latest']);
});

test('fireAndWait still resolves (with undefined) when the dispatch throws', async () => {
    const errors = [];
    const logger = { error: (e) => errors.push(e) };
    const gate = new LatestWinsGate(async () => { throw new Error('kaboom'); }, { logger });
    const r = await gate.fireAndWait('only');
    assert.equal(r, undefined);
    assert.equal(errors.length, 1);
    assert.ok(gate.lastError instanceof Error);
});
