import { assert } from "../../../core/assert.js";
import { Transport } from "./Transport.js";

/**
 * In-process Transport pair for tests and tooling.
 *
 * Two `LoopbackTransport` instances are paired via {@link bind_pair}. Calls to
 * `send` on one place a copy of the bytes into the peer's inbound queue, where
 * they sit until {@link deliver_all} is called. This gives tests precise control
 * over packet timing — no hidden setTimeout, no real network.
 *
 * Wire-condition simulation is offered as deterministic helpers
 * ({@link drop_next}, {@link reorder}) rather than randomized parameters; tests
 * are easier to write and reproduce when they pick which packet to drop, not
 * "5% of packets". Random conditions can be layered on top by the test if needed.
 *
 * Each `send` copies the bytes (because the caller may reuse the source buffer
 * immediately). Pool-based optimization is reasonable later but not necessary
 * for correctness.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class LoopbackTransport extends Transport {
    #peer;
    #in_queue;
    #stats;


    constructor() {
        super();

        // `deliver_all()` empties the queue front-to-back, so packets
        // arrive in send order, and the queue never drops them.
        this.reliable = true;
        this.ordered = true;

        /**
         * Peer transport; null until {@link bind_pair} is called.
         * @type {LoopbackTransport|null}
         * @private
         */
        this.#peer = null;

        /**
         * Inbound queue of pending packets (copies of the bytes the peer sent).
         * Entries are Uint8Array — already trimmed to packet length.
         * @type {Uint8Array[]}
         * @private
         */
        this.#in_queue = [];

        /**
         * Stats.
         * @private
         */
        this.#stats = {
            bytes_in: 0,
            bytes_out: 0,
            packets_in: 0,
            packets_out: 0,
        };
    }

    /**
     * Bind two LoopbackTransports together so they deliver to each other.
     *
     * @param {LoopbackTransport} a
     * @param {LoopbackTransport} b
     */
    static bind_pair(a, b) {
        assert.ok(a instanceof LoopbackTransport, 'a must be a LoopbackTransport');
        assert.ok(b instanceof LoopbackTransport, 'b must be a LoopbackTransport');
        a.#peer = b;
        b.#peer = a;
    }

    /**
     * Send a packet. Bytes are copied into the peer's inbound queue immediately;
     * delivery to the peer's `onReceive` happens when {@link deliver_all} runs
     * on the peer.
     *
     * @param {Uint8Array} bytes
     * @param {number} length
     */
    send(bytes, length) {
        assert.isNonNegativeInteger(length, 'length');
        if (this.#peer === null) {
            // Fire-and-forget when unbound; useful for echoing tests that ignore output.
            this.#stats.bytes_out += length;
            this.#stats.packets_out += 1;
            return;
        }

        const copy = new Uint8Array(length);
        copy.set(bytes.subarray(0, length));
        this.#peer.#in_queue.push(copy);

        this.#stats.bytes_out += length;
        this.#stats.packets_out += 1;
    }

    /**
     * Deliver every packet in the inbound queue to {@link onReceive}, in order.
     * Updates inbound stats. Returns the number of packets delivered.
     *
     * @returns {number}
     */
    deliver_all() {
        let delivered = 0;
        while (this.#in_queue.length > 0) {
            const packet = this.#in_queue.shift();
            this.#stats.bytes_in += packet.length;
            this.#stats.packets_in += 1;
            this.onReceive.send2(packet, packet.length);
            delivered++;
        }
        return delivered;
    }

    /**
     * Drop the next `n` queued inbound packets (simulates packet loss).
     * If fewer than `n` are queued, drops what's available.
     *
     * @param {number} n
     * @returns {number} actual number dropped
     */
    drop_next(n) {
        assert.isNonNegativeInteger(n, 'n');
        const actual = Math.min(n, this.#in_queue.length);
        this.#in_queue.splice(0, actual);
        return actual;
    }

    /**
     * Swap two queued inbound packets by index (simulates reorder).
     *
     * @param {number} i
     * @param {number} j
     */
    reorder(i, j) {
        assert.isNonNegativeInteger(i, 'i');
        assert.isNonNegativeInteger(j, 'j');
        if (i >= this.#in_queue.length || j >= this.#in_queue.length) {
            throw new Error(`LoopbackTransport.reorder: index out of range (queue has ${this.#in_queue.length} packets)`);
        }
        const tmp = this.#in_queue[i];
        this.#in_queue[i] = this.#in_queue[j];
        this.#in_queue[j] = tmp;
    }

    /**
     * Number of packets currently waiting to be delivered.
     * @returns {number}
     */
    queued_count() {
        return this.#in_queue.length;
    }

    /**
     * Clear the queue and unbind. Subsequent sends become no-ops. The peer is
     * also unbound so its `send()` calls don't keep accumulating into our
     * (now-undrained) inbound queue. The peer's own inbound queue is left
     * alone — the caller can still `deliver_all()` on the peer if needed.
     */
    disconnect() {
        this.#in_queue.length = 0;
        if (this.#peer !== null) {
            const p = this.#peer;
            this.#peer = null;
            if (p.#peer === this) {
                p.#peer = null;
                // Notify the peer's session-level disconnect listeners
                // so test harnesses can drive reconnect flows
                // deterministically through `disconnect()`.
                p.onDisconnect.send1('peer_disconnected');
            }
        }
        this.onDisconnect.send1('local_disconnect');
    }

    /**
     * Read-only stats snapshot.
     * @returns {{bytes_in: number, bytes_out: number, packets_in: number, packets_out: number}}
     */
    getStats() {
        return {
            bytes_in: this.#stats.bytes_in,
            bytes_out: this.#stats.bytes_out,
            packets_in: this.#stats.packets_in,
            packets_out: this.#stats.packets_out,
        };
    }
}
