// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2023 The Pybricks Authors

import PushStream from 'zen-push';
import { AsyncSaga, delay } from '../../test';
import {
    didFailToWrite,
    didNotify,
    didWrite,
    write,
} from '../ble-nordic-uart-service/actions';
import {
    didFailToSendCommand,
    didReceiveWriteStdout,
    didSendCommand,
    sendWriteStdinCommand,
} from '../ble-pybricks-service/actions';
import { checksum, hubDidStartRepl } from '../hub/actions';
import { HubRuntimeState } from '../hub/reducers';
import { createCountFunc } from '../utils/iter';
import { receiveData, sendData } from './actions';
import terminal from './sagas';

const encoder = new TextEncoder();

describe('receiving stdout from hub', () => {
    test('legacy UART message', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({ hub: { useLegacyDownload: true, useLegacyStdio: true } });

        // sending ASCII space character
        saga.put(didNotify(new DataView(new Uint8Array([0x20]).buffer)));

        await expect(saga.take()).resolves.toEqual(sendData(' '));

        await saga.end();
    });

    test('legacy download checksum message', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });

        saga.updateState({
            hub: {
                runtime: HubRuntimeState.Loading,
                useLegacyDownload: true,
                useLegacyStdio: true,
            },
        });

        saga.put(didNotify(new DataView(new Uint8Array([0xaa]).buffer)));

        await expect(saga.take()).resolves.toEqual(checksum(0xaa));

        await saga.end();
    });

    test('Pybricks Profile v1.3.0 ignores UART service', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });

        saga.updateState({
            hub: {
                useLegacyDownload: false,
                useLegacyStdio: false,
            },
        });

        saga.put(didNotify(new DataView(new Uint8Array([0x20]).buffer)));

        await delay(50);

        // no further actions should be pending
        await saga.end();
    });

    test('Pybricks Profile v1.3.0 write stdout command', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });

        saga.updateState({
            hub: {
                useLegacyDownload: false,
                useLegacyStdio: false,
            },
        });

        saga.put(didReceiveWriteStdout(new Uint8Array([0x20]).buffer));

        await expect(saga.take()).resolves.toEqual(sendData(' '));

        await saga.end();
    });
});

test('Terminal data source responds to send data actions', async () => {
    const dataSource = new PushStream<string>();
    const saga = new AsyncSaga(terminal, {
        nextMessageId: createCountFunc(),
        terminal: { dataSource },
    });

    const data = new Array<string>();
    dataSource.observable.subscribe({ next: (v) => data.push(v) });

    saga.put(sendData('1'));
    saga.put(sendData('2'));
    saga.put(sendData('3'));

    expect(data.length).toBe(3);
    expect(data[0]).toBe('1');
    expect(data[1]).toBe('2');
    expect(data[2]).toBe('3');

    await saga.end();
});

describe('Terminal data source responds to receive data actions', () => {
    const expected = encoder.encode('test1234');

    test('basic function works', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: {
                runtime: HubRuntimeState.Running,
                useLegacyStdio: false,
                maxBleWriteSize: 20,
            },
        });

        saga.put(receiveData('test1234'));

        await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(0, expected));

        await saga.end();
    });

    test('messages are queued until previous has completed', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: {
                runtime: HubRuntimeState.Running,
                useLegacyStdio: false,
                maxBleWriteSize: 20,
            },
        });

        saga.put(receiveData('test1234'));
        await delay(50); // without delay, messages are combined
        saga.put(receiveData('test1234'));

        // second message is queued until didSend or didFailToSend
        expect(saga.numPending()).toBe(1);

        await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(0, expected));

        // second message is queued until didSend or didFailToSend
        expect(saga.numPending()).toBe(0);

        saga.put(didSendCommand(0));

        await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(1, expected));

        saga.put(didSendCommand(1));

        await saga.end();
    });

    test('messages are queued until previous has failed', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: {
                runtime: HubRuntimeState.Running,
                useLegacyStdio: false,
                maxBleWriteSize: 20,
            },
        });

        saga.put(receiveData('test1234'));
        await delay(50); // without delay, messages are combined
        saga.put(receiveData('test1234'));

        // second message is queued until didSend or didFailToSend
        expect(saga.numPending()).toBe(1);

        await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(0, expected));

        // second message is queued until didSend or didFailToSend
        expect(saga.numPending()).toBe(0);

        saga.put(didFailToSendCommand(0, new Error('test error')));

        await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(1, expected));

        saga.put(didSendCommand(1));

        await saga.end();
    });

    test('small messages are combined', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: {
                runtime: HubRuntimeState.Running,
                useLegacyStdio: false,
                maxBleWriteSize: 20,
            },
        });

        saga.put(receiveData('test1234'));
        saga.put(receiveData('test1234'));

        await expect(saga.take()).resolves.toEqual(
            sendWriteStdinCommand(0, new Uint8Array([...expected, ...expected])),
        );

        await saga.end();
    });

    test('long messages are split', async () => {
        const testData = '012345678901234567890123456789';

        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: {
                runtime: HubRuntimeState.Running,
                useLegacyStdio: false,
                maxBleWriteSize: 20,
            },
        });

        saga.put(receiveData(testData));

        await expect(saga.take()).resolves.toEqual(
            sendWriteStdinCommand(0, encoder.encode(testData.slice(0, 20))),
        );

        saga.put(didSendCommand(0));

        await expect(saga.take()).resolves.toEqual(
            sendWriteStdinCommand(1, encoder.encode(testData.slice(20, 40))),
        );

        saga.put(didSendCommand(1));

        await saga.end();
    });

    test('if user program is not running echo BEL', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: {
                runtime: HubRuntimeState.Disconnected,
                useLegacyStdio: false,
                maxBleWriteSize: 20,
            },
        });

        saga.put(receiveData('test1234'));

        await delay(50);

        // sends BEL character on error
        await expect(saga.take()).resolves.toEqual(sendData('\x07'));

        await saga.end();
    });
});

