// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable max-lines */
import { enable, disable } from 'farrow-pipeline/asyncHooks.node';
import {
  createPipeline,
  createAsyncPipeline,
  createContext,
  createContainer,
} from 'farrow-pipeline';
import { createManager, createAsyncManager, useRunner } from '../src/manager';
import { createWaterfall, createAsyncWaterfall } from '../src/waterfall';
import {
  createWorkflow,
  createAsyncWorkflow,
  createParallelWorkflow,
} from '../src/workflow';
import { main } from './fixtures/async/core';
import foo from './fixtures/async/base/foo';
import bar, { getBar } from './fixtures/async/base/bar';
import dFoo from './fixtures/async/dynamic/foo';
import dBar, { getNumber } from './fixtures/async/dynamic/bar';
import { sleep } from './helpers';

describe('async manager', () => {
  it('base usage', async () => {
    const manager = main.clone().usePlugin(foo).usePlugin(bar);

    expect(getBar()).toBe(0);

    const runner = await manager.init({});

    expect(getBar()).toBe(1);

    runner.preDev();

    expect(getBar()).toBe(2);

    runner.postDev();

    expect(getBar()).toBe(3);
  });

  it('should support async initializer', async () => {
    const manager = createAsyncManager();

    const countContext = createContext(0);
    const useCount = () => countContext.use().value;

    const plugin = manager.createPlugin(async () => {
      await sleep(0);
      expect(useCount()).toBe(1);
    });
    manager.usePlugin(plugin);

    manager.run(() => {
      countContext.set(1);
    });

    enable();
    await manager.init();
    disable();

    const result0 = manager.run(() => countContext.get());

    expect(result0).toBe(1);
  });

  it('support support dynamicly register', async () => {
    const manager = main.clone().usePlugin(dFoo).usePlugin(dBar);

    expect(getNumber()).toBe(0);

    const runner = await manager.init();

    expect(getNumber()).toBe(1);

    runner.preDev();

    expect(getNumber()).toBe(2);
  });

  it('could without progress hook in plugin', async () => {
    // eslint-disable-next-line @typescript-eslint/no-shadow
    const foo = createWaterfall<number>();
    const manager = createAsyncManager({ foo });

    // eslint-disable-next-line @typescript-eslint/no-empty-function
    const plugin = manager.createPlugin(() => {});
    manager.usePlugin(plugin);

    const runner = await manager.init();

    expect(runner.foo(0)).toBe(0);
  });

  describe('pre order plugin', () => {
    it('default order is right order', async () => {
      const manager = createAsyncManager();

      const list: number[] = [];
      const plugin0 = manager.createPlugin(
        () => {
          list.push(0);
        },
        { name: 'plugin0' },
      );
      const plugin1 = manager.createPlugin(
        () => {
          list.push(1);
        },
        {
          name: 'plugin1',
          pre: ['plugin0'],
        },
      );
      const plugin2 = manager.createPlugin(
        () => {
          list.push(2);
        },
        { name: 'plugin2' },
      );

      manager.usePlugin(plugin0, plugin1, plugin2);

      await manager.init();

      expect(list).toStrictEqual([0, 1, 2]);
    });

    it('default order is incorrect order', async () => {
      const manager = createAsyncManager();

      const list: number[] = [];
      const plugin0 = manager.createPlugin(
        () => {
          list.push(0);
        },
        {
          name: 'plugin0',
          pre: ['plugin1'],
        },
      );
      const plugin1 = manager.createPlugin(
        () => {
          list.push(1);
        },
        { name: 'plugin1' },
      );
      const plugin2 = manager.createPlugin(
        () => {
          list.push(2);
        },
        { name: 'plugin2' },
      );

      manager.usePlugin(plugin0, plugin1, plugin2);

      await manager.init();

      expect(list).toStrictEqual([1, 0, 2]);
    });
  });

  describe('post order plugin', () => {
    it('default order is right order', async () => {
      const manager = createAsyncManager();

      const list: number[] = [];
      const plugin0 = manager.createPlugin(
        () => {
          list.push(0);
        },
        {
          name: 'plugin0',
          post: ['plugin1'],
        },
      );
      const plugin1 = manager.createPlugin(
        () => {
          list.push(1);
        },
        { name: 'plugin1' },
      );
      const plugin2 = manager.createPlugin(
        () => {
          list.push(2);
        },
        { name: 'plugin2' },
      );

      manager.usePlugin(plugin0, plugin1, plugin2);

      await manager.init();

      expect(list).toStrictEqual([0, 1, 2]);
    });

    it('default order is incorrect order', async () => {
      const manager = createAsyncManager();

      const list: number[] = [];
      const plugin0 = manager.createPlugin(
        () => {
          list.push(0);
        },
        { name: 'plugin0' },
      );
      const plugin1 = manager.createPlugin(
        () => {
          list.push(1);
        },
        {
          name: 'plugin1',
          post: ['plugin0'],
        },
      );
      const plugin2 = manager.createPlugin(
        () => {
          list.push(2);
        },
        { name: 'plugin2' },
      );

      manager.usePlugin(plugin0, plugin1, plugin2);

      await manager.init();

      expect(list).toStrictEqual([1, 0, 2]);
    });

    it('should support more plugin', async () => {
      const manager = createAsyncManager();

      let status = 0;
      const plugin1 = manager.createPlugin(
        () => {
          status = 1;
        },
        { name: 'plugin1' },
      );

      const plugin2 = manager.createPlugin(() => {
        status = 2;
      });

      const plugin3 = manager.createPlugin(() => {
        status = 3;
      });

      const plugin4 = manager.createPlugin(
        () => {
          status = 4;
        },
        { post: ['plugin1'] },
      );

      manager.usePlugin(plugin1);
      manager.usePlugin(plugin2);
      manager.usePlugin(plugin3);
      manager.usePlugin(plugin4);

      await manager.init({});

      expect(status).toBe(1);
    });
  });

  describe('rival plugin', () => {
    it('should throw error when attaching rival plugin', async () => {
      const manager = createAsyncManager();

      let count = 0;
      const plugin0 = manager.createPlugin(
        () => {
          count = 0;
        },
        { name: 'plugin0' },
      );
      const plugin1 = manager.createPlugin(
        () => {
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          count += 1;
        },
        {
          name: 'plugin1',
          rivals: ['plugin0'],
        },
      );

      manager.usePlugin(plugin0, plugin1);

      await expect(manager.init).rejects.toThrowError();
    });

    it('should not throw error without attaching rival plugin', async () => {
      const manager = createAsyncManager();

      let count = 0;
      const plugin0 = manager.createPlugin(
        () => {
          count = 0;
        },
        { name: 'plugin0' },
      );
      const plugin1 = manager.createPlugin(
        () => {
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          count += 1;
        },
        {
          name: 'plugin1',
          rivals: ['plugin2'],
        },
      );

      manager.usePlugin(plugin0, plugin1);

      await manager.init();
    });
  });

  describe('required plugin', () => {
    it('should throw error when it is without required plugin', async () => {
      const manager = createAsyncManager();

      // eslint-disable-next-line @typescript-eslint/no-empty-function
      const plugin0 = manager.createPlugin(() => {}, {
        name: 'plugin0',
        required: ['plugin1'],
      });

      manager.usePlugin(plugin0);

      await expect(manager.init).rejects.toThrowError();
    });

    it('should not throw error without attaching rival plugin', async () => {
      const manager = createAsyncManager();

      let count = 0;
      const plugin0 = manager.createPlugin(
        () => {
          count = 0;
        },
        {
          name: 'plugin0',
          required: ['plugin1'],
        },
      );
      const plugin1 = manager.createPlugin(
        () => {
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          count += 1;
        },
        { name: 'plugin1' },
      );

      manager.usePlugin(plugin0, plugin1);

      await manager.init();
    });
  });

  it('should de-duplicate plugins', async () => {
    const manager = createAsyncManager();

    let count = 0;
    const plugin = manager.createPlugin(() => {
      count += 1;
    });

    manager.usePlugin(plugin, plugin);
    manager.usePlugin(plugin);

    await manager.init();

    expect(count).toBe(1);
  });

  it('should support manager clone', async () => {
    const manager0 = createAsyncManager();

    let count = 0;
    const plugin = manager0.createPlugin(() => {
      count += 1;
    });

    manager0.usePlugin(plugin);
    await manager0.init();

    expect(count).toBe(1);

    const manager1 = manager0.clone();
    manager1.usePlugin(plugin);
    await manager1.init();

    expect(count).toBe(2);
  });

  it('isPlugin if exclusive plugins of manager', () => {
    const manager0 = createManager();
    const manager1 = createAsyncManager();

    // eslint-disable-next-line @typescript-eslint/no-empty-function
    const plugin = manager0.createPlugin(() => {});

    expect(manager0.isPlugin(plugin)).toBeTruthy();
    expect(manager1.isPlugin(plugin)).toBeFalsy();
    expect(manager1.isPlugin({})).toBeFalsy();
    expect(manager1.isPlugin('' as any)).toBeFalsy();
  });

  it('usePlugin should only add exclusive plugins of manager', async () => {
    const manager0 = createManager();
    const manager1 = createAsyncManager();

    let count = 0;
    const plugin = manager0.createPlugin(() => {
      count += 1;
    });

    manager1.usePlugin(plugin as any);
    await manager1.init();

    expect(count).toBe(0);
  });

  it('should support clear plugins', async () => {
    const manager = createAsyncManager();

    let count = 0;
    const plugin = manager.createPlugin(() => {
      count += 1;
    });

    manager.usePlugin(plugin);
    manager.clear();

    await manager.init();

    expect(count).toBe(0);
  });

  it('should run under the internal container', async () => {
    const manager = createAsyncManager();
    const Count = createContext(0);

    const plugin = manager.createPlugin(() => {
      Count.set(1);
    });

    manager.usePlugin(plugin);

    await manager.init();
    expect(manager.run(Count.get)).toBe(1);

    const container = createContainer();

    await manager.init({ container });
    expect(manager.run(Count.get, { container })).toBe(1);
  });

  it('should support all progress', async () => {
    const manager = createAsyncManager({
      pipeline: createPipeline(),
      asyncPipeline: createAsyncPipeline(),
      waterfall: createWaterfall(),
      asyncWaterfall: createAsyncWaterfall(),
      workflow: createWorkflow(),
      asyncWorkflow: createAsyncWorkflow(),
      parallelWorkflow: createParallelWorkflow(),
    });

    const list: string[] = [];
    const plugin = manager.createPlugin(() => ({
      pipeline: () => {
        list.push('pipeline');
      },
      asyncPipeline: () => {
        list.push('asyncPipeline');
      },
      waterfall: () => {
        list.push('waterfall');
      },
      asyncWaterfall: () => {
        list.push('asyncWaterfall');
      },
      workflow: () => {
        list.push('workflow');
      },
      asyncWorkflow: () => {
        list.push('asyncWorkflow');
      },
      parallelWorkflow: () => {
        list.push('parallelWorkflow');
      },
    }));
    manager.usePlugin(plugin);

    const runner = await manager.init();

    runner.pipeline({});
    await runner.asyncPipeline({});
    runner.waterfall({});
    await runner.asyncWaterfall({});
    runner.workflow({});
    await runner.asyncWorkflow({});
    await runner.parallelWorkflow({});

    expect(list).toStrictEqual([
      'pipeline',
      'asyncPipeline',
      'waterfall',
      'asyncWaterfall',
      'workflow',
      'asyncWorkflow',
      'parallelWorkflow',
    ]);
  });

  describe('useRunner', () => {
    it('should work well', async () => {
      // eslint-disable-next-line @typescript-eslint/no-shadow
      const foo = createAsyncPipeline();
      // eslint-disable-next-line @typescript-eslint/no-shadow
      const bar = createAsyncPipeline();
      const manager = createAsyncManager({ foo, bar });

      let count = 0;

      const plugin = manager.createPlugin(() => ({
        foo: async () => {
          await sleep(0);
          const runner = manager.useRunner();
          runner.bar({});
        },
        bar: () => {
          count = 1;
        },
      }));

      manager.usePlugin(plugin);

      const runner = await manager.init();

      enable();
      await runner.foo({});
      disable();

      expect(count).toBe(1);
    });

    it('should throw error useRunner out plugin hook', async () => {
      // eslint-disable-next-line @typescript-eslint/no-shadow
      const foo = createAsyncPipeline();
      // eslint-disable-next-line @typescript-eslint/no-shadow
      const bar = createAsyncPipeline();
      const manager = createAsyncManager({ foo, bar });

      let count = 0;

      const plugin = manager.createPlugin(() => {
        const runner = manager.useRunner();

        return {
          foo: async () => {
            await sleep(0);
            runner.bar({});
          },
          bar: () => {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            count = 1;
          },
        };
      });

      manager.usePlugin(plugin);

      await expect(manager.init).rejects.toThrowError(
        new Error(
          `Can't call useRunner out of scope, it should be placed in hooks of plugin`,
        ),
      );
    });

    it('should throw error useRunner out plugin', () => {
      expect(useRunner).toThrowError(
        new Error(
          `Can't call useContainer out of scope, it should be placed on top of the function`,
        ),
      );
    });
  });
});
