import { ApproxStructure, Assertions, Chain, Step, UiFinder } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock-client';
import { Arr, Optional } from '@ephox/katamari';

import * as AddEventsBehaviour from 'ephox/alloy/api/behaviour/AddEventsBehaviour';
import * as Behaviour from 'ephox/alloy/api/behaviour/Behaviour';
import { Reflecting } from 'ephox/alloy/api/behaviour/Reflecting';
import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory';
import * as AlloyEvents from 'ephox/alloy/api/events/AlloyEvents';
import { Container } from 'ephox/alloy/api/ui/Container';
import * as GuiSetup from 'ephox/alloy/test/GuiSetup';

UnitTest.asynctest('ReflectingTest', (success, failure) => {

  GuiSetup.setup((store, _doc, _body) => {
    const makeChild = (label: string) => ({
      dom: {
        tag: 'span',
        innerHtml: label,
        styles: {
          display: 'inline-block',
          border: '1px solid #ccc',
          margin: '0.5em',
          padding: '1em'
        }
      },
      behaviours: Behaviour.derive([
        AddEventsBehaviour.config('child-events', [
          AlloyEvents.runOnAttached(store.adder('child.' + label + '.attached')),
          AlloyEvents.runOnDetached(store.adder('child.' + label + '.detached'))
        ])
      ])
    });

    return GuiFactory.build(
      Container.sketch({
        components: [
          {
            dom: {
              tag: 'div',
              classes: [ 'reflector-1' ]
            },
            components: [
              makeChild('state-changes-only')
            ],
            behaviours: Behaviour.derive([
              Reflecting.config({
                channel: 'channel-1',
                updateState: (_, input) => Optional.some({ state: input })
              })
            ])
          },
          {
            dom: {
              tag: 'div',
              classes: [ 'reflector-2a' ]
            },
            components: [ ],
            behaviours: Behaviour.derive([
              Reflecting.config({
                channel: 'channel-2',
                renderComponents: (input) => Arr.map(input, makeChild),
                initialData: [ '2a-cat', '2a-dog' ]
              })
            ])
          },
          {
            dom: {
              tag: 'div',
              classes: [ 'reflector-2b' ]
            },
            components: [
              makeChild('render-only-inital-component')
            ],
            behaviours: Behaviour.derive([
              Reflecting.config({
                channel: 'channel-2',
                renderComponents: (input) => Arr.map(input, makeChild),
                initialData: [ '2b-cat', '2b-dog' ]
              })
            ])
          },
          {
            dom: {
              tag: 'div',
              classes: [ 'reflector-2c' ]
            },
            components: [ ],
            behaviours: Behaviour.derive([
              Reflecting.config({
                channel: 'channel-2',
                renderComponents: (input) => Arr.map(input, makeChild)
              })
            ])
          },

          {
            dom: {
              tag: 'div',
              classes: [ 'reflector-3' ]
            },
            components: [ ],
            behaviours: Behaviour.derive([
              Reflecting.config({
                channel: 'channel-3',
                renderComponents: (_input, state) => Arr.map(state.map((s) => s.state).getOr([ ]), makeChild),
                updateState: (_c, input) => Optional.some({ state: input })
              })
            ])
          }
        ]
      })
    );
  }, (_doc, _body, gui, component, store) => {
    const sAssertReflectState = (label: string, expected: any, selector: string) => Chain.asStep(component.element, [
      UiFinder.cFindIn(selector),
      Chain.binder(component.getSystem().getByDom),
      Chain.op((r1) => {
        const actual = Reflecting.getState(r1).get().getOrDie();
        Assertions.assertEq('Checking state for: ' + label, expected, actual.state);
      })
    ]);

    return [
      store.sAssertEq('Checking the original sequence of attached and detached', [
        'child.state-changes-only.attached',
        'child.2a-cat.attached',
        'child.2a-dog.attached',

        // This one is removed immediately due to initialData being set
        'child.render-only-inital-component.attached',
        'child.render-only-inital-component.detached',
        'child.2b-cat.attached',
        'child.2b-dog.attached'
      ]),
      store.sClear,
      Assertions.sAssertStructure(
        'Checking initial structure',
        ApproxStructure.build((s, str, _arr) => s.element('div', {
          children: [
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('state-changes-only')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('2a-cat')) ] }),
                s.element('span', { children: [ s.text(str.is('2a-dog')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('2b-cat')) ] }),
                s.element('span', { children: [ s.text(str.is('2b-dog')) ] })
              ]
            }),
            s.element('div', {
              children: [ ]
            }),
            s.element('div', {
              children: [ ]
            })
          ]
        })),
        component.element
      ),

      Step.sync(() => {
        gui.broadcastOn([ 'channel-1' ], { blah: true });
      }),

      sAssertReflectState('reflector 1', { blah: true }, '.reflector-1'),
      store.sAssertEq('No attached/detached should have occurred', [ ]),

      Step.sync(() => {
        gui.broadcastOn([ 'channel-2' ], [ 'alpha', 'beta' ]);
      }),

      store.sAssertEq('Attached and detached should have occurred when broadcasting on channel 2', [
        'child.2a-cat.detached',
        'child.2a-dog.detached',
        'child.alpha.attached',
        'child.beta.attached',
        'child.2b-cat.detached',
        'child.2b-dog.detached',
        'child.alpha.attached',
        'child.beta.attached',
        'child.alpha.attached',
        'child.beta.attached'
      ]),

      Assertions.sAssertStructure(
        'Checking structure after broadcast on channel-2',
        ApproxStructure.build((s, str, _arr) => s.element('div', {
          children: [
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('state-changes-only')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('alpha')) ] }),
                s.element('span', { children: [ s.text(str.is('beta')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('alpha')) ] }),
                s.element('span', { children: [ s.text(str.is('beta')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('alpha')) ] }),
                s.element('span', { children: [ s.text(str.is('beta')) ] })
              ]
            }),
            s.element('div', {
              children: [ ]
            })
          ]
        })),
        component.element
      ),

      Step.sync(() => {
        gui.broadcastOn([ 'channel-3' ], [ 'gamma' ]);
      }),

      Assertions.sAssertStructure(
        'Checking structure after broadcast on channel-3',
        ApproxStructure.build((s, str, _arr) => s.element('div', {
          children: [
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('state-changes-only')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('alpha')) ] }),
                s.element('span', { children: [ s.text(str.is('beta')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('alpha')) ] }),
                s.element('span', { children: [ s.text(str.is('beta')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('alpha')) ] }),
                s.element('span', { children: [ s.text(str.is('beta')) ] })
              ]
            }),
            s.element('div', {
              children: [
                s.element('span', { children: [ s.text(str.is('gamma')) ] })
              ]
            })
          ]
        })),
        component.element
      ),

      sAssertReflectState('reflector3', [ 'gamma' ], '.reflector-3')
    ];
  }, () => success(), failure);
});
