const { describe, it } = require('node:test');
const assert = require('node:assert');
const connection = require('../lib/connection');
const Frames = connection.Connection;
const HEARTBEAT = require('../lib/frame').HEARTBEAT;
const Stream = require('node:stream');
const PassThrough = Stream.PassThrough;
const { choice, forAll, repeat, label, sequence, transform, sized } = require('claire');
const amqp = require('./lib/data');
const assertEqualModuloDefaults = require('./lib/util').assertEqualModuloDefaults;
const defs = require('../lib/defs');

// We'll need to supply a stream which we manipulate ourselves
function inputs() {
  // don't coalesce buffers, since that could mess up properties (e.g., encoded frame size)
  return new PassThrough({ objectMode: true });
}

const HB = Buffer.from([
  defs.constants.FRAME_HEARTBEAT,
  0,
  0, // channel 0
  0,
  0,
  0,
  0, // zero size
  defs.constants.FRAME_END,
]);

describe('Frame', () => {

  describe('Explicit parsing', () => {
    it('Parse heartbeat', () => {
      const input = inputs();
      const frames = new Frames(input);
      input.write(HB);
      assert.ok(frames.recvFrame() === HEARTBEAT);
      assert.ok(!frames.recvFrame());
    });

    it('Parse partitioned', () => {
      const input = inputs();
      const frames = new Frames(input);
      input.write(HB.subarray(0, 3));
      assert.ok(!frames.recvFrame());
      input.write(HB.subarray(3));
      assert.ok(frames.recvFrame() === HEARTBEAT);
      assert.ok(!frames.recvFrame());
    });

    function testBogusFrame(name, bytes) {
      it(name, (_t, done) => {
        const input = inputs();
        const frames = new Frames(input);
        frames.frameMax = 5; //for the max frame test
        input.write(Buffer.from(bytes));
        frames.step((err, _frame) => {
          if (err != null) done();
          else assert.fail('Was a bogus frame!');
        });
      });
    }

    testBogusFrame('Wrong sized frame', [
      defs.constants.FRAME_BODY,
      0,
      0,
      0,
      0,
      0,
      0, // zero length
      65, // but a byte!
      defs.constants.FRAME_END,
    ]);

    testBogusFrame('Unknown method frame', [
      defs.constants.FRAME_METHOD,
      0,
      0,
      0,
      0,
      0,
      4,
      0,
      0,
      0,
      0, // garbage ID
      defs.constants.FRAME_END,
    ]);
  });

  const Trace = label('frame trace', repeat(choice.apply(choice, amqp.methods)));

  describe('Parsing', () => {
    function testPartitioning(partition) {
      return forAll(Trace)
        .satisfy((t) => {
          const bufs = [];
          const input = inputs();
          const frames = new Frames(input);
          let i = 0;
          let ex;
          frames.accept = (f) => {
            // A minor hack to make sure we get the assertion exception;
            // otherwise, it's just a test that we reached the line
            // incrementing `i` for each frame.
            try {
              assertEqualModuloDefaults(t[i], f.fields);
            } catch (e) {
              ex = e;
            }
            i++;
          };

          t.forEach((f) => {
            f.channel = 0;
            bufs.push(defs.encodeMethod(f.id, 0, f.fields));
          });

          partition(bufs).forEach((chunk) => input.write(chunk));
          frames.acceptLoop();
          if (ex) throw ex;
          return i === t.length;
        })
        .asTest({ times: 20 });
    }

    it('Parse trace of methods', testPartitioning((bufs) => bufs));

    it("Parse concat'd methods", testPartitioning((bufs) => [Buffer.concat(bufs)]));

    it('Parse partitioned methods', testPartitioning((bufs) => {
      const full = Buffer.concat(bufs);
      const onethird = Math.floor(full.length / 3);
      const twothirds = 2 * onethird;
      return [full.subarray(0, onethird), full.subarray(onethird, twothirds), full.subarray(twothirds)];
    }));
  });

  const FRAME_MAX_MAX = 4096 * 4;
  const FRAME_MAX_MIN = 4096;
  const FrameMax = amqp.rangeInt('frame max', FRAME_MAX_MIN, FRAME_MAX_MAX);
  const Body = sized((_n) => Math.floor(Math.random() * FRAME_MAX_MAX), repeat(amqp.Octet));

  const Content = transform(
    (args) => ({
      method: args[0].fields,
      header: args[1].fields,
      body: Buffer.from(args[2]),
    }),
    sequence(amqp.methods['BasicDeliver'], amqp.properties['BasicProperties'], Body),
  );

  describe('Content framing', () => {
    it('Adhere to frame max', forAll(Content, FrameMax)
      .satisfy((content, max) => {
        const input = inputs();
        const frames = new Frames(input);
        frames.frameMax = max;
        frames.sendMessage(0, defs.BasicDeliver, content.method, defs.BasicProperties, content.header, content.body);
        let _i = 0;
        let largest = 0;
        let frame = input.read();
        while (frame) {
          _i++;
          if (frame.length > largest) largest = frame.length;
          if (frame.length > max) {
            return false;
          }
          frame = input.read();
        }
        // The ratio of frames to 'contents' should always be >= 2
        // (one properties frame and at least one content frame); > 2
        // indicates fragmentation. The largest is always, of course <= frame max
        //console.log('Frames: %d; frames per message: %d; largest frame %d', _i, _i / t.length, largest);
        return true;
      })
      .asTest());
  });
});
