import { describe, expect, it } from '@jest/globals';
import { parseMessageId, parseWebSocketMessage, publish } from '../js/medium/nchan.js';

import MockFetch from './mocks/fetch.js';

describe('parseMessageId(msgIs)', () => {

    it('extracts timestamp and channel position from a message id string', () => {

        expect(parseMessageId('31234234:1')).toMatchObject({
            timestamp: 31234234,
            channel: 0
        })
        expect(parseMessageId('132133423:-,[1],-')).toMatchObject({
            timestamp: 132133423,
            channel: 1
        })
        expect(parseMessageId('31873261:3,1,[12]')).toMatchObject({
            timestamp: 31873261,
            channel: 2
        })
        expect(parseMessageId('31873261:-,-,[0]')).toMatchObject({
            timestamp: 31873261,
            channel: 2
        })
    })

})

describe('parseWebSocketMessage(msg) = { headers, timestamp, channel, payload }', () => {

    const message = parseWebSocketMessage([
        'id: 123432332:-,[0],1',
        'content-type: text/json',
        'my-header: some text',
        '',
        'message data'
    ].join('\n'));

    it('headers is a dictionary of message headers', () => {
        expect(message.headers).toMatchObject({
            'id': '123432332:-,[0],1',
            'content-type': 'text/json',
            'my-header': 'some text'
        })
    })

    it('payload is a string with the message content' , () => {
        expect(message.payload).toEqual("message data");
    })

    it('timestamp and channel are obtained from the id of the message', () => {
        const { timestamp, channel } = parseMessageId(message.headers.id);
        expect(message.timestamp).toEqual(timestamp);
        expect(message.channel).toEqual(channel);
    })

})

describe('publish(url, payload, options)', () => {

    // Create a mocked Fetch API
    MockFetch.setUp({ a:42 }, { status:201 });

    const url = 'https://test.com/path/to/resource';
    const payload = 'some payload';

    it ('makes a POST request to {url} with cookies and the given payload', async () => {        
        await publish(url, payload);
        expect(fetch).toBeCalled();

        const [ argUrl, argOptions ] = fetch.mock.calls[0];
        expect(argUrl).toEqual(url);
        expect(argOptions).toHaveProperty('method', 'POST');
        expect(argOptions).toHaveProperty('body', payload);
        expect(argOptions).toHaveProperty('credentials', 'include');
    })

    it ('sends headers for JSON content/response format', async () => {
        await publish(url, payload);

        const argOptions = fetch.mock.calls[0][1];
        expect(argOptions.headers['Content-Type']).toMatch(/(text|application)\/json/);
        expect(argOptions.headers['Accept']).toMatch(/(text|application|\*)\/json/);
    })

    it ('options.headers inserts additional headers, but Accept cannot be overriden', async () => {
        const customHeaders = {
            'Content-Type': 'text/plain',
            'X-Custom': 42,
            'X-Other': 'some value',
            'Accept': 'text/plain'
        };
        
        await publish(url, payload, { headers: customHeaders });
        // Reset Accept header and check the options for fetch
        customHeaders['Accept'] = 'text/json';
        expect(fetch.mock.calls[0][1].headers).toEqual(customHeaders);
    })

    it ('options.eventName overwrites X-EventSource-Event header', async () => {
        await publish(url, payload, { eventName: 'expected' });
        await publish(url, payload, {
            headers: { 'X-EventSource-Event': 'overwritten' },
            eventName: 'expected'
        });
        expect(fetch.mock.calls[0][1].headers).toHaveProperty('X-EventSource-Event', 'expected');
        expect(fetch.mock.calls[1][1].headers).toHaveProperty('X-EventSource-Event', 'expected');
    })

    it ('options.authorization adds Authorization header', async () => {
        await publish(url, payload, { authorization: 'expected' });
        await publish(url, payload, {
            headers: { 'Authorization': 'overwritten' },
            authorization: 'expected'
        });
        expect(fetch.mock.calls[0][1].headers).toHaveProperty('Authorization', 'expected');
        expect(fetch.mock.calls[1][1].headers).toHaveProperty('Authorization', 'expected');
    })

    it ('options.signal adds an abort signal object', async () => {
        const controller = new AbortController();
        await publish(url, payload, { signal: 'not a signal' });
        await publish(url, payload, { signal: controller.signal });
        expect(fetch.mock.calls[0][1]).not.toHaveProperty('signal');
        expect(fetch.mock.calls[1][1]).toHaveProperty('signal', controller.signal);
    })

    it.each([201,202])('resolves on HTTP response code %i', async (status) => {
        const response = { value: status };
        MockFetch.mockJsonOnce(response, { status });
        await expect(publish(url, payload)).resolves.toEqual(response);
        expect(fetch.mock.results[0].type).toEqual('return');
        expect(await fetch.mock.results[0].value).toHaveProperty('status', status);
    })

    it.each([101,304,401,404,502])('throws on HTTP response code %i', async (status) => {
        const response = { value: status };
        MockFetch.mockJsonOnce(response, { status });
        await expect(publish(url, payload)).rejects.toThrowError(Error);
        expect(fetch.mock.results[0].type).toEqual('return');
        expect(await fetch.mock.results[0].value).toHaveProperty('status', status);
    })

    it ('propagates exceptions from fetch', async () => {
        const error = Error('custom error');
        MockFetch.mockExceptionOnce(error);
        await expect(publish(url, payload)).rejects.toThrowError(error);
    })

})
