/* eslint-disable no-magic-numbers */

import { jest, describe, test, expect } from '@jest/globals';

import { callsTo, anyUnexpectedCalls } from '../testing/awaitCalls.js';

import Controller from './controller.js';

function controllerWithMocks(engineURL, engineThreadURL, gameIdentifier) {
  globalThis.Worker = jest.fn().mockName('Worker').mockReturnValue({
    addEventListener: jest.fn().mockName('addEventListener'),
    postMessage: jest.fn().mockName('postMessage'),
  });
  const result = new Controller(
    engineURL,
    engineThreadURL,
    gameIdentifier,
    jest.fn().mockName('propertyHandler'),
    jest.fn().mockName('moveHandler'),
  );
  return result;
}

describe('the controller\'s automatic behaviors', () => {
  test('connect as needed when setting strength', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.setStrength(999);
    controller.setStrength(9999);
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
    ]);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['setoption strength 999'],
      ['isready'],
    ]);
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['setoption strength 999'],
      ['isready'],
      ['setoption strength 9999'],
      ['isready'],
    ]);
  });
  test('connect and start a game as needed when setting a line', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.setLine({ serialization: 'jkl' }, [{ serialization: 'mno' }, { serialization: 'pqr' }]);
    controller.setLine({ serialization: 'stu' }, []);
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
    ]);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
    ]);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
      ['setline jkl mno pqr'],
      ['isready'],
    ]);
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
      ['setline jkl mno pqr'],
      ['isready'],
      ['setline stu'],
      ['isready'],
    ]);
  });
  test('connect and start a game as needed when starting a search', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.go();
    controller.stop();
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
    ]);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
    ]);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
      ['go'],
      ['isready'],
    ]);
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
      ['go'],
      ['isready'],
      ['stop'],
      ['isready'],
    ]);
  });
});

describe('the controller\'s communication checks', () => {
  test('block communication when the engine protocol is too new', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.setLine({ serialization: 'jkl' }, []);
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
    ]);
    controller.receiveMessage('protocol 9999.9.9');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await anyUnexpectedCalls();
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
    ]);
  });
  test('block a game line if the engine does not know the game', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.setLine({ serialization: 'jkl' }, []);
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
    ]);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
    ]);
    controller.receiveMessage('unknown');
    controller.receiveMessage('readyok');
    await anyUnexpectedCalls();
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
    ]);
  });
  test('block starting an already started search', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.go();
    controller.go();
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
    ]);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
    ]);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
      ['go'],
      ['isready'],
    ]);
    controller.receiveMessage('readyok');
    await anyUnexpectedCalls();
    expect(controller.worker.postMessage.mock.calls).toEqual([
      ['bei def'],
      ['isready'],
      ['newgame ghi'],
      ['isready'],
      ['go'],
      ['isready'],
    ]);
  });
  test('block stopping an already stopped search', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.stop();
    await anyUnexpectedCalls();
    expect(controller.worker.postMessage.mock.calls).toEqual([]);
  });
});

describe('the controller\'s callbacks', () => {
  test('report engine properties', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.setStrength(999);
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('id name jkl');
    controller.receiveMessage('id author mno');
    controller.receiveMessage('id version pqr');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    expect(controller.propertyHandler.mock.calls).toEqual([
      ['name', 'jkl'],
      ['author', 'mno'],
      ['version', 'pqr'],
    ]);
  });
  test('report single moves', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move jkl');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
    ]);
  });
  test('report consecutive moves', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move jkl');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
    ]);
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move mno');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
      ['mno'],
    ]);
  });
  test('do not report unwanted moves from stopped searches', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.stop();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move jkl');
    expect(controller.moveHandler.mock.calls).toEqual([]);
    controller.stop();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move mno');
    expect(controller.moveHandler.mock.calls).toEqual([]);
    controller.receiveMessage('move pqr');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['pqr'],
    ]);
  });
  test('do report wanted moves from stopped searches', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.stop(true);
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move jkl');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
    ]);
    controller.stop(true);
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move mno');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
      ['mno'],
    ]);
    controller.receiveMessage('move pqr');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
      ['mno'],
      ['pqr'],
    ]);
  });
  test('do report late-arriving wanted moves from stopped searches', async() => {
    const controller = controllerWithMocks('abc', 'def', 'ghi');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('protocol 1.0.0');
    controller.receiveMessage('beiok');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('known');
    controller.receiveMessage('readyok');
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.stop(true);
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.stop(false);
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.go();
    await callsTo(controller.worker.postMessage);
    controller.receiveMessage('readyok');
    controller.receiveMessage('move jkl');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
    ]);
    controller.receiveMessage('move mno');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
    ]);
    controller.receiveMessage('move pqr');
    expect(controller.moveHandler.mock.calls).toEqual([
      ['jkl'],
      ['pqr'],
    ]);
  });
});