describe('Terminal data source responds to receive data actions (legacy)', () => {
    const expected = encoder.encode('test1234');

    test('basic function works', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
        });

        saga.put(receiveData('test1234'));

        const action = await saga.take();
        expect(action).toEqual(write(0, expected));

        await saga.end();
    });

    test('messages are queued until previous has completed', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
        });

        saga.put(receiveData('test1234'));
        await delay(50); // without delay, messages are combined
        saga.put(receiveData('test1234'));

        // second message is queued until didWrite or didFailToWrite
        expect(saga.numPending()).toBe(1);

        const action = await saga.take();
        expect(action).toEqual(write(0, expected));

        // second message is queued until didWrite or didFailToWrite
        expect(saga.numPending()).toBe(0);

        saga.put(didWrite(0));

        const action2 = await saga.take();
        expect(action2).toEqual(write(1, expected));

        saga.put(didWrite(1));

        await saga.end();
    });

    test('messages are queued until previous has failed', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
        });

        saga.put(receiveData('test1234'));
        await delay(50); // without delay, messages are combined
        saga.put(receiveData('test1234'));

        // second message is queued until didWrite or didFailToWrite
        expect(saga.numPending()).toBe(1);

        const action = await saga.take();
        expect(action).toEqual(write(0, expected));

        // second message is queued until didWrite or didFailToWrite
        expect(saga.numPending()).toBe(0);

        saga.put(didFailToWrite(0, new Error('test error')));

        const action2 = await saga.take();
        expect(action2).toEqual(write(1, expected));

        saga.put(didWrite(1));

        await saga.end();
    });

    test('small messages are combined', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
        });

        saga.put(receiveData('test1234'));
        saga.put(receiveData('test1234'));

        const action = await saga.take();
        expect(action).toEqual(write(0, new Uint8Array([...expected, ...expected])));

        await saga.end();
    });

    test('long messages are split', async () => {
        const testData = '012345678901234567890123456789';

        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
        });

        saga.put(receiveData(testData));

        const action = await saga.take();
        expect(action).toEqual(write(0, encoder.encode(testData.slice(0, 20))));

        saga.put(didWrite(0));

        const action2 = await saga.take();
        expect(action2).toEqual(write(1, encoder.encode(testData.slice(20, 40))));

        saga.put(didWrite(1));

        await saga.end();
    });

    test('if user program is not running, echo BEL', async () => {
        const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
        saga.updateState({
            hub: { runtime: HubRuntimeState.Disconnected, useLegacyStdio: true },
        });

        saga.put(receiveData('test1234'));

        await delay(50);

        // sends BEL character on error
        await expect(saga.take()).resolves.toEqual(sendData('\x07'));

        await saga.end();
    });
});

it('should focus terminal when repl is started', async () => {
    const dispatchEventSpy = jest.spyOn(window, 'dispatchEvent');

    const saga = new AsyncSaga(terminal);

    saga.put(hubDidStartRepl());

    // https://stackoverflow.com/a/64787979/1976323
    expect(dispatchEventSpy.mock.calls[0][0].type).toBe('pb-terminal-focus');

    await saga.end();
});
