import { describe, expect, it, jest } from '@jest/globals';
import { Message, Request, ResponseController, Response, MessageType } from '../js/medium/protocol.js';


describe('Message represents a generic notification', () => {

    it ('exposes meta data (channel, name, timestamp, id) as readonly properties', () => {

        const meta = {
            channel: 'chan',
            timestamp: 123456,
            name: 'test',
            id: '123456:0,-'
        }        
        const msg = new Message(null, meta);

        for (const property in meta) {
            expect(msg).toHaveProperty(property, meta[property]);
            expect(() => { msg[property] = null; }).toThrowError(TypeError);
        }

    })

    it('exposes message content via a readonly property "payload"', () => {
        const content = { value: 42 };
        const msg = new Message(content, {});

        expect(msg).toHaveProperty('payload', content);
        expect(() => { msg.payload = null; }).toThrowError(TypeError);
    })

})

describe('Request represents a request received by the addressee', () => {
    
    const buildMockMessenger = () => ({
        sendRaw: jest.fn(() => Promise.resolve()),
        mockClear() { this.sendRaw.mockClear(); }
    });

    it('extends the Message class with readonly properties requestr and requestId', () => {
        const meta = {
            channel: 'chan',
            timestamp: 123456,
            name: 'test',
            id: '123456:0,-',
            reply: 'rchan',
            reqId: 'req12345'
        }        
        const msg = new Request(null, meta, null);

        expect(msg).toBeInstanceOf(Message);

        expect(msg).toHaveProperty('requester', meta.reply);
        expect(() => { msg.requester = null; }).toThrowError(TypeError);

        expect(msg).toHaveProperty('requestId', meta.reqId);
        expect(() => { msg.requestId = null; }).toThrowError(TypeError);
    })

    it('notify() sends a progress message to the requester channel with requestId', async () => {
        const meta = { reply: 'rchan', reqId: 'req12345' };
        const messenger = buildMockMessenger();
        const msg = new Request(null, meta, messenger);

        await msg.notify('test');
        expect(messenger.sendRaw).toBeCalled();

        const sent = messenger.sendRaw.mock.calls[0];
        expect(sent[1]).toEqual([ meta.reply ]);
        expect(sent[0]).toMatchObject({
            type: MessageType.PROGRESS,
            reqId: meta.reqId,
            payload: 'test'
        });
    })

    it('reply() sends a response message to the requester channel with requestId', async () => {
        const meta = { reply: 'rchan', reqId: 'req12345' };
        const messenger = buildMockMessenger();
        const msg = new Request(null, meta, messenger);

        await msg.respond('test');
        expect(messenger.sendRaw).toBeCalled();

        const sent = messenger.sendRaw.mock.calls[0];
        expect(sent[1]).toEqual([ meta.reply ]);
        expect(sent[0]).toMatchObject({
            type: MessageType.RESPONSE,
            reqId: meta.reqId,
            payload: 'test'
        });
    })

    it('reply() marks the request as done: following calls to reply() and notify() throw', async () => {
        const meta = { reply: 'rchan', reqId: 'req12345' };
        const messenger = buildMockMessenger();
        const msg = new Request(null, meta, messenger);
        await msg.respond('test');
        messenger.mockClear();
        
        await expect(msg.notify('test')).rejects.toThrow();
        await expect(msg.respond('test')).rejects.toThrow();

        expect(messenger.sendRaw).not.toBeCalled();
    })
})

describe('ResponseController delivers a future response', () => {

    it('response is an associated Response object', () => {
        const ctl = new ResponseController({});
        expect(ctl.response).toBeInstanceOf(Response);
        // all calls return the same object
        expect(ctl.response).toStrictEqual(ctl.response);
    })

    it('notify(content) dispatches a "progress" event on the associated response', () => {
        const handler = jest.fn();
        const payload = { content: 'test' };
        const ctl = new ResponseController({});

        ctl.response.onprogress = handler;
        ctl.notify(payload);

        expect(handler).toBeCalled();
        const event = handler.mock.calls[0][0];
        expect(event).toBeInstanceOf(MessageEvent);
        expect(event.type).toEqual('progress');
        expect(event.data).toStrictEqual(payload);
        expect(event.target).toEqual(ctl.response);
    })

    it('close(content) resolves the response.payload promise', async () => {
        const handler = jest.fn();
        const ctl = new ResponseController({});
        const payload = { content: 'test' };
        const promise = ctl.response.payload();

        ctl.close(payload);
        await expect(promise).resolves.toStrictEqual(payload);
    })

    it('response.close returns true once the response is received', () => {
        const ctl = new ResponseController({});
        expect(ctl.response.closed).toBeFalsy();
        ctl.close();
        expect(ctl.response.closed).toBeTruthy();
    })

    it('notify() and close() takes no effect once the response is closed', async () => {
        const ctl = new ResponseController({});
        const handler = jest.fn();

        ctl.close('first');
        ctl.response.onprogress = handler;

        ctl.notify('test');
        ctl.close('test');

        expect(handler).not.toBeCalled();
        await expect(ctl.response.payload()).resolves.toEqual('first');
    })

})

